├── .DS_Store ├── .eslintrc.json ├── .github └── workflows │ └── webpack.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── _redirects ├── api └── index.js ├── backend ├── controllers │ └── databaseController.js └── database.js ├── build ├── 35ae8ea12ef4615c3a89b6e5c4e1ef68.png ├── 51b74e752554fd943e5a940ff2585cf1.png ├── 6ad3da20dc4ca6df8055441bb10b08c0.png ├── 73182608525d9c4ce826eb83ab08cc2c.png ├── 86d1cc04d1d932b90ccb274cfbd73c9d.png ├── be9deffa546db0002bdf1aa25889294b.png ├── bundle.js ├── bundle.js.LICENSE.txt ├── d0e9bcc1cfc585f01ffff655b9635c61.png ├── ea6c55aa49e89097447f82022520ab93.png └── index.html ├── frontend ├── .DS_Store ├── App.jsx ├── apiSlice.js ├── assets │ ├── copy_icon.png │ ├── copy_icon_copied.png │ ├── dropdown_icon.png │ ├── dropdown_icon_white.png │ ├── favicon.png │ ├── info_icon.png │ ├── play_icon_black.png │ ├── play_icon_white.png │ ├── readmeScreenshots │ │ ├── erd_screenshot_readme.png │ │ ├── fullshot_screenshot_readme.png │ │ ├── hosted_connection_login_screenshot_readme.png │ │ ├── join_added_to_query_screenshot_readme.png │ │ ├── join_screenshot_readme.png │ │ ├── join_selection_screenshot_readme.png │ │ ├── landing_page_screenshot_readme.png │ │ ├── local_connection_login_screenshot_readme.png │ │ ├── query_result_table_screenshot_readme.png │ │ ├── query_results_screenshot_readme.png │ │ └── your_query_screenshot_readme.png │ ├── yesql_logo.png │ └── yesql_logo_2.png ├── index.js ├── querySlice.js ├── react │ ├── components │ │ ├── ClauseDropdown.jsx │ │ ├── DBFlow.jsx │ │ ├── DBForm.jsx │ │ ├── DBQuery.jsx │ │ ├── JoinModal.jsx │ │ ├── LandingPageText.jsx │ │ ├── Navbar.jsx │ │ ├── OnColumnsModal.jsx │ │ ├── TestResults.jsx │ │ └── TestResultsModal.jsx │ ├── containers │ │ ├── DBFlowContainer.jsx │ │ ├── FormPage.jsx │ │ ├── HostedFormPage.jsx │ │ └── Landing.jsx │ ├── customNode.js │ ├── dropdownData.jsx │ └── tableNode.js ├── store.js └── styles │ └── index.scss ├── index.html ├── package-lock.json ├── package.json ├── vercel.json └── webpack.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/.DS_Store -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | // "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "google", 9 | "plugin:react/recommended" 10 | ], 11 | "parserOptions": { 12 | "ecmaVersion": "latest", 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "react" 17 | ], 18 | "rules": { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/webpack.yml: -------------------------------------------------------------------------------- 1 | name: NodeJS with Webpack 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [14.x, 16.x, 18.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | 25 | - name: Build 26 | run: | 27 | npm install 28 | npm run build 29 | npx webpack 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .vercel 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Open Source Labs Beta 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yesql 2 | ![main logo](./frontend/assets/yesql_logo.png) 3 | 4 | Welcome to Yesql! This application allows you to connect your own PostgreSQL database and optimize your SQL select queries by rendering your database into an interactive entity-relationship diagram (ERD) and testing your results for speed and accuracy. With the confidence that comes from visualizing your entire database and the connections between tables, you will now be able to intuitively craft queries that return precisely the data you need without slowing down your process, and you can copy that query directly into your own codebase. 5 | 6 | ## Getting Started 7 | 8 | To get started, fork and clone this repository and run “npm install.” 9 | 10 | Start the server by running “npm run build." This should open your browser to localhost.com and you'll see our lovely landing page. 11 | 12 | ![landing page](./frontend/assets/readmeScreenshots/landing_page_screenshot_readme.png) 13 | 14 | If you have a local PostgreSQL database on your machine, ensure your database is running, and then add your connection credentials. 15 | ![local connection](./frontend/assets/readmeScreenshots/local_connection_login_screenshot_readme.png) 16 | 17 | Alternatively, you can connect via a URI string. 18 | ![hosted connection](./frontend/assets/readmeScreenshots/hosted_connection_login_screenshot_readme.png) 19 | 20 | Once you have submitted your database connection information, you’ll see a beautiful visualization of your database. Have fun dragging the tables around and checking out the relationships between them! 21 | ![full screen of ERD and query builder](./frontend/assets/readmeScreenshots/fullshot_screenshot_readme.png) 22 | 23 | Above your database entity relationship diagram is your query builder. As you click each column to select within a table, this will populate in real time. 24 | ![query builder](./frontend/assets/readmeScreenshots/your_query_screenshot_readme.png) 25 | If you select columns from separate tables, you'll trigger the option to create a join, along with an explanation of the join table types. 26 | ![join table text](./frontend/assets/readmeScreenshots/join_screenshot_readme.png) 27 | 28 | Select your join type of choice and our join builder will help you construct exactly what you need. 29 | ![join selection modal](./frontend/assets/readmeScreenshots/join_selection_screenshot_readme.png) 30 | 31 | Once you select, you'll see your join has been added to your query! 32 | ![updated query builder with join](./frontend/assets/readmeScreenshots/join_added_to_query_screenshot_readme.png) 33 | 34 | Feel free to remove columns from your query simply by clicking on them again. Once your query looks the way you want, you're ready to click "Test"! 35 | 36 | When you see the "Results" button bounce, your query result is ready. Click that button and check out your query result. Notice on the side you'll see the average length of time it took to return your query so you don't slow down your code execution. 37 | ![query result loaded](./frontend/assets/readmeScreenshots/query_results_screenshot_readme.png) 38 | 39 | Click the resulting query string to see a chart with all the data returned from executing your query. 40 | ![query return table](./frontend/assets/readmeScreenshots/query_result_table_screenshot_readme.png) 41 | 42 | If everything looks the way you want, congrats! Just click that clipboard to copy the query string and paste it into your code. 43 | 44 | ## Useful Docs 45 | Here are some useful documentation links that can help you with PostgreSQL databases and more: 46 | - https://www.postgresql.org/docs/current/intro-whatis.html 47 | 48 | 49 | ## How to Contribute 50 | Yesql is proud to be an open-source product and we welcome contributions! 51 | 52 | To contribute, please follow the following guidelines: 53 | 1. Fork and clone this repository. 54 | 2. On your local machine, create a new branch titled "YourName/DescriptiveFeatureYouWantToWorkOn." 55 | 3. Please include helpful comments in your code to explain what you're doing and why. 56 | 4. Push to the dev branch and create a pull request. 57 | 5. Profit! (Just kidding) 58 | 59 | Here are some stretch features that would make awesome contributions: 60 | - Building out cross-join functionality (the fifth join type in our modal) 61 | - Expanding beyond "SELECT" queries to "INSERT," "UPDATE," and "DELETE." (Note that these queries require modifying the database, so these features will require the application to copy the user's database and run test queries on that copy, so as not to mutate the source data.) 62 | 63 | ## Contributor Information 64 | - Eleanor Christopher https://www.linkedin.com/in/eleanor-christopher/; https://github.com/LNRtopher 65 | - Ben Jackson https://www.linkedin.com/in/benjaminreidjackson/; https://github.com/Hubbisand 66 | - Sara Kalkstein https://www.linkedin.com/in/sarakalkstein/; https://github.com/RococoKid 67 | - Nina Skyttmo https://www.linkedin.com/in/ninaskyttmo/; https://github.com/NinaSkyttis 68 | - Brian Taylor https://www.linkedin.com/in/brian-c-taylor/; https://github.com/TaylorBC 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | const express = require('express'); 3 | const path = require('path'); 4 | const app = express(); 5 | const cors = require('cors'); 6 | const PORT = process.env.PORT || 3000; 7 | const bodyParser = require('body-parser'); 8 | 9 | const databaseController = require('../backend/controllers/databaseController.js'); 10 | 11 | app.use(express.json()); 12 | app.use(bodyParser.urlencoded({extended: true})); 13 | app.use(bodyParser.json()); 14 | app.use(cors()); 15 | app.use(express.static('build')); 16 | 17 | app.get('/', (req, res) => res.sendFile(path.join(__dirname, '../build/index.html'))); 18 | 19 | app.get('/chart', (req, res) => res.redirect('/')); 20 | app.get('/formpage', (req, res) => res.redirect('/')); 21 | 22 | 23 | app.post('/testQuery', databaseController.getQueryResults, (req, res) => { 24 | res.status(200).json(res.locals.queryResult); 25 | }); 26 | // app.get('/chart', (req, res) => res.sendFile(path.join(__dirname, '../build/index.html'))); 27 | // route for connecting to user's local db: 28 | app.post('/connect', 29 | databaseController.connect, 30 | databaseController.query, 31 | (req, res) =>{ 32 | res.status(200).json(res.locals.tableData); 33 | }); 34 | 35 | // error handling used when return next(error) 36 | // 37 | // app.use((err, req, res, next) => { 38 | // const status = err.status || 500; 39 | // const message = err.message || 'Internal Server Error'; 40 | // res.status(status).json({ success: false, message }); 41 | // }); 42 | 43 | app.listen(PORT, () => { 44 | console.log(`Listening on port ${PORT}...`); 45 | }); 46 | 47 | module.exports = app; 48 | -------------------------------------------------------------------------------- /backend/controllers/databaseController.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | const databaseController = {}; 3 | const db = require('../database'); 4 | 5 | 6 | databaseController.connect = async (req, res, next) => { 7 | try { 8 | console.log('connectingDatabase'); 9 | // server receives the body from the client, and sends a fetch request to the db 10 | // console.log('Request Body => ', req.body) 11 | const {user, host, database, port, uri} = req.body; 12 | uri ? await db.connectDb(uri) : await db.connectDb(user, host, database, port); 13 | return next(); 14 | } catch (error) { 15 | console.log(error); 16 | return next(error); 17 | }; 18 | }; 19 | 20 | databaseController.query = async (req, res, next) => { 21 | // Query from server to db to get tables and column names: 22 | try { 23 | const tablesAndColumns = await db.query(`SELECT t.table_name, c.column_name, c.data_type 24 | FROM information_schema.tables t 25 | FULL OUTER JOIN information_schema.columns c 26 | ON c.table_name = t.table_name 27 | WHERE t.table_schema = 'public' AND t.table_type = 'BASE TABLE'`); 28 | const outputObject = {}; 29 | for (let i = 0; i < tablesAndColumns.rows.length; i++) { 30 | const columnName = tablesAndColumns.rows[i].column_name; 31 | const dataType = tablesAndColumns.rows[i].data_type; 32 | console.log('Data Type => ', dataType); 33 | if (!outputObject[tablesAndColumns.rows[i].table_name]) outputObject[tablesAndColumns.rows[i].table_name] = {columns: {}}; 34 | outputObject[tablesAndColumns.rows[i].table_name].columns[columnName] = dataType; 35 | }; 36 | // This section is for determining connections, foreign keys, and primary keys 37 | const connections = await db.query(`SELECT c.table_name, c.column_name AS primary_key, k.column_name AS foreign_key, c.constraint_name 38 | FROM information_schema.constraint_column_usage c 39 | INNER JOIN information_schema.key_column_usage k 40 | ON c.constraint_name=k.constraint_name 41 | WHERE c.constraint_name LIKE '%/_fk%' ESCAPE '/' OR c.constraint_name LIKE '%/_pk%' ESCAPE '/' `); 42 | const connectionRows = connections.rows; 43 | for (const row of connectionRows) { 44 | let foreignTable; 45 | row.constraint_name.includes('_fk') ? foreignTable = row.constraint_name.slice(0, row.constraint_name.search(/(_fk)/gm)) : foreignTable = row.constraint_name.slice(0, row.constraint_name.search(/(_pk)/gm)); 46 | console.log('Foreign Table => ', foreignTable); 47 | if (!outputObject[foreignTable].foreignKey) outputObject[foreignTable].foreignKey = []; 48 | if (row.primary_key === row.foreign_key) outputObject[row.table_name].primaryKey = row.primary_key; 49 | if (!outputObject[row.table_name].connections) outputObject[row.table_name].connections = []; 50 | outputObject[foreignTable].foreignKey.push(row.foreign_key); 51 | outputObject[row.table_name].connections.push(foreignTable); 52 | }; 53 | console.log('Output => ', outputObject); 54 | res.locals.tableData = outputObject; 55 | return next(); 56 | } catch (error) { 57 | console.log(error); 58 | return next(error); 59 | }; 60 | }; 61 | 62 | databaseController.getQueryResults = async (req, res, next) => { 63 | try { 64 | const startTime = process.hrtime.bigint(); // Capture start time in nanoseconds 65 | const queryResult = await db.query(req.body.query); 66 | 67 | const endTime = process.hrtime.bigint(); // Capture end time in nanoseconds 68 | const executionTime = (endTime - startTime) / BigInt(1000000); // Calculate execution time in milliseconds 69 | const executionTimeInSeconds = Number(executionTime) / 1000; // Convert milliseconds to seconds 70 | 71 | res.locals.queryResult = { 72 | queryResult: queryResult, 73 | time: executionTimeInSeconds, 74 | }; 75 | 76 | return next(); 77 | } catch (error) { 78 | console.log(error); 79 | return next(error); 80 | } 81 | }; 82 | 83 | module.exports = databaseController; 84 | -------------------------------------------------------------------------------- /backend/database.js: -------------------------------------------------------------------------------- 1 | // require dot env 2 | const dotenv = require('dotenv'); 3 | dotenv.config(); 4 | const {Pool} = require('pg'); 5 | 6 | let pool; 7 | 8 | let information; 9 | 10 | const connectDb = async (...credentials) => { 11 | 12 | information = credentials; 13 | 14 | if (!pool) { 15 | try { 16 | if (credentials.length === 1) { 17 | pool = new Pool({ 18 | connectionString: credentials[0], 19 | }); 20 | } else { 21 | pool = new Pool({ 22 | user: credentials[0], 23 | host: credentials[1], 24 | database: credentials[2], 25 | port: credentials[3], 26 | }); 27 | } 28 | 29 | await pool.connect(); 30 | // execute a query 31 | // const res = await pool.query('SELECT * FROM people'); 32 | 33 | // console.log('connected to database'); // access the rows 34 | 35 | // release the client back to the pool 36 | 37 | // await pool.end(); 38 | } catch (error) { 39 | console.log(error); 40 | pool = null; 41 | } 42 | } 43 | }; 44 | 45 | // connectDb() 46 | 47 | module.exports = { 48 | connectDb, 49 | query: (text, params, callback) => { 50 | // console.log(information) 51 | // connectDb(information); 52 | return pool.query(text, params, callback); 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /build/35ae8ea12ef4615c3a89b6e5c4e1ef68.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/build/35ae8ea12ef4615c3a89b6e5c4e1ef68.png -------------------------------------------------------------------------------- /build/51b74e752554fd943e5a940ff2585cf1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/build/51b74e752554fd943e5a940ff2585cf1.png -------------------------------------------------------------------------------- /build/6ad3da20dc4ca6df8055441bb10b08c0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/build/6ad3da20dc4ca6df8055441bb10b08c0.png -------------------------------------------------------------------------------- /build/73182608525d9c4ce826eb83ab08cc2c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/build/73182608525d9c4ce826eb83ab08cc2c.png -------------------------------------------------------------------------------- /build/86d1cc04d1d932b90ccb274cfbd73c9d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/build/86d1cc04d1d932b90ccb274cfbd73c9d.png -------------------------------------------------------------------------------- /build/be9deffa546db0002bdf1aa25889294b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/build/be9deffa546db0002bdf1aa25889294b.png -------------------------------------------------------------------------------- /build/bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | Copyright (c) 2015 Jed Watson. 3 | Based on code that is Copyright 2013-2015, Facebook, Inc. 4 | All rights reserved. 5 | */ 6 | 7 | /*! 8 | * Adapted from jQuery UI core 9 | * 10 | * http://jqueryui.com 11 | * 12 | * Copyright 2014 jQuery Foundation and other contributors 13 | * Released under the MIT license. 14 | * http://jquery.org/license 15 | * 16 | * http://api.jqueryui.com/category/ui-core/ 17 | */ 18 | 19 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ 20 | 21 | /** 22 | * @license React 23 | * react-dom.production.min.js 24 | * 25 | * Copyright (c) Facebook, Inc. and its affiliates. 26 | * 27 | * This source code is licensed under the MIT license found in the 28 | * LICENSE file in the root directory of this source tree. 29 | */ 30 | 31 | /** 32 | * @license React 33 | * react.production.min.js 34 | * 35 | * Copyright (c) Facebook, Inc. and its affiliates. 36 | * 37 | * This source code is licensed under the MIT license found in the 38 | * LICENSE file in the root directory of this source tree. 39 | */ 40 | 41 | /** 42 | * @license React 43 | * scheduler.production.min.js 44 | * 45 | * Copyright (c) Facebook, Inc. and its affiliates. 46 | * 47 | * This source code is licensed under the MIT license found in the 48 | * LICENSE file in the root directory of this source tree. 49 | */ 50 | 51 | /** 52 | * @license React 53 | * use-sync-external-store-shim.production.min.js 54 | * 55 | * Copyright (c) Facebook, Inc. and its affiliates. 56 | * 57 | * This source code is licensed under the MIT license found in the 58 | * LICENSE file in the root directory of this source tree. 59 | */ 60 | 61 | /** 62 | * @license React 63 | * use-sync-external-store-shim/with-selector.production.min.js 64 | * 65 | * Copyright (c) Facebook, Inc. and its affiliates. 66 | * 67 | * This source code is licensed under the MIT license found in the 68 | * LICENSE file in the root directory of this source tree. 69 | */ 70 | 71 | /** 72 | * @license React 73 | * use-sync-external-store-with-selector.production.min.js 74 | * 75 | * Copyright (c) Facebook, Inc. and its affiliates. 76 | * 77 | * This source code is licensed under the MIT license found in the 78 | * LICENSE file in the root directory of this source tree. 79 | */ 80 | 81 | /** 82 | * @remix-run/router v1.15.2 83 | * 84 | * Copyright (c) Remix Software Inc. 85 | * 86 | * This source code is licensed under the MIT license found in the 87 | * LICENSE.md file in the root directory of this source tree. 88 | * 89 | * @license MIT 90 | */ 91 | 92 | /** 93 | * React Router DOM v6.22.2 94 | * 95 | * Copyright (c) Remix Software Inc. 96 | * 97 | * This source code is licensed under the MIT license found in the 98 | * LICENSE.md file in the root directory of this source tree. 99 | * 100 | * @license MIT 101 | */ 102 | 103 | /** 104 | * React Router v6.22.2 105 | * 106 | * Copyright (c) Remix Software Inc. 107 | * 108 | * This source code is licensed under the MIT license found in the 109 | * LICENSE.md file in the root directory of this source tree. 110 | * 111 | * @license MIT 112 | */ 113 | -------------------------------------------------------------------------------- /build/d0e9bcc1cfc585f01ffff655b9635c61.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/build/d0e9bcc1cfc585f01ffff655b9635c61.png -------------------------------------------------------------------------------- /build/ea6c55aa49e89097447f82022520ab93.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/build/ea6c55aa49e89097447f82022520ab93.png -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 | Yesql
-------------------------------------------------------------------------------- /frontend/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/frontend/.DS_Store -------------------------------------------------------------------------------- /frontend/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Landing from './react/containers/Landing.jsx'; 3 | import Navbar from './react/components/Navbar.jsx'; 4 | import DBFlowContainer from './react/containers/DBFlowContainer.jsx'; 5 | import { BrowserRouter, Routes, Route } from 'react-router-dom'; 6 | import FormPage from './react/containers/FormPage.jsx'; 7 | import HostedFormPage from './react/containers/HostedFormPage.jsx'; 8 | const App = () => { 9 | return ( 10 | //wrap in browserRouter 11 | 12 | 13 |
14 | 15 | } 19 | /> 20 | } 23 | /> 24 | } 27 | /> 28 | } 31 | /> 32 | 33 |
34 |
35 | ) 36 | }; 37 | 38 | 39 | export default App; -------------------------------------------------------------------------------- /frontend/apiSlice.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | // import { createSlice } from '@reduxjs/toolkit' 3 | import {createApi, fetchBaseQuery} from '@reduxjs/toolkit/query/react'; 4 | 5 | 6 | export const apiSlice = createApi({ 7 | reducerPath: 'api', 8 | baseQuery: fetchBaseQuery({baseUrl: 'http://localhost:3000'}), 9 | endpoints: (builder) => ({ 10 | connect: builder.mutation({ 11 | query: (credentials) => ({ 12 | url: '/connect', 13 | method: 'POST', 14 | // Include the user's credentials on body: 15 | headers: { 16 | 'Content-Type': 'application/json', 17 | }, 18 | body: credentials, 19 | }), 20 | }), 21 | }), 22 | }); 23 | // Export hooks for usage in functional components, which are 24 | // auto-generated based on the defined endpoints 25 | export const {useConnectMutation} = apiSlice; 26 | // Action creators are generated for each case reducer function 27 | // export const { increment, decrement, incrementByAmount } = counterSlice.actions 28 | 29 | // export default counterSlice.reducer 30 | -------------------------------------------------------------------------------- /frontend/assets/copy_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/frontend/assets/copy_icon.png -------------------------------------------------------------------------------- /frontend/assets/copy_icon_copied.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/frontend/assets/copy_icon_copied.png -------------------------------------------------------------------------------- /frontend/assets/dropdown_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/frontend/assets/dropdown_icon.png -------------------------------------------------------------------------------- /frontend/assets/dropdown_icon_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/frontend/assets/dropdown_icon_white.png -------------------------------------------------------------------------------- /frontend/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/frontend/assets/favicon.png -------------------------------------------------------------------------------- /frontend/assets/info_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/frontend/assets/info_icon.png -------------------------------------------------------------------------------- /frontend/assets/play_icon_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/frontend/assets/play_icon_black.png -------------------------------------------------------------------------------- /frontend/assets/play_icon_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/frontend/assets/play_icon_white.png -------------------------------------------------------------------------------- /frontend/assets/readmeScreenshots/erd_screenshot_readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/frontend/assets/readmeScreenshots/erd_screenshot_readme.png -------------------------------------------------------------------------------- /frontend/assets/readmeScreenshots/fullshot_screenshot_readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/frontend/assets/readmeScreenshots/fullshot_screenshot_readme.png -------------------------------------------------------------------------------- /frontend/assets/readmeScreenshots/hosted_connection_login_screenshot_readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/frontend/assets/readmeScreenshots/hosted_connection_login_screenshot_readme.png -------------------------------------------------------------------------------- /frontend/assets/readmeScreenshots/join_added_to_query_screenshot_readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/frontend/assets/readmeScreenshots/join_added_to_query_screenshot_readme.png -------------------------------------------------------------------------------- /frontend/assets/readmeScreenshots/join_screenshot_readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/frontend/assets/readmeScreenshots/join_screenshot_readme.png -------------------------------------------------------------------------------- /frontend/assets/readmeScreenshots/join_selection_screenshot_readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/frontend/assets/readmeScreenshots/join_selection_screenshot_readme.png -------------------------------------------------------------------------------- /frontend/assets/readmeScreenshots/landing_page_screenshot_readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/frontend/assets/readmeScreenshots/landing_page_screenshot_readme.png -------------------------------------------------------------------------------- /frontend/assets/readmeScreenshots/local_connection_login_screenshot_readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/frontend/assets/readmeScreenshots/local_connection_login_screenshot_readme.png -------------------------------------------------------------------------------- /frontend/assets/readmeScreenshots/query_result_table_screenshot_readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/frontend/assets/readmeScreenshots/query_result_table_screenshot_readme.png -------------------------------------------------------------------------------- /frontend/assets/readmeScreenshots/query_results_screenshot_readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/frontend/assets/readmeScreenshots/query_results_screenshot_readme.png -------------------------------------------------------------------------------- /frontend/assets/readmeScreenshots/your_query_screenshot_readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/frontend/assets/readmeScreenshots/your_query_screenshot_readme.png -------------------------------------------------------------------------------- /frontend/assets/yesql_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/frontend/assets/yesql_logo.png -------------------------------------------------------------------------------- /frontend/assets/yesql_logo_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/YESQL/0f1c60bb9f8ec003530db1ec5790248b143edb34/frontend/assets/yesql_logo_2.png -------------------------------------------------------------------------------- /frontend/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App.jsx'; 4 | import './styles/index.scss'; 5 | import {store} from './store'; 6 | import {Provider} from 'react-redux'; 7 | 8 | const root = ReactDOM.createRoot(document.getElementById('root')); 9 | 10 | 11 | root.render( 12 | 13 | 14 | , 15 | ); 16 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/querySlice.js: -------------------------------------------------------------------------------- 1 | import {createSlice} from '@reduxjs/toolkit'; 2 | 3 | const initialState = { 4 | query: [{ 5 | string: 'SELECT', 6 | parent: 'clause', 7 | hasComma: false, 8 | }], 9 | testResults: [], 10 | testResultsToDisplay: '', 11 | removedNode: {}, 12 | numOfClauses: 1, 13 | numOfColumns: 0, 14 | tableConnected: false, 15 | isModalOpen: false, 16 | isClause: false, 17 | currentParent: '', 18 | addedParent: '', 19 | isColumnModalOpen: false, 20 | selectedJoin: '', 21 | }; 22 | 23 | const querySlice = createSlice({ 24 | name: 'query', 25 | initialState, 26 | reducers: { 27 | addColumn(state, action) { 28 | const array = state.query; 29 | const [isColumn] = state.query; 30 | const queryLength = state.query.length; 31 | const column = action.payload; 32 | const connections = action.payload.foreignConnections; 33 | const isConnection = state.query.some((el) => { 34 | return connections ? connections.includes(el.string) : false; 35 | }); 36 | const isParentIncluded = state.query.some((node) => { 37 | return node.string === action.payload.parent; 38 | }); 39 | const indexOfFrom = state.query.findIndex((node) => { 40 | return node.string === 'FROM'; 41 | }); 42 | 43 | if (state.numOfClauses > 1 && indexOfFrom !== -1 && !state.isClause) { 44 | for (let i = 1; i < indexOfFrom; i++) { 45 | if (!state.query[i].string.endsWith(',')) { 46 | state.query[i].string += ','; 47 | } 48 | } 49 | } 50 | 51 | if (state.numOfClauses > 2) { 52 | array.push(column); 53 | } 54 | 55 | if (array.length === 1 && isColumn) { 56 | state.numOfClauses++; 57 | array.push( 58 | column, 59 | {string: 'FROM', parent: 'clause'}, 60 | {string: column.parent, parent: column.parent}, 61 | ); 62 | } 63 | 64 | if (isParentIncluded && isColumn && state.numOfClauses === 2) { 65 | array.splice(indexOfFrom, 0, column); 66 | } 67 | 68 | if (!isParentIncluded && queryLength > 1) { 69 | if (!isConnection) { 70 | // this is what we want to hijack for our modal 71 | alert('this table has no connections!'); 72 | } else { 73 | state.query.splice(indexOfFrom, 0, action.payload); 74 | state.query.splice(indexOfFrom + 3, 0, { 75 | string: action.payload.parent, 76 | parent: action.payload.parent, 77 | }); 78 | } 79 | } 80 | state.isClause = false; 81 | }, 82 | removeColumn(state, action) { 83 | state.removedNode = action.payload; 84 | if (!action.payload.string.endsWith(',')) { 85 | state.query = state.query.filter((node) => { 86 | return (action.payload.string !== node.string.slice(0, -1)); 87 | }); 88 | } 89 | state.query = state.query.filter((node) => { 90 | return (action.payload.string !== node.string); 91 | }); 92 | 93 | if (state.numOfColumns > 1) { 94 | state.numOfColumns--; 95 | } 96 | 97 | const includesFrom = state.query.some((item) => item.string === 'FROM'); 98 | 99 | if (includesFrom && state.query.length < 4 && state.query.length > 2) { 100 | if (state.query.length > 1 && state.numOfClauses >= 2) { 101 | state.numOfClauses--; 102 | } 103 | state.query = state.query.slice(0, -2); 104 | } 105 | 106 | for (let i = 0; i < state.query.length; i++) { 107 | if ( 108 | state.query[i].string === 'FROM' && 109 | i > 1 && 110 | state.query[i - 1].string.endsWith(',') 111 | ) { 112 | state.query[i - 1].string = state.query[i - 1].string.slice(0, -1); 113 | } 114 | } 115 | }, 116 | addClauseOrCondition(state, action) { 117 | state.isClause = true; 118 | if (action.payload !== '=' && action.payload !== '*') { 119 | state.query.push({ 120 | string: action.payload, 121 | parent: 'clause', 122 | }); 123 | state.numOfClauses++; 124 | } else { 125 | state.query.push({ 126 | string: action.payload, 127 | parent: 'condition', 128 | inputVisible: true, 129 | }); 130 | state.numOfClauses++; 131 | } 132 | }, 133 | removeClauseOrCondition(state, action) { 134 | state.removedNode = action.payload; 135 | state.query = state.query.filter((node, index) => { 136 | return !( 137 | node.string === action.payload.string && 138 | index === action.payload.index 139 | ); 140 | }); 141 | if (state.numOfClauses > 1 && action.payload.parent !== 'value') { 142 | state.numOfClauses--; 143 | }; 144 | }, 145 | addInput(state, action) { 146 | state.query.push(action.payload); 147 | }, 148 | addJoin(state, action) { 149 | const { 150 | currentParent, 151 | addedParent, 152 | selectedJoin, 153 | selectedColumnOne, 154 | selectedColumnTwo, 155 | } = action.payload; 156 | if (selectedJoin !== 'CROSS JOIN') { 157 | state.query.push({ 158 | string: `${selectedJoin} ${addedParent} ON ${currentParent}.${selectedColumnOne} = ${addedParent}.${selectedColumnTwo}`, 159 | parent: 'JOIN', 160 | }); 161 | } else if (selectedJoin === 'CROSS JOIN') { 162 | state.query.push({ 163 | string: `${selectedJoin} ${addedParent} ON ${currentParent}.${selectedColumnOne} = ${addedParent}.${selectedColumnTwo}`, 164 | parent: 'JOIN', 165 | }); 166 | } 167 | state.isModalOpen = false; 168 | }, 169 | // add this to query from the join: 170 | // INNER JOIN table2 ON table1.column = table2.column; 171 | // FULL OUTER JOIN table2 ON table1.column_name = table2.column_name; 172 | // LEFT JOIN table2 ON table1.column_name = table2.column_name; 173 | // RIGHT JOIN table2 ON table1.column_name = table2.column_name; 174 | // Cross join is kinda funky** SELECT Table1.FirstName, Table1.LastName, Table2.Department FROM Employees Table1 CROSS JOIN Departments Table2; 175 | removeInputWindow(state) { 176 | state.query[state.query.length - 2].inputVisible = false; 177 | }, 178 | removeValue(state, action) { 179 | state.removedNode = action.payload; 180 | state.query = state.query.filter((node, index) => { 181 | return !(node.string === action.payload.string && node.parent === action.payload.parent); 182 | }); 183 | }, 184 | openModal(state, action) { 185 | // redefining indexOfFrom 186 | const indexOfFrom = state.query.findIndex((node) => { 187 | return node.string === 'FROM'; 188 | }); 189 | // this is the table that is in the query already 190 | state.currentParent = state.query[indexOfFrom + 1].string; 191 | // updating value of addedParent to the parent value of the clicked node (aka the table of the clicked column) 192 | state.addedParent = action.payload.parent; 193 | // reassigning isModalOpen property to true so that the subscriber in DBFlowContainer can see that state has changed so it can open the modal 194 | state.isModalOpen = true; 195 | }, 196 | openColumnModal(state, action) { 197 | state.isColumnModalOpen = true; 198 | state.selectedJoin = action.payload; 199 | }, 200 | closeModal(state, action) { 201 | state.isModalOpen = false; 202 | }, 203 | addTestResults(state, action) { 204 | console.log(action.payload, ' :action.payload'); 205 | const {query, time, data} = action.payload; 206 | state.testResults.push({ 207 | query, 208 | data, 209 | time, 210 | }); 211 | }, 212 | testResultsToDisplay(state, action) { 213 | console.log('this is what we want to display: ', action.payload); 214 | state.testResultsToDisplay = action.payload; 215 | }, 216 | }, 217 | }); 218 | 219 | export const { 220 | addColumn, 221 | removeColumn, 222 | addClauseOrCondition, 223 | removeClauseOrCondition, 224 | addInput, 225 | addJoin, 226 | removeInputWindow, 227 | removeValue, 228 | openModal, 229 | closeModal, 230 | currentParent, 231 | addedParent, 232 | openColumnModal, 233 | addTestResults, 234 | testResultsToDisplay, 235 | } = querySlice.actions; 236 | export default querySlice.reducer; 237 | -------------------------------------------------------------------------------- /frontend/react/components/ClauseDropdown.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import React, {useState} from 'react'; 3 | import {useDispatch} from 'react-redux'; 4 | import {addClauseOrCondition} from '../../querySlice'; 5 | import menuData from '../dropdownData'; 6 | import dropdownIcon from '../../assets/dropdown_icon.png'; 7 | 8 | const ClauseDropdown = () => { 9 | const dispatch = useDispatch(); 10 | const [toggledButtons, setToggledButtons] = useState({}); 11 | 12 | const handleChange = (event) => { 13 | dispatch(addClauseOrCondition(event.target.value)); 14 | }; 15 | 16 | const toggleSubMenu = (e, value) => { 17 | e.stopPropagation(); 18 | const submenu = e.target.querySelector('ul'); 19 | const buttonIndex = e.target.getAttribute('data-index'); 20 | 21 | if (!submenu) { 22 | dispatch(addClauseOrCondition(value)); 23 | document.querySelector('.nestedMenu').style.display = 'none'; 24 | return; 25 | } 26 | 27 | const isToggled = toggledButtons[buttonIndex]; 28 | setToggledButtons({...toggledButtons, [buttonIndex]: !isToggled}); 29 | 30 | if (submenu.style.display === 'none' || !submenu.style.display) { 31 | submenu.style.display = 'inline-block'; 32 | } else { 33 | submenu.style.display = 'none'; 34 | } 35 | }; 36 | 37 | const renderSubMenu = (subMenu) => { 38 | return ( 39 | 47 | ); 48 | }; 49 | 50 | return ( 51 | <> 52 |
toggleSubMenu(e, '')} onChange={handleChange}> 53 | Add + 54 | 74 |
75 | 76 | ); 77 | }; 78 | 79 | export default ClauseDropdown; 80 | -------------------------------------------------------------------------------- /frontend/react/components/DBFlow.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable guard-for-in */ 2 | /* eslint-disable max-len */ 3 | import React, {useMemo} from 'react'; 4 | import ReactFlow, {useNodesState, useEdgesState} from 'reactflow'; 5 | import customNode from '../customNode.js'; 6 | import 'reactflow/dist/style.css'; 7 | 8 | // const nodeTypes = {custom: customNode}; 9 | const DBFlow = ({data}) => { 10 | const initialNodes = []; 11 | const initialEdges = []; 12 | 13 | // map table and column nodes: 14 | const nodeHelper = () => { 15 | let tableNum = 0; 16 | const rowLength = Math.ceil(Math.sqrt(Object.keys(data).length)); 17 | let columnNum = 0; 18 | // loop through data object, iterating on each key (table): 19 | for (const table in data) { 20 | // key name: 21 | const tableName = `${table}`; 22 | // populate array of nodes: 23 | initialNodes.push({ 24 | id: tableName, 25 | type: 'custom', 26 | position: { 27 | x: (tableNum * 370), 28 | y: (columnNum * 700)}, 29 | data: {label: table, 30 | foreignKeyTables: data[table].connections}, 31 | style: {width: 200, height: (data[table].length * 70)}, 32 | }); 33 | // attach each child (column) node to the parent (table) node: 34 | let i = 0; 35 | // let lastNode = false; 36 | for (const column in data[table].columns) { 37 | console.log('Column => ', column); 38 | initialNodes.push({ 39 | id: `${tableName} ${column}`, 40 | type: 'custom', 41 | position: {x: 0, y: (++i * 50)}, 42 | data: { 43 | label: column, 44 | dataType: data[table].columns[column], 45 | parent: tableName, 46 | primaryKey: data[table].primaryKey, 47 | foreignKey: data[table].foreignKey, 48 | }, 49 | parentNode: tableName, 50 | draggable: false, 51 | }); 52 | }; 53 | if (data[table].connections) { 54 | for (let i = 0; i < data[table].connections.length; i++) { 55 | initialEdges.push({source: table, target: data[table].connections[i]}); 56 | }; 57 | }; 58 | console.log('Row Length, Column Num => ', rowLength, columnNum); 59 | if ((tableNum + 1) % rowLength === 0) { 60 | columnNum++; 61 | tableNum = 0; 62 | } else tableNum += 1; 63 | }; 64 | }; 65 | 66 | nodeHelper(); 67 | 68 | // define the type we set in our initialNodes: 69 | const nodeTypes = useMemo(() => ({ 70 | custom: customNode, 71 | }), [], 72 | ); 73 | 74 | const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); 75 | const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); 76 | 77 | return ( 78 |
79 | 88 |
89 | ); 90 | }; 91 | export default DBFlow; 92 | -------------------------------------------------------------------------------- /frontend/react/components/DBForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {useNavigate} from 'react-router-dom'; 3 | import {useConnectMutation} from '../../apiSlice'; 4 | 5 | const DBForm = (props) => { 6 | const navigate = useNavigate(); 7 | const [postConnect, {isLoading, isError}] = useConnectMutation({ 8 | fixedCacheKey: 'databaseSchema', 9 | }); 10 | console.log(props); 11 | 12 | const handleSubmit = async (event) => { 13 | event.preventDefault(); 14 | console.log(event.target.elements); 15 | // capture user's input credentials in an object to send in post request to connect to user's db: 16 | const formData = {}; 17 | if (props.location === 'hosted') { 18 | formData.uri= event.target.elements.uri.value; 19 | } else { 20 | formData.user= event.target.elements.user.value; 21 | formData.host= event.target.elements.host.value; 22 | formData.database= event.target.elements.database.value; 23 | formData.port= event.target.elements.port.value; 24 | } 25 | try { 26 | // sending user credentials in a post request; 27 | const {data} = await postConnect(formData); 28 | console.log('we got something back!', data); 29 | // we navigate to /chart (dbFlow container) only once we confirm our post request with the user credentials was successful 30 | navigate('/chart'); 31 | } catch (error) { 32 | console.error('we have an error', error); 33 | } 34 | }; 35 | return ( 36 | props.location === 'hosted' ? 37 |
38 |

Connect to Database

39 | 40 | 41 | 42 | {isError &&
Error submitting form
} 43 |
: 44 |
45 |

Connect to Database

46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | {isError &&
Error submitting form
} 56 |
57 | ); 58 | }; 59 | 60 | export default DBForm; 61 | -------------------------------------------------------------------------------- /frontend/react/components/DBQuery.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import React, {useState, useEffect} from 'react'; 3 | import {useSelector, useDispatch} from 'react-redux'; 4 | import copyIcon from '../../assets/copy_icon.png'; 5 | import playIconWhite from '../../assets/play_icon_white.png'; 6 | import playIconBlack from '../../assets/play_icon_black.png'; 7 | import copyIconCopied from '../../assets/copy_icon_copied.png'; 8 | 9 | import { 10 | removeColumn, 11 | removeClauseOrCondition, 12 | addInput, 13 | removeInputWindow, 14 | removeValue, 15 | addTestResults, 16 | } from '../../querySlice'; 17 | import ClauseDropdown from './ClauseDropdown'; 18 | import TestResults from './TestResults'; 19 | 20 | const DBQuery = () => { 21 | const store = useSelector((state) => state.queryReducer); 22 | const [copyClipboard, setCopyClipboard] = useState(false); 23 | const dispatch = useDispatch(); 24 | 25 | useEffect(() => { 26 | if (copyClipboard) { 27 | setCopyClipboard(false); 28 | } 29 | }, [store.query]); 30 | // handleClick is the function we're using for dispatching the remove 31 | // action and accessing the reducer function inside of querySlice. 32 | const handleClick = (element) => { 33 | // Here we are making sure that the initial SELECT clause can't be 34 | // deleted from the query 35 | if (element.string !== 'SELECT') { 36 | if (element.parent === 'clause' || 37 | element.parent === 'condition' || 38 | element.parent === 'input') { 39 | dispatch(removeClauseOrCondition({string: element.string, parent: element.parent, index: element.index})); 40 | } else if (element.parent === 'value') { 41 | dispatch(removeValue({string: element.string, parent: element.parent, index: element.index})); 42 | } else { 43 | dispatch(removeColumn({string: element.string, parent: element.parent})); 44 | } 45 | } 46 | }; 47 | 48 | // On line 5, we are importing the ClauseDropdown so that we can display the 49 | // dropdown inside of the queryField. Once the clause has been selected, 50 | // we're dispatching the add action to the add reducer function in querySlice, 51 | // passing in the clause as the string, and the parent being an empty string, 52 | // I (Nina) think that we should potentially change the name of for all pieces 53 | // of the query to 'type' and name each clause as having the type 'clause' 54 | const handleInput = (event) => { 55 | dispatch(addInput({string: event, parent: 'value'})); 56 | // removeInputWindow is another function inside of querySlice that will remove the input 57 | // field if the clause that was added is an '=' sign. 58 | dispatch(removeInputWindow()); 59 | }; 60 | 61 | 62 | const handleTestQuery = async () => { 63 | const query = store.query.map((node) => node.string).join(' '); 64 | console.log(query); 65 | const response = await fetch('/testQuery', { 66 | method: 'POST', 67 | headers: { 68 | 'Content-Type': 'application/json', 69 | }, 70 | body: JSON.stringify({query: query}), 71 | }); 72 | const data = await response.json(); 73 | console.log(data); 74 | dispatch(addTestResults({data, query})); 75 | }; 76 | 77 | const handleCopyToClipboard = () => { 78 | const query = store.query.map((node) => node.string).join(' '); 79 | navigator.clipboard.writeText(query) 80 | .then(() => setCopyClipboard(true)) 81 | .catch((error) => console.error('Unable to copy query to clipboard: ', error)); 82 | }; 83 | 84 | let indexNum = 0; 85 | 86 | const queryInputs = store.query.map((node, index) => { 87 | return ( 88 | 89 | { node.string === '=' && node.inputVisible ? ( 90 | <> 91 | 92 | { 93 | if (e.key === 'Enter') handleInput(e.target.value); 94 | }}/> 95 | 96 | ) : node.parent === 'clause' && node.string !== 'SELECT' ? ( 97 | <> 98 |
99 | 100 | 101 | ) : node.hasComma ? ( 102 | 103 | ) : ( 104 | 105 | )} 106 |
107 | ); 108 | }); 109 | 110 | return ( 111 |
112 |

YOUR QUERY:

113 |
114 |
115 | {queryInputs} 116 | 117 |
118 | copy query icon 124 | 129 |
130 | 131 |
132 | ); 133 | }; 134 | 135 | export default DBQuery; 136 | -------------------------------------------------------------------------------- /frontend/react/components/JoinModal.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {useSelector, useDispatch} from 'react-redux'; 3 | // import { addClauseOrCondition } from '../../querySlice'; 4 | import {openColumnModal} from '../../querySlice'; 5 | import infoIcon from '../../assets/info_icon.png'; 6 | // import { modalData } from '../modalData'; 7 | const JoinModal = () => { 8 | // give access to store 9 | const currParent = useSelector((state) => state.queryReducer.currentParent); 10 | const addParent = useSelector((state) => state.queryReducer.addedParent); 11 | const [toggleInfo, setToggleInfo] = useState(); 12 | const dispatch = useDispatch(); 13 | 14 | const modalData = [ 15 | { 16 | type: 'INNER JOIN', 17 | innerText: `Inner Join selects records that have a common matching value between ${currParent} and ${addParent}.`, 18 | }, 19 | { 20 | type: 'FULL JOIN', 21 | innerText: `Full Join (Full Outer Join) returns all records if there is any match in ${currParent} and ${addParent}. 22 | Non-matching rows will return null for those values.`, 23 | }, 24 | { 25 | type: 'RIGHT JOIN', 26 | innerText: `Right Join returns all records ${addParent} and matching records from ${currParent}. 27 | If there are no matches in ${currParent} they will return null.`, 28 | }, 29 | { 30 | type: 'LEFT JOIN', 31 | innerText: `Right Join returns all records ${currParent} and matching records from ${addParent}. 32 | If there are no matches in ${addParent} they will return null.`, 33 | }, 34 | { 35 | type: 'CROSS JOIN', 36 | innerText: `Cross Join combines each row from ${currParent} with each row from ${addParent} for all possible combinations. 37 | WARNING: Can get very computationally expensive! Processing time can increase exponentially depending on size of tables!`, 38 | }, 39 | ]; 40 | 41 | 42 | const handleClick = (event) => { 43 | if (event.target.tagName === 'BUTTON') { 44 | // dispatch(addClauseOrCondition(event.target.value)); 45 | dispatch(openColumnModal(event.target.value)); 46 | } 47 | }; 48 | 49 | const openInfo = (join) => { 50 | if (!toggleInfo) { 51 | setToggleInfo(join); 52 | } else { 53 | setToggleInfo(null); 54 | } 55 | }; 56 | 57 | return ( 58 |
59 |

How would you like to join
60 | {`${currParent} and ${addParent}?`} 61 |

62 | {modalData.map((joinType, index) => ( 63 | <> 64 | 77 |

83 | {joinType.innerText} 84 |

85 | 86 | ))} 87 |
88 | ); 89 | }; 90 | 91 | export default JoinModal; 92 | -------------------------------------------------------------------------------- /frontend/react/components/LandingPageText.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LandingPageText = () => { 4 | return ( 5 |
6 |
7 | 1. Make sure your local database is running 8 |
9 |
10 | 2. Enter your databse credentials 11 |
12 |
13 | 3. Click on the table and columns you need 14 |
15 |
16 | 4. Copy your SQL query and use it in your own codebase 17 |
18 | 22 | 27 | 28 |
29 | ); 30 | }; 31 | export default LandingPageText; 32 | -------------------------------------------------------------------------------- /frontend/react/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import logo from '../../assets/yesql_logo.png'; 3 | 4 | const Navbar = () => { 5 | return ( 6 | 13 | ) 14 | } 15 | 16 | export default Navbar; -------------------------------------------------------------------------------- /frontend/react/components/OnColumnsModal.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import {useSelector, useDispatch} from 'react-redux'; 3 | import {addJoin} from '../../querySlice'; 4 | 5 | const OnColumnsModal = () => { 6 | const dispatch = useDispatch(); 7 | const { 8 | currentParent, 9 | addedParent, 10 | selectedJoin, 11 | isOpen} = useSelector((state) => state.queryReducer); 12 | const tableOne = useSelector((state) => state.api.mutations.databaseSchema.data[currentParent]); 13 | const tableTwo = useSelector((state) => state.api.mutations.databaseSchema.data[addedParent]); 14 | const [selectedColumnOne, setSelectedColumnOne] = useState(); 15 | const [selectedColumnTwo, setSelectedColumnTwo] = useState(); 16 | 17 | 18 | const tableOneMap = []; 19 | Object.keys(tableOne.columns).forEach((el) => { 20 | const obj = {column: el, type: (tableOne.columns[el] === 'character varying' ? 'charvar' : tableOne.columns[el])}; 21 | if (tableOne.foreignKey.includes(el)) { 22 | obj.key = 'FK'; 23 | } 24 | if (el === tableOne.primaryKey) { 25 | obj.key = 'PK'; 26 | } 27 | tableOneMap.push(obj); 28 | }); 29 | const tableTwoMap = []; 30 | Object.keys(tableTwo.columns).forEach((el) => { 31 | const obj = {column: el, type: (tableTwo.columns[el] === 'character varying' ? 'charvar' : tableTwo.columns[el])}; 32 | if (tableTwo.foreignKey.includes(el)) { 33 | obj.key = 'FK'; 34 | } 35 | if (el === tableTwo.primaryKey) { 36 | obj.key = 'PK'; 37 | } 38 | tableTwoMap.push(obj); 39 | }); 40 | 41 | const handleClick = (column, table) => { 42 | if (table === currentParent) { 43 | selectedColumnOne ? 44 | setSelectedColumnOne(null) : setSelectedColumnOne(column); 45 | } else if (table === addedParent) { 46 | selectedColumnTwo ? 47 | setSelectedColumnTwo(null) : setSelectedColumnTwo(column); 48 | } 49 | }; 50 | 51 | const submitToQuery = () => { 52 | const joinObj = { 53 | currentParent, 54 | addedParent, 55 | selectedColumnOne, 56 | selectedColumnTwo, 57 | selectedJoin, 58 | }; 59 | dispatch(addJoin(joinObj)); 60 | }; 61 | 62 | return ( 63 |
64 |

Select which columns you'd like to connect?

65 |
66 |

{currentParent}

67 | { tableOneMap.map((column, index) => ( 68 | 85 | ))} 86 |
87 |

ON

88 |
89 |

{addedParent}

90 | {tableTwoMap.map((column, index) => ( 91 | 105 | ))} 106 |
107 | 113 |
114 | ); 115 | }; 116 | 117 | export default OnColumnsModal; 118 | -------------------------------------------------------------------------------- /frontend/react/components/TestResults.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import React, {useEffect, useRef, useState} from 'react'; 3 | import ReactModal from 'react-modal'; 4 | import {useSelector, useDispatch} from 'react-redux'; 5 | import dropdownIconWhite from '../../assets/dropdown_icon_white.png'; 6 | import dropdownIcon from '../../assets/dropdown_icon.png'; 7 | import { testResultsToDisplay } from '../../querySlice';// import copyIcon from '../../assets/copy_icon.png'; 8 | import TestResultsModal from './TestResultsModal'; 9 | 10 | const TestResults = () => { 11 | const [toggled, setToggled] = useState(false); 12 | const testResults = useSelector((state) => state.queryReducer.testResults); 13 | const [notIcon, setNotIcon] = useState(false); 14 | const [seeData, setSeeData] = useState(false); 15 | const dispatch = useDispatch(); 16 | 17 | useEffect(() => { 18 | if (!toggled && testResults.length > 0) { 19 | setNotIcon(true); 20 | } 21 | }, [testResults]); 22 | 23 | const toggleResults = () => { 24 | const list = document.querySelector('.test-results'); 25 | if (list.style.display === 'none') { 26 | list.style.display = 'block'; 27 | setToggled(true); 28 | } else { 29 | list.style.display = 'none'; 30 | setToggled(false); 31 | setNotIcon(false); 32 | } 33 | }; 34 | 35 | const handleResultData = (result) => { 36 | if (result) { 37 | dispatch(testResultsToDisplay(result)); 38 | setSeeData(true); 39 | } 40 | }; 41 | 42 | const closeModal = () => { 43 | setSeeData(false); 44 | } 45 | 46 | return ( 47 | <> 48 |
49 |
50 | Results 51 | 52 |
53 |
    54 | { testResults.map((result, index) => ( 55 |
  1. handleResultData(result.query)}> 56 | {result.query} 57 | {`${result.data.time}s`} 58 |
  2. 59 | ))} 60 |
61 |
62 | 67 | 68 | 69 | 70 | ); 71 | }; 72 | 73 | 74 | export default TestResults; 75 | -------------------------------------------------------------------------------- /frontend/react/components/TestResultsModal.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable guard-for-in */ 2 | /* eslint-disable max-len */ 3 | import React from 'react'; 4 | // import ReactFlow, {useNodesState, useEdgesState} from 'reactflow'; 5 | // import customNode from '../customNode.js'; 6 | // import 'reactflow/dist/style.css'; 7 | import {useSelector} from 'react-redux'; 8 | 9 | const TestResultsModal = () => { 10 | const {testResults, testResultsToDisplay} = useSelector((state) => state.queryReducer); 11 | console.log('testResults in testresultsmodal', testResults); 12 | const filteredResults = testResults.filter((el) => el.query === testResultsToDisplay); 13 | const filteredData = filteredResults[0].data.queryResult.rows; 14 | console.log('Filtered Results => ', filteredData); 15 | const tableHeaders = Object.keys(filteredData[0]).map((el) => { 16 | return ( 17 | {el} 18 | ); 19 | }); 20 | const tableData = []; 21 | (function tableBodyData() { 22 | if (filteredData) { 23 | for (const row in filteredData) { 24 | tableData.push(Object.values(filteredData[row]).map((el) => { 25 | console.log('El => ', el); 26 | if (el == null) { 27 | return ( 28 | null 29 | ); 30 | } else { 31 | return ( 32 | {el} 33 | ); 34 | } 35 | })); 36 | } 37 | }; 38 | })(); 39 | const tabledataRows = filteredData.map((el, index) => { 40 | return ( 41 | 42 | {tableData[index]} 43 | 44 | ) 45 | }); 46 | console.log('Data => ', tableData); 47 | console.log('Table Data => ', tabledataRows); 48 | return ( 49 | 50 | 51 | 52 | {tableHeaders} 53 | 54 | 55 | 56 | {tabledataRows} 57 | 58 |
59 | ); 60 | }; 61 | 62 | export default TestResultsModal; 63 | -------------------------------------------------------------------------------- /frontend/react/containers/DBFlowContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {useState} from 'react'; 3 | import DBFlow from '../components/DBFlow.jsx'; 4 | import ReactModal from 'react-modal'; 5 | import TestResultsModal from '../components/TestResultsModal.jsx'; 6 | import DBQuery from '../components/DBQuery.jsx'; 7 | import {redirect} from 'react-router-dom'; 8 | import DBForm from '../components/DBForm'; 9 | import {useSelector, useDispatch} from 'react-redux'; 10 | import {useConnectMutation} from '../../apiSlice.js'; 11 | import {closeModal} from '../../querySlice.js'; 12 | import JoinModal from '../components/JoinModal.jsx'; 13 | import OnColumnsModal from '../components/OnColumnsModal.jsx'; 14 | import {Overlay} from 'react-bootstrap'; 15 | import {Position} from 'reactflow'; 16 | 17 | const DBFlowContainer = () => { 18 | const dispatch = useDispatch(); 19 | const [modalOpen, setOpen] = useState(false); 20 | // subscribing to state to check if state is updated (i.e. if openModal has been changed) 21 | const modalIsOpen = useSelector((state) => state.queryReducer.isModalOpen); 22 | // console.log('have we made it past our openModal subscription??? It should be', modalIsOpen); 23 | // subscribe to state to see which modal should render 24 | const isOnColumnsModalOpen = useSelector((state) => state.queryReducer.isColumnModalOpen); 25 | // Using a query hook automatically fetches data and returns query values 26 | function handleCloseModal() { 27 | dispatch(closeModal()); 28 | // setOpen(false); 29 | }; 30 | // Do we actually need useConnectMutation here?? 31 | const [postConnect, {data, error, isLoading, isSuccess}] = useConnectMutation({ 32 | fixedCacheKey: 'databaseSchema', 33 | }); 34 | // conditional rendering: 35 | if (isLoading) { 36 | return
Loading...
; 37 | } 38 | if (error) { 39 | return
Error:{error}
; 40 | } 41 | // if query is status: success, render components, pass data on props down to DBFlow 42 | if (isSuccess && data) { 43 | console.log('Data => ', data); 44 | return ( 45 |
46 | 47 | 54 | { isOnColumnsModalOpen && } 56 | { !isOnColumnsModalOpen && } 58 | 59 | 60 |
61 | ); 62 | }; 63 | }; 64 | export default DBFlowContainer; 65 | -------------------------------------------------------------------------------- /frontend/react/containers/FormPage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import DBForm from "../components/DBForm"; 3 | 4 | const FormPage = () => { 5 | return ( 6 |
7 |

Fill out your credentials below
to get started

8 | 9 |
10 | ) 11 | } 12 | 13 | export default FormPage; -------------------------------------------------------------------------------- /frontend/react/containers/HostedFormPage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import DBForm from "../components/DBForm"; 3 | 4 | const HostedFormPage = () => { 5 | return ( 6 |
7 |

Fill out your credentials below
to get started

8 | 9 |
10 | ) 11 | } 12 | 13 | export default HostedFormPage; -------------------------------------------------------------------------------- /frontend/react/containers/Landing.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LandingPageText from '../components/LandingPageText.jsx'; 3 | import DBForm from '../components/DBForm.jsx'; 4 | import { Link } from 'react-router-dom'; 5 | 6 | const Landing = () => { 7 | return ( 8 |
9 |

Say goodbye to
inefficiencies
and hello to
precision with Yesql

10 | {/*

11 | make your queries optimal with Yesql

*/} 12 |
13 |
14 | {/* render instructions: */} 15 | 16 |
17 |
18 |

19 | In a world where time is currency, 20 |
21 | optimize SQL queries effortlessly with Yesql. 22 |

23 |

24 | Visualize records, columns, and table segments to 25 |
26 | craft finely tuned queries for optimal performance. 27 |

28 |
29 | Connect to local Database 30 | Connect to hosted Database 31 |
32 |
33 |
34 |
35 | ) 36 | } 37 | 38 | export default Landing; -------------------------------------------------------------------------------- /frontend/react/customNode.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect, useRef} from 'react'; 2 | import {useDispatch, useSelector} from 'react-redux'; 3 | import {Handle, Position} from 'reactflow'; 4 | import {addColumn, removeColumn, openModal} from '../querySlice'; 5 | 6 | 7 | const customNode = ({data, isConnectable}) => { 8 | const removedNode = useSelector((state) => state.queryReducer.removedNode); 9 | const query = useSelector((state) => state.queryReducer.query); 10 | const [clicked, setClicked] = useState(false); 11 | const dispatch = useDispatch(); 12 | const buttonRef = useRef(null); 13 | let label; 14 | if (data.label === data.primaryKey) label = `PK`; 15 | else if (data.foreignKey && data.foreignKey.includes(data.label)) label = `FK`; 16 | 17 | console.log('Data and Label => ', data, label); 18 | 19 | useEffect(() => { 20 | if (removedNode) { 21 | let nodeString = removedNode.string; 22 | if (typeof nodeString === 'string' && nodeString.endsWith(',')) { 23 | nodeString = nodeString.slice(0, -1); 24 | } 25 | if (nodeString === data.label && removedNode.parent === data.parent) { 26 | setClicked(false); 27 | buttonRef.current.className = 'flowButton'; 28 | } 29 | } 30 | }, [removedNode, data.label, data.parent]); 31 | // logic for showing which nodes/columns are clicked: 32 | const handleClick = async (data) => { 33 | const connections = data.foreignKeyTables; 34 | const isConnection = query.some((el) => { 35 | return connections ? connections.includes(el.string) : false; 36 | }); 37 | const isParentIncluded = query.some((node) => { 38 | // check to see if parent of clicked node matches any string in query 39 | return node.string === data.parent; 40 | }); 41 | setClicked((prevClicked) => !prevClicked); 42 | // check if parent (table) of clicked node matches what's in the query thus far 43 | if (!isParentIncluded && query.length > 1) { 44 | console.log('are we in !isParentIncluded?'); 45 | dispatch(openModal({ 46 | parent: data.parent, 47 | })); 48 | } else if (!clicked && (isParentIncluded || isConnection || query.length === 1)) { 49 | dispatch(addColumn({ 50 | string: data.label, 51 | parent: data.parent, 52 | isColumn: true, 53 | hasComma: false, 54 | foreignConnections: data.foreignKeyTables, 55 | })); 56 | } else { 57 | dispatch(removeColumn({ 58 | string: data.label, 59 | parent: data.parent, 60 | })); 61 | }; 62 | }; 63 | 64 | if (!data.parent) { 65 | return ( 66 |
67 | 68 |
69 | 79 |
80 | 81 |
82 | ); 83 | } else { 84 | return ( 85 |
86 | 87 |
88 | 111 |
112 | 113 |
114 | ); 115 | } 116 | }; 117 | 118 | export default customNode; 119 | -------------------------------------------------------------------------------- /frontend/react/dropdownData.jsx: -------------------------------------------------------------------------------- 1 | const menuData = [ 2 | { top: true, label: 'SELECT' }, 3 | { top: true, label: 'FROM' }, 4 | { top: true, label: 'WHERE' }, 5 | { top: true, label: 'GROUP BY' }, 6 | { top: true, label: 'ORDER BY' }, 7 | { top: true, label: 'JOIN' }, 8 | { top: true, label: 'HAVING' }, 9 | { top: true, label: 'DISTINCT' }, 10 | { top: true, label: 'LIMIT' }, 11 | { top: true, label: 'INSERT INTO' }, 12 | { 13 | label: 'Clauses', 14 | submenu: [ 15 | { label: 'SELECT' }, 16 | { label: 'FROM' }, 17 | { label: 'GROUP BY' }, 18 | { label: 'HAVING' }, 19 | { label: 'ORDER BY' }, 20 | { label: 'LIMIT' }, 21 | { label: 'OFFSET' }, 22 | { label: 'DISTINCT' }, 23 | { label: 'LIKE' }, 24 | { label: 'IN' }, 25 | { label: 'BETWEEN' }, 26 | ] 27 | }, 28 | { 29 | label: 'Comparison Operators', 30 | submenu: [ 31 | { label: '=' }, 32 | { label: '>' }, 33 | { label: '<' }, 34 | { label: '>=' }, 35 | { label: '<=' }, 36 | { label: '!=' }, 37 | { label: '<>' }, 38 | ] 39 | }, 40 | { 41 | label: 'Arithmetic Operators', 42 | submenu: [ 43 | { label: '+' }, 44 | { label: '-' }, 45 | { label: '*' }, 46 | { label: '/' }, 47 | { label: '%' }, 48 | ] 49 | }, 50 | { 51 | label: 'Operators', 52 | submenu: [ 53 | { label: 'AND' }, 54 | { label: 'OR' }, 55 | { label: 'NOT' }, 56 | { label: 'BETWEEN' }, 57 | { label: 'EXISTS' }, 58 | { label: 'IN' }, 59 | { label: 'IS NULL' }, 60 | { label: 'IS NOT NULL' }, 61 | { label: 'LIKE' }, 62 | { label: 'NOT' }, 63 | { label: 'OR' }, 64 | ] 65 | }, 66 | ] 67 | 68 | export default menuData; -------------------------------------------------------------------------------- /frontend/react/tableNode.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Handle, Position} from 'reactflow'; 3 | 4 | const tableNode = ({isConnectable}) => { 5 | return ( 6 |
7 | 8 |
9 | 10 |
11 | 12 |
13 | ) 14 | } 15 | 16 | export default tableNode; -------------------------------------------------------------------------------- /frontend/store.js: -------------------------------------------------------------------------------- 1 | import {configureStore} from '@reduxjs/toolkit'; 2 | import {setupListeners} from '@reduxjs/toolkit/query'; 3 | import {apiSlice} from './apiSlice.js'; 4 | import queryReducer from './querySlice.js'; 5 | 6 | 7 | export const store = configureStore({ 8 | reducer: { 9 | [apiSlice.reducerPath]: apiSlice.reducer, 10 | queryReducer: queryReducer, 11 | }, 12 | middleware: (getDefaultMiddleware) => 13 | getDefaultMiddleware().concat(apiSlice.middleware), 14 | }); 15 | // database: dbReducer, 16 | 17 | setupListeners(store.dispatch); 18 | -------------------------------------------------------------------------------- /frontend/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Comfortaa:wght@300..700&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); 2 | $font: "Comfortaa", 3 | sans-serif; 4 | $primary-blue: #556AFF; 5 | $primary-blue-pale: #5569ff; 6 | // $primary-blue: #ff00f2; 7 | $dark-blue: #0b002d; 8 | $table-blue: #a3bdff; 9 | $primary-purple: #7300ff; 10 | $primary-purple-pale: #eedfff; 11 | $button-2-background: #a3bdff; 12 | $modal-gradient: linear-gradient(323deg, rgba(219, 224, 255, 1) 0%, rgba(247, 248, 255, 1) 100%); 13 | // $gray: linear-gradient(160deg, rgba(255, 255, 255, 1) 60%, rgb(230, 233, 255) 91%, rgb(219, 223, 255) 100%); 14 | 15 | $gray: #fafafa; 16 | $white: #fff; 17 | $header-font: "Poppins", sans-serif; 18 | $box-shadow: rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, 19 | rgba(0, 0, 0, 0.05) 0px 4px 6px -2px; 20 | $h1-size: 64px; 21 | $h1-weight: 400; 22 | $h1-letter-spacing: 0.02em; 23 | $h2-size: 36px; 24 | $h2-weight: 600; 25 | $h3-size: 24px; 26 | $h3-weight: 700; 27 | $h4-size: 18px; 28 | $h4-weight: 400; 29 | $h6-size: 14px; 30 | $h6-weight: 100; 31 | $p-size: 12px; 32 | $p-weight: 100; 33 | 34 | // media queries 35 | $small-screen: 768px; 36 | $medium-screen: 1023px; 37 | $large-screen: 1200px; 38 | $extra-large-screen: 1600px; 39 | 40 | 41 | 42 | // basic styling 43 | body { 44 | background: rgb(223, 227, 255); 45 | background: linear-gradient(180deg, rgba(223, 227, 255, 1) 0%, rgba(234, 220, 255, 1) 100%); 46 | font-family: $font; 47 | font-optical-sizing: auto; 48 | font-weight: 400; 49 | font-style: normal; 50 | // height: 100vh; 51 | margin: 0px 50px; 52 | } 53 | h1, h2, h3, h4 { 54 | font-family: $header-font; 55 | color: $dark-blue; 56 | } 57 | 58 | h2 { 59 | font-size: $h2-size; 60 | font-weight: $h2-weight; 61 | text-align: center; 62 | } 63 | 64 | h3 { 65 | font-weight: $h3-weight; 66 | font-size: $h3-size; 67 | } 68 | 69 | h4 { 70 | font-weight: $h4-weight; 71 | } 72 | 73 | 74 | .button { 75 | background: $primary-blue; 76 | color: #fff; 77 | font-weight: $p-weight; 78 | font-size: $h4-size; 79 | text-decoration: none; 80 | border-radius: 18px; 81 | border: none; 82 | padding: 20px 50px; 83 | margin: 10px 0px; 84 | font-family: $font; 85 | box-shadow: rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px; 86 | cursor: pointer; 87 | &:hover { 88 | transition: 0.1s ease-in; 89 | letter-spacing: 0.02em; 90 | border-radius: 16px; 91 | } 92 | } 93 | 94 | .container { 95 | padding: 50px; 96 | background: $white; 97 | padding: 50px; 98 | margin: auto; 99 | border-radius: 18px; 100 | color: $primary-blue; 101 | } 102 | 103 | 104 | // navbar styling 105 | nav { 106 | margin: 25px 60px 0px 60px; 107 | display: flex; 108 | justify-content: space-between; 109 | color: $primary-blue; 110 | } 111 | 112 | .navbar-links { 113 | display: flex; 114 | font-size: 16px; 115 | } 116 | 117 | .navbar-links li { 118 | padding-left: 50px; 119 | list-style: none; 120 | } 121 | 122 | .logo { 123 | height: 40px; 124 | } 125 | 126 | // landing-page styling 127 | 128 | .landing-page-grid { 129 | background: $gray; 130 | box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; 131 | border-radius: 18px; 132 | display: grid; 133 | grid-template-columns: 1fr 1fr; 134 | grid-template-rows: 1fr; 135 | align-items: center; 136 | text-align: center; 137 | @media (max-width: $large-screen) { 138 | display: flex; 139 | justify-content: center; 140 | padding: 50px; 141 | } 142 | } 143 | .landing-page { 144 | background: white; 145 | padding: 50px; 146 | border-radius: 18px; 147 | display: flex; 148 | flex-direction: column; 149 | color: $primary-blue; 150 | h1 { 151 | font-size: $h1-size; 152 | font-weight: $h1-weight; 153 | letter-spacing: $h1-letter-spacing; 154 | color: $dark-blue; 155 | margin: 0px 0px 50px 50px; 156 | line-height: 1.1; 157 | span { 158 | color: $primary-blue; 159 | font-weight: 600 160 | } 161 | } 162 | h4 { 163 | color: $dark-blue; 164 | padding: 0px; 165 | letter-spacing: 0.04em; 166 | margin-top: 10px; 167 | } 168 | h3 { 169 | margin-bottom: 10px; 170 | font-size: 21px; 171 | } 172 | p { 173 | font-size: $p-size; 174 | font-weight: $p-weight; 175 | } 176 | .button { 177 | width: 50%; 178 | } 179 | } 180 | 181 | .landing-page section { 182 | position: relative; 183 | // display: grid; 184 | grid-template-columns: repeat(1, 1fr); 185 | grid-template-rows: auto; 186 | grid-template-areas: 187 | "one" 188 | "two" 189 | "three" 190 | "four" 191 | ; 192 | row-gap: 20px; 193 | column-gap: 10px; 194 | font-size: 18px; 195 | line-height: 1.2; 196 | padding: 20px 50px; 197 | div { 198 | position: relative; 199 | background: $gray; 200 | z-index: 2; 201 | padding: 10px; 202 | // background: $pale-blue; 203 | height: 50px; 204 | border-radius: 0px 18px 18px 18px; 205 | color: $dark-blue; 206 | border: 3px solid $dark-blue; 207 | display: flex; 208 | align-items: center; 209 | justify-content: flex-start; 210 | width: 400px; 211 | // height: 100px; 212 | // background-color: #ccc; 213 | margin: 20px; 214 | text-align: start; 215 | // line-height: 100px; 216 | } 217 | span { 218 | font-size: 48px; 219 | padding-right: 10px; 220 | } 221 | 222 | .step-1 { 223 | grid-area: one; 224 | // background: linear-gradient(90deg, rgba(243, 115, 56, 0.703) 0%, rgba(231, 84, 199, 0.797) 25%); 225 | } 226 | 227 | .step-2 { 228 | grid-area: two; 229 | //background: linear-gradient(90deg, rgba(231, 84, 198, 0.797) 0%, rgba(184, 75, 252, 0.7) 75%); 230 | } 231 | 232 | .step-3 { 233 | grid-area: three; 234 | //background: linear-gradient(90deg, rgba(184, 75, 252, 0.7) 0%, rgba(160, 78, 252, 0.7) 100%); 235 | } 236 | 237 | .step-4 { 238 | grid-area: four; 239 | //background: linear-gradient(90deg, rgba(160, 78, 252, 0.7) 0%, rgba(112, 82, 254, 0.7) 100%); 240 | } 241 | @media (max-width: $large-screen) { 242 | display: none; 243 | } 244 | } 245 | 246 | .line { 247 | position: absolute; 248 | z-index: 0; 249 | top: 0; 250 | left: 0; 251 | height: 100%; 252 | width: 100%; 253 | } 254 | 255 | 256 | 257 | .landing-button-container { 258 | display: flex; 259 | flex-direction: column; 260 | align-items: center; 261 | .button-2 { 262 | background-color: $button-2-background; 263 | color: #fff; 264 | } 265 | } 266 | 267 | 268 | // form styling 269 | 270 | .form-container { 271 | background: white; 272 | padding: 50px; 273 | border-radius: 18px; 274 | } 275 | 276 | form { 277 | display: flex; 278 | flex-direction: column; 279 | justify-content: center; 280 | padding: 20px; 281 | border-radius: 18px; 282 | max-width: 800px; 283 | margin: auto; 284 | box-shadow: rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, 285 | rgba(0, 0, 0, 0.05) 0px 4px 6px -2px; 286 | color: $dark-blue; 287 | background: #fff; 288 | input, label { 289 | margin: 0px 20px; 290 | font-family: $font; 291 | } 292 | 293 | input { 294 | border: none; 295 | box-shadow: rgb(0 0 0 / 8%) 3px 3px 6px 0px inset, 296 | rgba(255, 255, 255, 0.5) -3px -3px 6px 1px inset; 297 | padding: 10px; 298 | background: #F8F9FF; 299 | } 300 | h3 { 301 | text-align: center; 302 | font-weight: 400; 303 | } 304 | .button { 305 | margin: 25px auto; 306 | padding: 10px 20px; 307 | width: 200px; 308 | } 309 | } 310 | 311 | label { 312 | padding: 15px 0px 5px 10px; 313 | } 314 | 315 | // .center-link { 316 | // text-align: center; 317 | // } 318 | 319 | //flow chart styling 320 | 321 | .chart-page-container { 322 | display: flex; 323 | flex-direction: column; 324 | } 325 | 326 | .db-query-container { 327 | background: $primary-purple; 328 | border-radius: 18px 18px 0px 0px; 329 | box-sizing: border-box; 330 | border: 1px solid $primary-purple; 331 | width: 100%; 332 | color: white; 333 | padding: 10px 30px 0px 30px; 334 | margin: auto; 335 | p { 336 | margin: 5px; 337 | } 338 | section { 339 | display: grid; 340 | grid-template-columns: 3fr 1fr; 341 | grid-template-rows: 1fr 1fr; 342 | grid-template-areas: 343 | 'query query query copyButton' 344 | 'query query query testButton' 345 | ; 346 | align-items: center; 347 | background: $white; 348 | color: black; 349 | padding: 10px; 350 | img { 351 | width: 15px; 352 | grid-area: copyButton; 353 | justify-self: end; 354 | &:hover { 355 | width: 16px; 356 | } 357 | } 358 | .copied { 359 | width: 17px; 360 | margin-right: -2px; 361 | } 362 | .test-button { 363 | grid-area: testButton; 364 | color: white; 365 | background: $primary-blue; 366 | border-radius: 8px 0px 8px 8px; 367 | font-size: 12px; 368 | width: 60px; 369 | padding: 6px 0px; 370 | text-align: center; 371 | margin: auto; 372 | img { 373 | margin-left: 5px; 374 | width: 12px; 375 | } 376 | &:hover { 377 | transition: 0.1s ease-in; 378 | letter-spacing: 0.02em; 379 | border-radius: 0px 17.5px 17.5px 17.5px; 380 | } 381 | } 382 | 383 | 384 | input { 385 | text-align: center; 386 | box-shadow: none; 387 | background: transparent; 388 | padding-top: 1px; 389 | padding-bottom: 3px; 390 | margin: 0px; 391 | font-family: $font; 392 | font-size: 16px; 393 | border: 2px solid #dadada; 394 | } 395 | form { 396 | display: flex; 397 | flex-direction: row; 398 | box-shadow: none; 399 | flex-wrap: wrap; 400 | } 401 | select { 402 | width: 20px; 403 | border: none; 404 | background: $primary-blue; 405 | color: white; 406 | border-radius: 0px 8px 8px 8px; 407 | font-size: 18px; 408 | margin: 5px; 409 | text-align: center; 410 | text-decoration: none; 411 | appearance: none; 412 | -moz-appearance: none; 413 | -webkit-appearance: none; 414 | } 415 | .button { 416 | letter-spacing: 0.02em; 417 | align-self: self-end; 418 | margin: 0px; 419 | } 420 | } 421 | } 422 | 423 | 424 | .queryAndButton { 425 | grid-area: query; 426 | } 427 | 428 | //reactflow custom styling 429 | .react-flow { 430 | // background: #fafafa; 431 | background-image: 432 | radial-gradient(#dadada 6%, transparent 0%); 433 | background-position: 12px 12px; 434 | background-size: 25px 25px; 435 | background-color: rgb(255, 255, 255); 436 | } 437 | 438 | .custom-node { 439 | display: flex; 440 | justify-content: center; 441 | align-items: center; 442 | button{ 443 | border: none; 444 | background: transparent; 445 | margin: 20px; 446 | font-family: $font; 447 | } 448 | } 449 | 450 | .react-flow__handle-top { 451 | background: transparent; 452 | border: none; 453 | } 454 | .react-flow__handle-bottom { 455 | background: transparent; 456 | border: none; 457 | } 458 | .react-flow__handle-left { 459 | background: transparent; 460 | border: none; 461 | } 462 | .react-flow__handle-right { 463 | background: transparent; 464 | border: none; 465 | } 466 | 467 | .react-flow__node { 468 | background: #fff; 469 | } 470 | 471 | .flowButton { 472 | background: none; 473 | font-family: $font; 474 | border: none; 475 | text-align: left; 476 | padding: 0px 10px; 477 | cursor: pointer; 478 | font-size: 16px; 479 | } 480 | 481 | .clicked { 482 | background: #efefef; 483 | } 484 | 485 | .db-query-container button { 486 | font-family: $font; 487 | margin: 2px; 488 | border: none; 489 | padding: 5px; 490 | } 491 | 492 | [id^="undefined"] { 493 | font-weight: 800; 494 | background: $table-blue; 495 | font-size: 18px; 496 | text-align: center; 497 | padding: none; 498 | letter-spacing: 0.02em; 499 | border-radius: 18px 18px 0px 0px; 500 | } 501 | 502 | 503 | 504 | .clause-dropdown { 505 | color: red; 506 | } 507 | 508 | 509 | .submenu { 510 | display: none; 511 | } 512 | 513 | ul { 514 | cursor: pointer; 515 | } 516 | 517 | .queryBuilder { 518 | background: $primary-blue; 519 | border-radius: 0px 8px 8px 8px; 520 | color: $white; 521 | text-align: center; 522 | font-size: 12px; 523 | height: 20px; 524 | width: 50px; 525 | margin-top: 3px; 526 | margin-left: 2px; 527 | padding-top: 5px; 528 | cursor: pointer; 529 | &:hover { 530 | transition: 0.1s ease-in; 531 | letter-spacing: 0.02em; 532 | border-radius: 8px 0px 8px 8px; 533 | } 534 | ul { 535 | list-style: none; 536 | display: none; 537 | color: black; 538 | background: #f1f0f0; 539 | z-index: 2; 540 | position: relative; 541 | padding: 20px; 542 | min-width: 200px; 543 | text-align: left; 544 | font-size: 12px; 545 | border-radius: 0px 18px 18px 18px; 546 | box-shadow: rgba(50, 50, 93, 0.25) 0px 30px 60px -12px, rgba(0, 0, 0, 0.3) 0px 18px 36px -18px; 547 | ul { 548 | box-shadow: none; 549 | // background: #fff; 550 | margin-top:5px; 551 | margin: auto; 552 | li { 553 | text-align: left; 554 | padding-left: 20px; 555 | &:hover { 556 | background: #dadada; 557 | 558 | } 559 | } 560 | } 561 | li { 562 | transition: 0.1s ease-out; 563 | padding: 5px 0px; 564 | font-size: 14px; 565 | cursor: pointer; 566 | &:hover { 567 | transition: 0.01s ease-in; 568 | letter-spacing: 0.02em; 569 | color: blue; 570 | } 571 | } 572 | } 573 | } 574 | 575 | .test-results-button { 576 | margin: 10px 0px; 577 | width: fit-content; 578 | padding: 5px 10px; 579 | height: 30px; 580 | cursor: pointer; 581 | img { 582 | height: 12px; 583 | } 584 | &:hover { 585 | letter-spacing: 0.02em; 586 | } 587 | } 588 | .test-results-button-expanded { 589 | margin: 10px 0px; 590 | cursor: pointer; 591 | list-style: none; 592 | color: black; 593 | background: #f1f0f0; 594 | width: inherit; 595 | text-align: left; 596 | display: block; 597 | border-radius: 0px 18px 18px 18px; 598 | box-shadow: rgba(50, 50, 93, 0.25) 0px 30px 60px -12px, rgba(0, 0, 0, 0.3) 0px 18px 36px -18px; 599 | div { 600 | padding: 20px 20px 0px 20px; 601 | } 602 | ol { 603 | padding: 10px; 604 | padding-top: 0px; 605 | li { 606 | justify-content: space-between; 607 | display: flex !important; 608 | flex-wrap: wrap; 609 | padding: 5px; 610 | margin: 5px 0px; 611 | background: #eaeaea; 612 | span { 613 | display: inline-block; 614 | white-space: nowrap; 615 | overflow: hidden; 616 | text-overflow: ellipsis; 617 | } 618 | &:hover { 619 | color: $primary-blue; 620 | border-radius: 18px; 621 | background-color: #fafafa; 622 | padding: 5px; 623 | span { 624 | transition: 0.2s; 625 | max-width: 100%; 626 | letter-spacing: 0.02em; 627 | margin-right: 10px; 628 | } 629 | // overflow: visible; 630 | // white-space: normal; 631 | // text-overflow: ellipsis; 632 | } 633 | } 634 | } 635 | img { 636 | height: 12px; 637 | } 638 | } 639 | 640 | .test-results-icon { 641 | margin: 0px 5px; 642 | transition: 0.5s; 643 | } 644 | 645 | .test-results-icon-expanded { 646 | margin: 0px 0px -2px 5px; 647 | transition: 0.5s; 648 | transform: rotate(180deg); 649 | } 650 | 651 | .top10 { 652 | // color: rgb(64, 0, 255); 653 | font-size: 14px !important; 654 | font-weight: bold; 655 | } 656 | 657 | .dropdownIcon { 658 | float: right; 659 | height: 0.7em; 660 | } 661 | 662 | .dropdownIconExpanded { 663 | transition: 0.1s ease-out; 664 | transform: rotate(-180deg); 665 | float: right; 666 | height: 0.7em; 667 | } 668 | 669 | .modal { 670 | display: grid; 671 | grid-template-rows: repeat(5, auto); 672 | padding-bottom: 30px; 673 | p { 674 | padding: 0px 20px; 675 | } 676 | 677 | .button { 678 | padding: 10px 20px; 679 | cursor: pointer; 680 | position: relative; 681 | margin: 10px auto; 682 | width: 75%; 683 | } 684 | 685 | p { 686 | display: none; 687 | text-align: center; 688 | } 689 | 690 | h3 { 691 | font-family: $header-font; 692 | font-size: 24px; 693 | font-weight: bold; 694 | text-align: center; 695 | } 696 | 697 | img { 698 | float: right; 699 | cursor: help; 700 | position: relative; 701 | } 702 | } 703 | 704 | .overlay { 705 | margin: auto; 706 | margin-top: 8%; 707 | overflow: auto; 708 | width: 600px; 709 | background: $modal-gradient; 710 | opacity: 1; 711 | box-shadow: rgba(50, 50, 93, 0.25) 0px 30px 60px -12px, rgba(0, 0, 0, 0.3) 0px 18px 36px -18px; 712 | border-radius: 18px; 713 | outline: none; 714 | } 715 | 716 | 717 | .on-columns-modal { 718 | display: grid; 719 | grid-template-columns: repeat(3, auto); 720 | grid-template-rows: repeat(3, auto); 721 | justify-content: space-around; 722 | justify-items: center; 723 | grid-template-areas: 724 | "title title title" 725 | "tableOne on tableTwo" 726 | "buttonModal buttonModal buttonModal"; 727 | padding: 20px; 728 | button { 729 | font-size: 12px; 730 | } 731 | .button { 732 | grid-area: buttonModal; 733 | padding: 10px 20px; 734 | margin-top: 30px; 735 | 736 | } 737 | h3 { 738 | grid-area: title; 739 | text-align: center; 740 | } 741 | div { 742 | h3 { 743 | font-weight: 800; 744 | font-family: $font; 745 | background: #a3bdff; 746 | font-size: 16px; 747 | text-align: center; 748 | padding: 10px;; 749 | margin: 0px; 750 | letter-spacing: 0.02em; 751 | border-radius: 18px 18px 0px 0px; 752 | // border-bottom: 2px solid #dadada; 753 | } 754 | h1 { 755 | grid-area: on; 756 | } 757 | button { 758 | padding: 10px 10px; 759 | display: flex; 760 | justify-content: space-between; 761 | &:hover { 762 | // background: #f4f4f4; 763 | letter-spacing: 0.02em; 764 | cursor: pointer; 765 | } 766 | } 767 | display: flex; 768 | flex-direction: column; 769 | width: 200px; 770 | background: #fafafa; 771 | border-radius: 18px 18px 5px 5px; 772 | } 773 | .tableOne { 774 | grid-area: tableOne; 775 | } 776 | .tableTwo { 777 | grid-area: tableTwo; 778 | } 779 | } 780 | 781 | .nodeGrid { 782 | display: flex; 783 | justify-content: space-between; 784 | } 785 | 786 | .FKPK { 787 | color: #b0b0b0; 788 | margin-left: 5px; 789 | } 790 | 791 | .dataType { 792 | color: #b0b0b0; 793 | text-align: right; 794 | } 795 | 796 | .label { 797 | text-align: right; 798 | } 799 | 800 | 801 | 802 | .notification-box { 803 | position: relative; 804 | z-index: 2; 805 | align-self: flex-end; 806 | animation-duration: 2s; 807 | animation-iteration-count: 2; 808 | font-family: $header-font; 809 | transform-origin: bottom; 810 | } 811 | 812 | .notification { 813 | animation-name: bounce-7; 814 | animation-timing-function: cubic-bezier(0.280, 0.840, 0.420, 1); 815 | } 816 | 817 | @keyframes bounce-7 { 818 | 0% { 819 | transform: scale(1, 1) translateY(0); 820 | } 821 | 822 | 10% { 823 | transform: scale(1.1, .9) translateY(0); 824 | } 825 | 826 | 30% { 827 | transform: scale(.9, 1.1) translateY(-10px); 828 | } 829 | 830 | 50% { 831 | transform: scale(1.05, .95) translateY(0); 832 | } 833 | 834 | 57% { 835 | transform: scale(1, 1) translateY(-2px); 836 | } 837 | 838 | 64% { 839 | transform: scale(1, 1) translateY(0); 840 | } 841 | 842 | 100% { 843 | transform: scale(1, 1) translateY(0); 844 | } 845 | } 846 | 847 | .border { 848 | border-style: solid; 849 | border-radius: 1em; 850 | } 851 | 852 | table { 853 | margin-left: auto; 854 | margin-right: auto; 855 | border-collapse: collapse; 856 | border-spacing: 0; 857 | border-radius: 30px 30px 0px 0px; 858 | border: 10px solid #7300ff; 859 | } 860 | 861 | th, td { 862 | border: 1px solid; 863 | text-align: center; 864 | } 865 | 866 | th { 867 | font-size: 1.5em; 868 | font-weight: bold; 869 | } 870 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Yesql 7 | 8 | 9 | 10 |
11 |
12 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yesql", 3 | "homepage": "https://oslabs-beta.github.io/YESQL/", 4 | "version": "1.0.0", 5 | "description": "", 6 | "main": "./build/bundle.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "start": "NODE_ENV=production node api/index.js", 10 | "dev": "cross-env NODE_ENV=development concurrently \"webpack serve --open\" \"nodemon api/index.js\"", 11 | "build": "NODE_ENV=production webpack" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "@babel/preset-env": "^7.24.0", 17 | "@babel/preset-react": "^7.23.3", 18 | "concurrently": "^8.2.2", 19 | "css-loader": "^6.10.0", 20 | "eslint": "^8.47.0", 21 | "eslint-config-airbnb": "^19.0.4", 22 | "eslint-config-google": "^0.14.0", 23 | "eslint-config-prettier": "^9.0.0", 24 | "eslint-import-resolver-typescript": "^3.6.0", 25 | "eslint-plugin-import": "^2.28.1", 26 | "eslint-plugin-jsx-a11y": "^6.7.1", 27 | "eslint-plugin-react": "^7.33.2", 28 | "eslint-plugin-react-hooks": "^4.6.0", 29 | "file-loader": "^6.2.0", 30 | "html-webpack-plugin": "^5.6.0", 31 | "nodemon": "^3.1.0", 32 | "react-bootstrap": "^2.10.2", 33 | "sass-loader": "^14.1.1", 34 | "style-loader": "^3.3.4", 35 | "typescript": "^5.3.3", 36 | "webpack-cli": "^5.1.4", 37 | "webpack-dev-server": "^5.0.2" 38 | }, 39 | "dependencies": { 40 | "@reduxjs/toolkit": "^2.2.1", 41 | "babel-loader": "^9.1.3", 42 | "body-parser": "^1.20.2", 43 | "cors": "^2.8.5", 44 | "cross-env": "^7.0.3", 45 | "dotenv": "^16.4.5", 46 | "express": "^4.18.2", 47 | "pg": "^8.11.3", 48 | "react": "^18.2.0", 49 | "react-dom": "^18.2.0", 50 | "react-modal": "^3.16.1", 51 | "react-redux": "^9.1.0", 52 | "react-router": "^6.22.3", 53 | "react-router-dom": "^6.22.2", 54 | "reactflow": "^11.10.4", 55 | "sass": "^1.71.1", 56 | "ts-node": "^10.9.2", 57 | "url-loader": "^4.1.1" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { "version": 2, 2 | "routes": [ 3 | { 4 | "src": "/(.*)", 5 | "dest": "/" 6 | } 7 | ], 8 | "installCommand": "npm install", 9 | "outputDirectory": "build"} -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | module.exports = { 6 | entry: './frontend/index.js', 7 | output: { 8 | path: path.join(__dirname, 'build'), 9 | publicPath: '/', 10 | filename: 'bundle.js', 11 | }, 12 | mode: process.env.NODE_ENV, 13 | plugins: [ 14 | new HtmlWebpackPlugin({ 15 | template: './index.html', 16 | }), 17 | ], 18 | resolve: { 19 | extensions: ['.jsx', '.js'], 20 | }, 21 | devServer: { 22 | port: 8080, 23 | open: true, 24 | hot: true, 25 | proxy: [{ 26 | context: ['/connect', '/', '/chart'], 27 | target: 'http://localhost:3000', 28 | }], 29 | static: { 30 | directory: path.resolve(__dirname, 'build'), 31 | publicPath: '/', 32 | }, 33 | }, 34 | module: { 35 | rules: [ 36 | { 37 | test: /.(js|jsx)$/, 38 | exclude: /node_modules/, 39 | use: { 40 | loader: 'babel-loader', 41 | options: { 42 | presets: ['@babel/preset-env', '@babel/preset-react'], 43 | }, 44 | }, 45 | }, 46 | { 47 | test: /\.css$/, 48 | use: ['style-loader', 'css-loader'], 49 | }, 50 | { 51 | test: /\.(s(a|c)ss)$/, 52 | use: ['style-loader', 'css-loader', 'sass-loader'], 53 | }, 54 | { 55 | test: /\.(png|svg|jpg|jpeg|gif)$/i, 56 | use: [ 57 | { 58 | loader: 'file-loader', 59 | }, 60 | ], 61 | }, 62 | ], 63 | }, 64 | }; 65 | --------------------------------------------------------------------------------