├── public ├── favicon.ico ├── images │ ├── folder.png │ ├── Sigma Logo.png │ ├── Create Folder.png │ ├── Sigma Logo Full.png │ └── Browser Preview Final.png └── vercel.svg ├── postcss.config.js ├── fonts └── FontsFree-Net-Hustle-Bright.ttf ├── cors.json ├── .eslintrc.json ├── next.config.js ├── amplify ├── backend │ ├── tags.json │ ├── types │ │ └── amplify-dependent-resources-ref.d.ts │ ├── backend-config.json │ └── auth │ │ └── sigma517eb24f │ │ └── cli-inputs.json ├── README.md ├── hooks │ └── README.md ├── .config │ └── project-config.json ├── team-provider-info.json └── cli.json ├── pages ├── api │ └── hello.js ├── _app.js ├── _document.js ├── index.js ├── auth.js ├── files.js └── edit.js ├── store ├── userSlice.js ├── selectedElementSlice.js ├── store.js ├── pageSlice.js ├── roomSlice.js ├── backgroundSlice.js └── elementSlice.js ├── styles ├── globals.css ├── ColorPicker.module.css ├── SmallBoard.module.css ├── Board.module.css ├── IconForm.module.css ├── CodeModal.module.css ├── ImageForm.module.css ├── TextForm.module.css ├── Icons.module.css ├── BackgroundForm.module.css ├── ItemResizer.module.css ├── Auth.module.css ├── ShapesForm.module.css ├── Unsplash.module.css ├── Files.module.css ├── Edit.module.css └── Home.module.css ├── components ├── CodeModal.js ├── ColorPicker.js ├── SmallBoard.js ├── Icons.js ├── ImageForm.js ├── ShapesForm.js ├── IconForm.js ├── BackgroundForm.js ├── Unsplash.js ├── TextForm.js ├── ItemResizer.js └── Board.js ├── .gitignore ├── src └── aws-exports.js ├── package.json ├── LICENSE.md ├── firebaseConfig.js ├── hooks └── useElementUpdate.js └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Keshraf/sigma/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/images/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Keshraf/sigma/HEAD/public/images/folder.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /public/images/Sigma Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Keshraf/sigma/HEAD/public/images/Sigma Logo.png -------------------------------------------------------------------------------- /public/images/Create Folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Keshraf/sigma/HEAD/public/images/Create Folder.png -------------------------------------------------------------------------------- /public/images/Sigma Logo Full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Keshraf/sigma/HEAD/public/images/Sigma Logo Full.png -------------------------------------------------------------------------------- /fonts/FontsFree-Net-Hustle-Bright.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Keshraf/sigma/HEAD/fonts/FontsFree-Net-Hustle-Bright.ttf -------------------------------------------------------------------------------- /cors.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "origin": ["*"], 4 | "method": ["GET"], 5 | "maxAgeSeconds": 3600 6 | } 7 | ] -------------------------------------------------------------------------------- /public/images/Browser Preview Final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Keshraf/sigma/HEAD/public/images/Browser Preview Final.png -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "@next/next/no-img-element": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: false, 4 | swcMinify: true, 5 | }; 6 | 7 | module.exports = nextConfig; 8 | -------------------------------------------------------------------------------- /amplify/backend/tags.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Key": "user:Stack", 4 | "Value": "{project-env}" 5 | }, 6 | { 7 | "Key": "user:Application", 8 | "Value": "{project-name}" 9 | } 10 | ] -------------------------------------------------------------------------------- /pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default function handler(req, res) { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /amplify/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Amplify CLI 2 | This directory was generated by [Amplify CLI](https://docs.amplify.aws/cli). 3 | 4 | Helpful resources: 5 | - Amplify documentation: https://docs.amplify.aws 6 | - Amplify CLI documentation: https://docs.amplify.aws/cli 7 | - More details on this folder & generated files: https://docs.amplify.aws/cli/reference/files 8 | - Join Amplify's community: https://amplify.aws/community/ 9 | -------------------------------------------------------------------------------- /amplify/hooks/README.md: -------------------------------------------------------------------------------- 1 | # Command Hooks 2 | 3 | Command hooks can be used to run custom scripts upon Amplify CLI lifecycle events like pre-push, post-add-function, etc. 4 | 5 | To get started, add your script files based on the expected naming convention in this directory. 6 | 7 | Learn more about the script file naming convention, hook parameters, third party dependencies, and advanced configurations at https://docs.amplify.aws/cli/usage/command-hooks 8 | -------------------------------------------------------------------------------- /amplify/.config/project-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "sigma", 3 | "version": "3.1", 4 | "frontend": "javascript", 5 | "javascript": { 6 | "framework": "react", 7 | "config": { 8 | "SourceDir": "src", 9 | "DistributionDir": "build", 10 | "BuildCommand": "npm.cmd run-script build", 11 | "StartCommand": "npm.cmd run-script start" 12 | } 13 | }, 14 | "providers": [ 15 | "awscloudformation" 16 | ] 17 | } -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import { store } from "../store/store"; 3 | import { Provider } from "react-redux"; 4 | import { Amplify } from "aws-amplify"; 5 | import awsconfig from "../src/aws-exports"; 6 | Amplify.configure(awsconfig); 7 | 8 | function MyApp({ Component, pageProps }) { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | } 15 | 16 | export default MyApp; 17 | -------------------------------------------------------------------------------- /amplify/backend/types/amplify-dependent-resources-ref.d.ts: -------------------------------------------------------------------------------- 1 | export type AmplifyDependentResourcesAttributes = { 2 | "auth": { 3 | "sigma517eb24f": { 4 | "IdentityPoolId": "string", 5 | "IdentityPoolName": "string", 6 | "UserPoolId": "string", 7 | "UserPoolArn": "string", 8 | "UserPoolName": "string", 9 | "AppClientIDWeb": "string", 10 | "AppClientID": "string" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /store/userSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | export const userSlice = createSlice({ 4 | name: "user", 5 | initialState: { 6 | user: "", 7 | }, 8 | reducers: { 9 | setUser(state, action) { 10 | console.log("SET USER: ", action.payload); 11 | state.user = action.payload; 12 | }, 13 | resetUser(state) { 14 | state.user = ""; 15 | }, 16 | }, 17 | }); 18 | 19 | export const { setUser, resetUser } = userSlice.actions; 20 | export default userSlice.reducer; 21 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | overflow-x: hidden; 4 | padding: 0; 5 | margin: 0; 6 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 7 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 8 | background-color: #121219; 9 | color: #FFF !important; 10 | scrollbar-gutter: auto; 11 | } 12 | 13 | a { 14 | color: inherit; 15 | text-decoration: none; 16 | } 17 | 18 | * { 19 | box-sizing: border-box; 20 | } 21 | 22 | @font-face { 23 | font-family: "Hustle Bright"; 24 | src: url('../fonts/FontsFree-Net-Hustle-Bright.ttf'); 25 | } -------------------------------------------------------------------------------- /styles/ColorPicker.module.css: -------------------------------------------------------------------------------- 1 | .alignBox { 2 | width: 100%; 3 | display: flex; 4 | flex-direction: row; 5 | justify-content: flex-start; 6 | align-items: center; 7 | border: 1px solid #121219; 8 | border-radius: 5px; 9 | height: 50px; 10 | padding-left: 24px; 11 | gap: 5px; 12 | } 13 | 14 | 15 | .colorLabel { 16 | width: 24px; 17 | height: 24px; 18 | border: 1px solid #121219; 19 | cursor: pointer; 20 | } 21 | 22 | .colorInputText { 23 | background-color: transparent; 24 | font-size: 16px; 25 | color: #fff; 26 | width: 75px; 27 | border: none; 28 | outline: none; 29 | } -------------------------------------------------------------------------------- /store/selectedElementSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | id: "", 5 | }; 6 | 7 | export const selectedElementSlice = createSlice({ 8 | name: "selectedElement", 9 | initialState, 10 | reducers: { 11 | setSelectedElement(state, action) { 12 | return { 13 | ...action.payload, 14 | }; 15 | }, 16 | resetSelected(state) { 17 | return { 18 | id: "", 19 | }; 20 | }, 21 | }, 22 | }); 23 | 24 | export const { setSelectedElement, resetSelected } = 25 | selectedElementSlice.actions; 26 | 27 | export default selectedElementSlice.reducer; 28 | -------------------------------------------------------------------------------- /store/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import elementReducer from "./elementSlice"; 3 | import selectedElementReducer from "./selectedElementSlice"; 4 | import backgroundReducer from "./backgroundSlice"; 5 | import pageReducer from "./pageSlice"; 6 | import roomReducer from "./roomSlice"; 7 | import userReducer from "./userSlice"; 8 | 9 | export const store = configureStore({ 10 | reducer: { 11 | elements: elementReducer, 12 | selectedElement: selectedElementReducer, 13 | background: backgroundReducer, 14 | page: pageReducer, 15 | room: roomReducer, 16 | user: userReducer, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /amplify/backend/backend-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "auth": { 3 | "sigma517eb24f": { 4 | "service": "Cognito", 5 | "providerPlugin": "awscloudformation", 6 | "dependsOn": [], 7 | "customAuth": false, 8 | "frontendAuthConfig": { 9 | "socialProviders": [], 10 | "usernameAttributes": [], 11 | "signupAttributes": [ 12 | "EMAIL" 13 | ], 14 | "passwordProtectionSettings": { 15 | "passwordPolicyMinLength": 8, 16 | "passwordPolicyCharacters": [] 17 | }, 18 | "mfaConfiguration": "OFF", 19 | "mfaTypes": [ 20 | "SMS" 21 | ], 22 | "verificationMechanisms": [ 23 | "EMAIL" 24 | ] 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /amplify/team-provider-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "dev": { 3 | "awscloudformation": { 4 | "AuthRoleName": "amplify-sigma-dev-193652-authRole", 5 | "UnauthRoleArn": "arn:aws:iam::742859811898:role/amplify-sigma-dev-193652-unauthRole", 6 | "AuthRoleArn": "arn:aws:iam::742859811898:role/amplify-sigma-dev-193652-authRole", 7 | "Region": "us-west-2", 8 | "DeploymentBucketName": "amplify-sigma-dev-193652-deployment", 9 | "UnauthRoleName": "amplify-sigma-dev-193652-unauthRole", 10 | "StackName": "amplify-sigma-dev-193652", 11 | "StackId": "arn:aws:cloudformation:us-west-2:742859811898:stack/amplify-sigma-dev-193652/f8d21000-3fff-11ed-ab66-0afb0b50396d", 12 | "AmplifyAppId": "d20ev0puquaogc" 13 | }, 14 | "categories": { 15 | "auth": { 16 | "sigma517eb24f": {} 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | // pages/_document.js 2 | 3 | import Document, { Html, Head, Main, NextScript } from "next/document"; 4 | 5 | class MyDocument extends Document { 6 | render() { 7 | return ( 8 | 9 | 10 | 11 | 16 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | ); 27 | } 28 | } 29 | 30 | export default MyDocument; 31 | -------------------------------------------------------------------------------- /components/CodeModal.js: -------------------------------------------------------------------------------- 1 | import toast from "react-hot-toast"; 2 | import { FiCopy } from "react-icons/fi"; 3 | import { useSelector } from "react-redux"; 4 | import styles from "../styles/CodeModal.module.css"; 5 | 6 | const CodeModal = ({ setModalOpen }) => { 7 | const roomId = useSelector((state) => state.room.id); 8 | 9 | // Attaches the text to the clipboard 10 | const copyHandler = () => { 11 | navigator.clipboard.writeText(roomId); 12 | toast.success("Copied to Clipboard!"); 13 | console.log("Copied!"); 14 | setModalOpen(false); 15 | }; 16 | 17 | return ( 18 |
setModalOpen(false)}> 19 |
20 |
21 | 22 |
23 |
{roomId}
24 |
25 |
26 | ); 27 | }; 28 | 29 | export default CodeModal; 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | #amplify-do-not-edit-begin 39 | amplify/\#current-cloud-backend 40 | amplify/.config/local-* 41 | amplify/logs 42 | amplify/mock-data 43 | amplify/backend/amplify-meta.json 44 | amplify/backend/.temp 45 | build/ 46 | dist/ 47 | node_modules/ 48 | awsconfiguration.json 49 | amplifyconfiguration.json 50 | amplifyconfiguration.dart 51 | amplify-build-config.json 52 | amplify-gradle-config.json 53 | amplifytools.xcconfig 54 | .secret-* 55 | **.sample 56 | #amplify-do-not-edit-end 57 | -------------------------------------------------------------------------------- /styles/SmallBoard.module.css: -------------------------------------------------------------------------------- 1 | .board { 2 | width: 192px; 3 | height: 108px; 4 | 5 | min-width: 192px; 6 | min-height: 108px; 7 | 8 | background-repeat: no-repeat; 9 | background-size:cover; 10 | border-radius: 5px; 11 | border: 1px solid #FFFFFF; 12 | cursor: pointer; 13 | position: relative; 14 | display: flex; 15 | flex-direction: row; 16 | align-items: center; 17 | justify-content: center; 18 | gap: 40px; 19 | font-size: 24px; 20 | background-color: #FFFFFF; 21 | } 22 | 23 | .page{ 24 | width: 40px; 25 | height: 40px; 26 | background-color: rgba(236, 86, 86, 0.342); 27 | display: flex; 28 | flex-direction: row; 29 | justify-content: center; 30 | align-items: center; 31 | font-family: 'Poppins'; 32 | } 33 | .trash{ 34 | width: 40px; 35 | height: 40px; 36 | color: #7f1d1d; 37 | display: flex; 38 | flex-direction: row; 39 | justify-content: center; 40 | align-items: center; 41 | background-color: #dc2626; 42 | } -------------------------------------------------------------------------------- /src/aws-exports.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // WARNING: DO NOT EDIT. This file is automatically generated by AWS Amplify. It will be overwritten. 3 | 4 | const awsmobile = { 5 | "aws_project_region": "us-west-2", 6 | "aws_cognito_identity_pool_id": "us-west-2:7b707546-b946-4dcc-b855-422781a147ce", 7 | "aws_cognito_region": "us-west-2", 8 | "aws_user_pools_id": "us-west-2_om2vYM3Nf", 9 | "aws_user_pools_web_client_id": "3jecl4c13tl8lcq60t2cesqjvb", 10 | "oauth": {}, 11 | "aws_cognito_username_attributes": [], 12 | "aws_cognito_social_providers": [], 13 | "aws_cognito_signup_attributes": [ 14 | "EMAIL" 15 | ], 16 | "aws_cognito_mfa_configuration": "OFF", 17 | "aws_cognito_mfa_types": [ 18 | "SMS" 19 | ], 20 | "aws_cognito_password_protection_settings": { 21 | "passwordPolicyMinLength": 8, 22 | "passwordPolicyCharacters": [] 23 | }, 24 | "aws_cognito_verification_mechanisms": [ 25 | "EMAIL" 26 | ] 27 | }; 28 | 29 | 30 | export default awsmobile; 31 | -------------------------------------------------------------------------------- /store/pageSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | current: 1, 5 | pages: [1], 6 | }; 7 | 8 | export const pageSlice = createSlice({ 9 | name: "pages", 10 | initialState, 11 | reducers: { 12 | addPage(state) { 13 | const value = state.pages[state.pages.length - 1] + 1; 14 | state.pages.push(value); 15 | }, 16 | setCurrentPage(state, action) { 17 | const newState = { 18 | ...state, 19 | current: action.payload.current, 20 | }; 21 | return newState; 22 | }, 23 | setPage(state, action) { 24 | const value = []; 25 | for (let i = 1; i <= action.payload.pages; i++) { 26 | value.push(i); 27 | } 28 | state.pages = value; 29 | }, 30 | resetPage(state) { 31 | return { 32 | current: 1, 33 | pages: [1], 34 | }; 35 | }, 36 | }, 37 | }); 38 | 39 | export const { addPage, setCurrentPage, setPage, resetPage } = 40 | pageSlice.actions; 41 | export default pageSlice.reducer; 42 | -------------------------------------------------------------------------------- /store/roomSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | id: "", 5 | pages: 1, 6 | admin: "", 7 | name: "", 8 | }; 9 | 10 | export const roomSlice = createSlice({ 11 | name: "room", 12 | initialState, 13 | reducers: { 14 | setInitialRoom(state, action) { 15 | return { 16 | id: action.payload.id, 17 | admin: action.payload.admin, 18 | name: action.payload.name, 19 | pages: action.payload.pages, 20 | }; 21 | }, 22 | setRoom(state, action) { 23 | state.id = action.payload.id; 24 | }, 25 | addPageRoom(state) { 26 | state.pages++; 27 | }, 28 | setPageRoom(state, action) { 29 | state.pages = action.payload.pages; 30 | }, 31 | setRoomName(state, action) { 32 | state.name = action.payload.name; 33 | }, 34 | }, 35 | }); 36 | 37 | export const { 38 | setRoom, 39 | addPageRoom, 40 | setPageRoom, 41 | setInitialRoom, 42 | setRoomName, 43 | } = roomSlice.actions; 44 | export default roomSlice.reducer; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sigma", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@onehop/client": "^1.3.14", 13 | "@onehop/react": "^1.3.14", 14 | "@react-spring/web": "^9.5.2", 15 | "@reduxjs/toolkit": "^1.8.5", 16 | "@stitches/react": "^1.2.8", 17 | "@use-gesture/react": "^10.2.19", 18 | "aws-amplify": "^4.3.36", 19 | "axios": "^0.27.2", 20 | "firebase": "^9.9.4", 21 | "html2canvas": "^1.4.1", 22 | "nanoid": "^4.0.0", 23 | "next": "12.2.5", 24 | "react": "18.2.0", 25 | "react-dom": "18.2.0", 26 | "react-hot-toast": "^2.3.0", 27 | "react-icons": "^4.4.0", 28 | "react-redux": "^8.0.2", 29 | "react-spring": "^9.5.2", 30 | "unsplash-js": "^7.0.15" 31 | }, 32 | "devDependencies": { 33 | "autoprefixer": "^10.4.8", 34 | "eslint": "8.23.0", 35 | "eslint-config-next": "12.2.5", 36 | "postcss": "^8.4.16" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /styles/Board.module.css: -------------------------------------------------------------------------------- 1 | .board { 2 | position: relative; 3 | background-color: #171720; 4 | border: 1px solid #20222D; 5 | border-radius: 10px; 6 | height: 540px; 7 | width: 960px; 8 | margin-top: 20px; 9 | margin-bottom: 30px; 10 | /* transform: scale(0.5); */ 11 | } 12 | 13 | .smallBoard { 14 | position: absolute; 15 | top: -191px; 16 | left: -369px; 17 | /* background-color: #171720; */ 18 | border: 1px solid #20222D; 19 | border-radius: 10px; 20 | height: 540px; 21 | width: 960px; 22 | min-width: 192px; 23 | transform: scale(0.2,0.2); 24 | border: 1px solid #ffffff; 25 | } 26 | 27 | .smallBoard:hover { 28 | opacity: 0.7; 29 | } 30 | 31 | .disable { 32 | position: relative; 33 | z-index: 40000 !important; 34 | width: 100%; 35 | height: 100%; 36 | background-color: rgba(0, 0, 0, 0.5); 37 | } 38 | 39 | .selectPage { 40 | position: absolute; 41 | top: -50px; 42 | left: -50px; 43 | font-size: 125px; 44 | color: #FAFC6F; 45 | border: 5px solid #FAFC6F; 46 | padding: 15px; 47 | border-radius: 100%; 48 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ketan Saraf 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 | -------------------------------------------------------------------------------- /firebaseConfig.js: -------------------------------------------------------------------------------- 1 | // Import the functions you need from the SDKs you need 2 | import { initializeApp } from "firebase/app"; 3 | import { getAnalytics } from "firebase/analytics"; 4 | import { getStorage } from "firebase/storage"; 5 | import { getDatabase } from "firebase/database"; 6 | // TODO: Add SDKs for Firebase products that you want to use 7 | // https://firebase.google.com/docs/web/setup#available-libraries 8 | 9 | // Your web app's Firebase configuration 10 | // For Firebase JS SDK v7.20.0 and later, measurementId is optional 11 | const firebaseConfig = { 12 | apiKey: "AIzaSyB94cIrlL_xf6xd2N88Op2c87fKDnMHNis", 13 | authDomain: "sigma-1662454704866.firebaseapp.com", 14 | projectId: "sigma-1662454704866", 15 | storageBucket: "sigma-1662454704866.appspot.com", 16 | messagingSenderId: "208208695629", 17 | appId: "1:208208695629:web:e245e91655dffd5ea3e704", 18 | measurementId: "G-ZQE4KTEQC1", 19 | databaseURL: 20 | "https://sigma-1662454704866-default-rtdb.asia-southeast1.firebasedatabase.app", 21 | }; 22 | 23 | // Initialize Firebase 24 | const app = initializeApp(firebaseConfig); 25 | /* const analytics = getAnalytics(app); */ 26 | export const storage = getStorage(app); 27 | export const database = getDatabase(app); 28 | -------------------------------------------------------------------------------- /components/ColorPicker.js: -------------------------------------------------------------------------------- 1 | import styles from "../styles/ColorPicker.module.css"; 2 | 3 | import { toast } from "react-hot-toast"; 4 | 5 | const ColorPicker = ({ color, setColor }) => { 6 | // Sets Color and Warns on & Resets wrong input 7 | const colorBlurHandler = (e) => { 8 | const value = e.target.value; 9 | if (value.length < 7) { 10 | toast.error("Invalid Color Input"); 11 | setColor("#FFFFFF"); 12 | return; 13 | } 14 | if (!value.startsWith("#")) { 15 | toast.error(`Add '#' to your Color Input`); 16 | setColor("#FFFFFF"); 17 | return; 18 | } 19 | }; 20 | 21 | return ( 22 |
23 | 28 | setColor(e.target.value)} 33 | style={{ display: "none", backgroundColor: color }} 34 | /> 35 | setColor(e.target.value)} 40 | className={styles.colorInputText} 41 | > 42 |
43 | ); 44 | }; 45 | 46 | export default ColorPicker; 47 | -------------------------------------------------------------------------------- /components/SmallBoard.js: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from "react-redux"; 2 | import { setCurrentPage } from "../store/pageSlice"; 3 | import { FiTrash2 } from "react-icons/fi"; 4 | import styles from "../styles/SmallBoard.module.css"; 5 | 6 | const SmallBoard = ({ page }) => { 7 | const dispatch = useDispatch(); 8 | const background = useSelector((state) => state.background); 9 | const current = useSelector((state) => state.page.current); 10 | const pageBackground = background.filter((bg) => bg.page === page); 11 | const latestBackground = pageBackground[pageBackground.length - 1]; 12 | 13 | const selectHandler = () => { 14 | dispatch( 15 | setCurrentPage({ 16 | current: page, 17 | }) 18 | ); 19 | }; 20 | 21 | return ( 22 |
32 | {/*
{current}
33 |
34 | 35 |
*/} 36 |
37 | ); 38 | }; 39 | 40 | export default SmallBoard; 41 | -------------------------------------------------------------------------------- /styles/IconForm.module.css: -------------------------------------------------------------------------------- 1 | .alignRow { 2 | display: flex; 3 | width: 100%; 4 | height: auto; 5 | flex-direction: row; 6 | gap: 15px; 7 | } 8 | 9 | .inputUnshaded { 10 | background-color: transparent; 11 | padding-left: 24px; 12 | padding-right: 24px; 13 | height: 50px; 14 | width: 100%; 15 | font-size: 18px; 16 | border: 1px solid #121219; 17 | border-radius: 5px; 18 | font-family: 'Poppins'; 19 | margin-bottom: 15px; 20 | color: #ffffff; 21 | } 22 | 23 | .clickButtonUnshaded { 24 | width: 100%; 25 | height: 50px; 26 | background-color: transparent; 27 | display: flex; 28 | flex-direction: row; 29 | justify-content: flex-start; 30 | align-items: center; 31 | border: 1px solid #121219; 32 | border-radius: 5px; 33 | font-family: 'Poppins'; 34 | font-size: 18px; 35 | gap:15px; 36 | padding-left: 24px; 37 | margin-bottom: 15px; 38 | color: #FFFFFF; 39 | } 40 | .clickButtonUnshaded:hover{ 41 | background-color: #171720; 42 | } 43 | 44 | .submitButton { 45 | background-color: #2599FF; 46 | display: flex; 47 | flex-direction: row; 48 | justify-content: center; 49 | margin-top: 20px; 50 | align-items: center; 51 | height: 50px; 52 | width: 300px; 53 | font-size: 18px; 54 | border: none; 55 | border-radius: 5px; 56 | font-family: 'Poppins'; 57 | color: #fff; 58 | } -------------------------------------------------------------------------------- /hooks/useElementUpdate.js: -------------------------------------------------------------------------------- 1 | // React & Redux 2 | import { useCallback } from "react"; 3 | import { useSelector } from "react-redux"; 4 | 5 | // Firebase 6 | import { ref, get, child, update } from "firebase/database"; 7 | import { database } from "../firebaseConfig"; 8 | 9 | export default function useElementUpdate() { 10 | const selected = useSelector((state) => state.selectedElement); // Gets the selected Element 11 | const roomId = useSelector((state) => state.room.id); // gets the room id 12 | 13 | const elementUpdate = useCallback( 14 | (data) => { 15 | const dbRef = ref(database); 16 | // Gets the list of all the elements in the database for that room 17 | get(child(dbRef, `elements/${roomId}`)).then((snapshot) => { 18 | if (snapshot.exists()) { 19 | // Loops over each element of that room 20 | snapshot.forEach((childSnapshot) => { 21 | const childValue = childSnapshot.val(); 22 | // Updates the selected element with the passed data 23 | if (childValue.id === selected.id) { 24 | update( 25 | ref(database, `elements/${roomId}/${childSnapshot.key}`), 26 | data 27 | ); 28 | } 29 | }); 30 | } else { 31 | // Prints this if there are no elements 32 | console.log("No data available"); 33 | } 34 | }); 35 | }, 36 | [roomId, selected.id] 37 | ); 38 | 39 | return elementUpdate; 40 | } 41 | -------------------------------------------------------------------------------- /styles/CodeModal.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: absolute; 3 | top: -50%; 4 | bottom: 0; 5 | right: 0; 6 | left: -50%; 7 | width: 200%; 8 | height: 200%; 9 | box-shadow: 0 0 2000px 2000px rgb(0 0 0 / 30%); 10 | z-index: 20000; 11 | background-color: rgba(0, 0, 0, 0.4); 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: center; 15 | align-items: center; 16 | backdrop-filter: blur(100px); 17 | overflow: hidden !important; 18 | animation: blury 1s ease-out; 19 | } 20 | 21 | @keyframes blury { 22 | 0% { backdrop-filter: blur(1px); } 23 | 100% { backdrop-filter: blur(100px); } 24 | } 25 | 26 | .innerContainer { 27 | width: 400px; 28 | height: 8vh; 29 | background-color: transparent; 30 | border: 1px solid #20222D; 31 | border-radius: 10px; 32 | padding: 15px; 33 | padding-left: 25px; 34 | padding-right: 25px; 35 | display: flex; 36 | flex-direction: row; 37 | justify-content: space-between; 38 | align-items: center; 39 | gap: 5px; 40 | 41 | } 42 | 43 | .codeContainer { 44 | width: 280px; 45 | height: 40px; 46 | border-radius: 5px; 47 | background-color: #303544; 48 | color: #ffffff; 49 | display: flex; 50 | justify-content: center; 51 | align-items: center; 52 | } 53 | 54 | .iconContainer { 55 | width: 45px; 56 | height: 40px; 57 | display: flex; 58 | justify-content: center; 59 | align-items: center; 60 | border-radius: 5px; 61 | cursor: pointer; 62 | } 63 | 64 | .iconContainer:hover { 65 | background-color: #20222D; 66 | } -------------------------------------------------------------------------------- /amplify/backend/auth/sigma517eb24f/cli-inputs.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1", 3 | "cognitoConfig": { 4 | "identityPoolName": "sigma517eb24f_identitypool_517eb24f", 5 | "allowUnauthenticatedIdentities": false, 6 | "resourceNameTruncated": "sigma517eb24f", 7 | "userPoolName": "sigma517eb24f_userpool_517eb24f", 8 | "autoVerifiedAttributes": [ 9 | "email" 10 | ], 11 | "mfaConfiguration": "OFF", 12 | "mfaTypes": [ 13 | "SMS Text Message" 14 | ], 15 | "smsAuthenticationMessage": "Your authentication code is {####}", 16 | "smsVerificationMessage": "Your verification code is {####}", 17 | "emailVerificationSubject": "Your verification code", 18 | "emailVerificationMessage": "Your verification code is {####}", 19 | "defaultPasswordPolicy": false, 20 | "passwordPolicyMinLength": 8, 21 | "passwordPolicyCharacters": [], 22 | "requiredAttributes": [ 23 | "email" 24 | ], 25 | "aliasAttributes": [], 26 | "userpoolClientGenerateSecret": false, 27 | "userpoolClientRefreshTokenValidity": 30, 28 | "userpoolClientWriteAttributes": [ 29 | "email" 30 | ], 31 | "userpoolClientReadAttributes": [ 32 | "email" 33 | ], 34 | "userpoolClientLambdaRole": "sigma5517eb24f_userpoolclient_lambda_role", 35 | "userpoolClientSetAttributes": false, 36 | "sharedId": "517eb24f", 37 | "resourceName": "sigma517eb24f", 38 | "authSelections": "identityPoolAndUserPool", 39 | "useDefault": "default", 40 | "userPoolGroupList": [], 41 | "serviceName": "Cognito", 42 | "usernameCaseSensitive": false, 43 | "useEnabledMfas": true 44 | } 45 | } -------------------------------------------------------------------------------- /styles/ImageForm.module.css: -------------------------------------------------------------------------------- 1 | .clickButtonShaded { 2 | width: 100%; 3 | height: 50px; 4 | background-color: #303544; 5 | display: flex; 6 | flex-direction: row; 7 | justify-content: flex-start; 8 | align-items: center; 9 | border: none; 10 | border-radius: 5px; 11 | font-family: 'Poppins'; 12 | font-size: 18px; 13 | gap:5px; 14 | padding-left: 24px; 15 | cursor: pointer; 16 | } 17 | .clickButtonShaded:hover{ 18 | background-color: #171720; 19 | } 20 | 21 | .clickButtonUnshaded { 22 | width: 100%; 23 | height: 50px; 24 | background-color: transparent; 25 | display: flex; 26 | flex-direction: row; 27 | justify-content: flex-start; 28 | align-items: center; 29 | border: 1px solid #121219; 30 | border-radius: 5px; 31 | font-family: 'Poppins'; 32 | font-size: 18px; 33 | gap:15px; 34 | padding-left: 24px; 35 | } 36 | 37 | .clickButtonUnshaded:hover{ 38 | background-color: #171720; 39 | } 40 | 41 | .buttonLabel { 42 | display: flex; 43 | flex-direction: row; 44 | justify-content: center; 45 | align-items: center; 46 | gap:15px 47 | } 48 | 49 | .bgLayout { 50 | display: flex; 51 | flex-direction: column; 52 | gap: 20px; 53 | } 54 | 55 | .submitButton { 56 | background-color: #2599FF; 57 | display: flex; 58 | flex-direction: row; 59 | justify-content: center; 60 | margin-top: 20px; 61 | align-items: center; 62 | height: 50px; 63 | width: 300px; 64 | font-size: 18px; 65 | border: none; 66 | border-radius: 5px; 67 | font-family: 'Poppins'; 68 | color: #fff; 69 | } 70 | 71 | .launchButton { 72 | background-color: transparent; 73 | } -------------------------------------------------------------------------------- /styles/TextForm.module.css: -------------------------------------------------------------------------------- 1 | .alignRow { 2 | display: flex; 3 | width: 100%; 4 | height: auto; 5 | flex-direction: row; 6 | gap: 15px; 7 | } 8 | 9 | .alignBox { 10 | width: 100%; 11 | display: flex; 12 | flex-direction: row; 13 | justify-content: flex-start; 14 | align-items: center; 15 | border: 1px solid #121219; 16 | border-radius: 5px; 17 | height: 50px; 18 | overflow: hidden; 19 | } 20 | 21 | .submitButton { 22 | background-color: #2599FF; 23 | display: flex; 24 | flex-direction: row; 25 | justify-content: center; 26 | margin-top: 20px; 27 | align-items: center; 28 | height: 50px; 29 | width: 300px; 30 | font-size: 18px; 31 | border: none; 32 | border-radius: 5px; 33 | font-family: 'Poppins'; 34 | cursor: pointer; 35 | color: #fff; 36 | } 37 | 38 | .inputShaded { 39 | background-color: #303544; 40 | padding-left: 24px; 41 | height: 50px; 42 | width: 300px; 43 | font-size: 18px; 44 | border: none; 45 | border-radius: 5px; 46 | font-family: 'Poppins'; 47 | margin-bottom: 15px; 48 | color: #FFFFFF; 49 | } 50 | 51 | .inputUnshaded { 52 | background-color: transparent; 53 | padding-left: 24px; 54 | padding-right: 24px; 55 | height: 50px; 56 | width: 100%; 57 | font-size: 18px; 58 | border: 1px solid #121219; 59 | border-radius: 5px; 60 | font-family: 'Poppins'; 61 | margin-bottom: 15px; 62 | color: #FFFFFF; 63 | } 64 | 65 | 66 | .options { 67 | background-color: #121219; 68 | color: #fff; 69 | } 70 | 71 | .alignDiv { 72 | width: 100%; 73 | height: 100%; 74 | display: flex; 75 | flex-direction: row; 76 | justify-content: center; 77 | align-items: center; 78 | cursor: pointer; 79 | } 80 | -------------------------------------------------------------------------------- /components/Icons.js: -------------------------------------------------------------------------------- 1 | import styles from "../styles/Icons.module.css"; 2 | 3 | // Imports all the icons in feather icons 4 | import * as FeatherIcons from "react-icons/fi"; 5 | 6 | // Icons 7 | import { FaReact } from "react-icons/fa"; 8 | import { MdClose } from "react-icons/md"; 9 | 10 | // Other Libs 11 | import toast from "react-hot-toast"; 12 | 13 | const Icons = ({ setIconsOpen, setName }) => { 14 | const icons = Object.keys(FeatherIcons); 15 | const values = Object.values(FeatherIcons); 16 | 17 | const addIconHandler = (e) => { 18 | // Gets the name of the icon that was passed to the attribute name while rendering the icon 19 | toast.success("Icon selected!"); 20 | const name = e.currentTarget.attributes.name.value; 21 | setName(name); 22 | setIconsOpen(false); 23 | }; 24 | 25 | return ( 26 |
setIconsOpen(false)} */> 27 |
28 |
setIconsOpen(false)}> 29 | 30 | Close 31 |
32 |
33 | 34 | Powered by React Icons 35 |
36 |
37 |
38 | {/* It maps over each function in the react-icons package and executes it */} 39 | {values.map((value, index) => { 40 | return ( 41 |
47 | {value()} 48 |
49 | ); 50 | })} 51 |
52 |
53 | ); 54 | }; 55 | 56 | export default Icons; 57 | -------------------------------------------------------------------------------- /amplify/cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": { 3 | "graphqltransformer": { 4 | "addmissingownerfields": true, 5 | "improvepluralization": false, 6 | "validatetypenamereservedwords": true, 7 | "useexperimentalpipelinedtransformer": true, 8 | "enableiterativegsiupdates": true, 9 | "secondarykeyasgsi": true, 10 | "skipoverridemutationinputtypes": true, 11 | "transformerversion": 2, 12 | "suppressschemamigrationprompt": true, 13 | "securityenhancementnotification": false, 14 | "showfieldauthnotification": false, 15 | "usesubusernamefordefaultidentityclaim": true, 16 | "usefieldnameforprimarykeyconnectionfield": false, 17 | "enableautoindexquerynames": false, 18 | "respectprimarykeyattributesonconnectionfield": false, 19 | "shoulddeepmergedirectiveconfigdefaults": false, 20 | "populateownerfieldforstaticgroupauth": false 21 | }, 22 | "frontend-ios": { 23 | "enablexcodeintegration": true 24 | }, 25 | "auth": { 26 | "enablecaseinsensitivity": true, 27 | "useinclusiveterminology": true, 28 | "breakcirculardependency": true, 29 | "forcealiasattributes": false, 30 | "useenabledmfas": true 31 | }, 32 | "codegen": { 33 | "useappsyncmodelgenplugin": true, 34 | "usedocsgeneratorplugin": true, 35 | "usetypesgeneratorplugin": true, 36 | "cleangeneratedmodelsdirectory": true, 37 | "retaincasestyle": true, 38 | "addtimestampfields": true, 39 | "handlelistnullabilitytransparently": true, 40 | "emitauthprovider": true, 41 | "generateindexrules": true, 42 | "enabledartnullsafety": true 43 | }, 44 | "appsync": { 45 | "generategraphqlpermissions": true 46 | }, 47 | "latestregionsupport": { 48 | "pinpoint": 1, 49 | "translate": 1, 50 | "transcribe": 1, 51 | "rekognition": 1, 52 | "textract": 1, 53 | "comprehend": 1 54 | }, 55 | "project": { 56 | "overrides": true 57 | } 58 | }, 59 | "debug": { 60 | "shareProjectConfig": false 61 | } 62 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | 6 | 7 | 8 | 9 |

10 |

11 | 12 | # About 13 | 14 | It is made to simplify making custom designs. It has in-built features which allow users to access beatiful designs assets from a single web application. 15 | 16 | 🗞 Read more about it [here](https://keshraf.hashnode.dev/sigma-real-time-collaboration-design-tool) 17 | 18 | ![Preview With Background](https://user-images.githubusercontent.com/82109991/193294707-e6cf3234-dae3-4453-a388-d35e39eb1351.png) 19 | 20 | # Features 21 | 22 | 🚀 Launched: 23 | 24 | - Smooth Drag & Drop of elements 25 | - Responsive Resizing of elements 26 | - Ability to add text, images, background, icons & shapes 27 | - Ability to upload images from local device 28 | - Access to Images from Unsplash 29 | - Access to Icons from Feather Icons 30 | - Ability to add collaborators using room code 31 | - Real-time Collaboration 32 | - Can add Multiple Pages to their design 33 | - Can download pages as images 34 | - Synchronised Element Updates (like color, etc.) 35 | - Authentication 36 | - Can Save Projects 37 | 38 | 🛠 In Progress: 39 | 40 | - Deleting Pages 41 | - Adding Patterns 42 | 43 | ✨ Upcoming 44 | 45 | - Add Shadows 46 | - Add Borders 47 | - Add Gradients 48 | - Add More Icons 49 | - Authentication 50 | - Google Maps Integration to add Static Maps to your design 51 | 52 | # Tech Stack 53 | 54 | - NextJS 55 | - AWS Amplify 56 | - Firebase 57 | - Redux Toolkit 58 | - use-gesture 59 | - React Spring 60 | 61 | # License 62 | 63 | MIT 64 | -------------------------------------------------------------------------------- /store/backgroundSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = []; 4 | 5 | export const backgroundSlice = createSlice({ 6 | name: "background", 7 | initialState, 8 | reducers: { 9 | addBackground(state, action) { 10 | const newState = { 11 | id: action.payload.id, 12 | page: action.payload.page, 13 | background: action.payload.background, 14 | type: action.payload.type, 15 | source: action.payload.source, 16 | }; 17 | 18 | const removedArr = state.filter( 19 | (background) => background.page !== action.payload.page 20 | ); 21 | removedArr.push(newState); 22 | return removedArr; 23 | }, 24 | addBackgroundUnsplash(state, action) { 25 | const newState = { 26 | id: action.payload.id, 27 | page: action.payload.page, 28 | background: action.payload.background, 29 | type: "background", 30 | source: "unsplash", 31 | }; 32 | 33 | const removedArr = state.filter( 34 | (background) => background.page !== action.payload.page 35 | ); 36 | removedArr.push(newState); 37 | return removedArr; 38 | }, 39 | addBackgroundFirebase(state, action) { 40 | const newState = { 41 | id: action.payload.id, 42 | page: action.payload.page, 43 | background: action.payload.background, 44 | type: "background", 45 | source: "firebase", 46 | }; 47 | 48 | const removedArr = state.filter( 49 | (background) => background.page !== action.payload.page 50 | ); 51 | removedArr.push(newState); 52 | return removedArr; 53 | }, 54 | addBackgroundColor(state, action) { 55 | const newState = { 56 | id: action.payload.id, 57 | page: action.payload.page, 58 | background: action.payload.background, 59 | type: "background", 60 | source: "color", 61 | }; 62 | 63 | const removedArr = state.filter( 64 | (background) => background.page !== action.payload.page 65 | ); 66 | removedArr.push(newState); 67 | return removedArr; 68 | }, 69 | resetBackground(state) { 70 | return []; 71 | }, 72 | }, 73 | }); 74 | 75 | export const { 76 | addBackgroundColor, 77 | addBackgroundFirebase, 78 | addBackgroundUnsplash, 79 | addBackground, 80 | resetBackground, 81 | } = backgroundSlice.actions; 82 | export default backgroundSlice.reducer; 83 | -------------------------------------------------------------------------------- /styles/Icons.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: absolute; 3 | top: -50%; 4 | bottom: 0; 5 | right: 0; 6 | left: -50%; 7 | width: 200%; 8 | height: 200%; 9 | box-shadow: 0 0 2000px 2000px rgb(0 0 0 / 30%); 10 | z-index: 20000; 11 | background-color: rgba(0, 0, 0, 0.4); 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: center; 15 | align-items: center; 16 | backdrop-filter: blur(100px); 17 | overflow: hidden !important; 18 | animation: blury 1s ease-out; 19 | } 20 | 21 | @keyframes blury { 22 | 0% { backdrop-filter: blur(1px); } 23 | 100% { backdrop-filter: blur(100px); } 24 | } 25 | 26 | .innerContainer { 27 | width: 800px; 28 | height: 70vh; 29 | background-color: transparent; 30 | border: 1px solid #20222D; 31 | border-radius: 5px; 32 | padding: 15px; 33 | overflow-y: scroll; 34 | display: flex; 35 | flex-direction: row; 36 | justify-content: space-between; 37 | align-items: center; 38 | flex-wrap: wrap; 39 | gap: 10px; 40 | 41 | 42 | animation: appear 1s ease-out; 43 | } 44 | 45 | @keyframes appear { 46 | 0% { height: 0vh; } 47 | 100% { height: 70vh; } 48 | } 49 | 50 | 51 | .icon { 52 | font-size: 24px; 53 | color: #ffffff; 54 | border: 1px solid #20222D; 55 | height: 55px; 56 | width: 55px; 57 | display: flex; 58 | justify-content: center; 59 | align-items: center; 60 | border-radius: 5px; 61 | transition: ease-out 0.1s; 62 | } 63 | 64 | .icon:hover{ 65 | background-color: #20222D; 66 | font-size: 26px; 67 | cursor: pointer; 68 | } 69 | 70 | .actionContainer { 71 | width: 800px; 72 | margin-bottom: 15px; 73 | display: flex; 74 | flex-direction: row; 75 | justify-content: flex-start; 76 | gap: 15px; 77 | align-items: center; 78 | height: 25px; 79 | } 80 | 81 | .actionIcon { 82 | color: #505765; 83 | font-size: 14px; 84 | border-radius: 5px; 85 | background-color: #20222D; 86 | height: 28px; 87 | width: auto; 88 | padding: 2px; 89 | cursor: pointer; 90 | display: flex; 91 | flex-direction: row; 92 | padding-left: 10px; 93 | padding-right: 10px; 94 | justify-content: flex-start; 95 | align-items: center; 96 | gap: 5px; 97 | } 98 | 99 | .actionIcon:hover{ 100 | background-color: #3B3D41; 101 | color: #fff; 102 | } -------------------------------------------------------------------------------- /styles/BackgroundForm.module.css: -------------------------------------------------------------------------------- 1 | .clickButtonShaded { 2 | width: 100%; 3 | height: 50px; 4 | background-color: #303544; 5 | display: flex; 6 | flex-direction: row; 7 | justify-content: flex-start; 8 | align-items: center; 9 | border: none; 10 | border-radius: 5px; 11 | font-family: 'Poppins'; 12 | font-size: 18px; 13 | gap:5px; 14 | padding-left: 24px; 15 | } 16 | .clickButtonShaded:hover{ 17 | background-color: #171720; 18 | } 19 | .clickButtonUnshaded { 20 | width: 100%; 21 | height: 50px; 22 | background-color: transparent; 23 | display: flex; 24 | flex-direction: row; 25 | justify-content: flex-start; 26 | align-items: center; 27 | border: 1px solid #121219; 28 | border-radius: 5px; 29 | font-family: 'Poppins'; 30 | font-size: 18px; 31 | gap:15px; 32 | padding-left: 24px; 33 | } 34 | .clickButtonUnshaded:hover{ 35 | background-color: #171720; 36 | } 37 | 38 | .buttonLabel { 39 | display: flex; 40 | flex-direction: row; 41 | justify-content: center; 42 | align-items: center; 43 | gap:15px 44 | } 45 | 46 | .bgLayout { 47 | display: flex; 48 | flex-direction: column; 49 | gap: 20px; 50 | } 51 | 52 | .submitButton { 53 | background-color: #2599FF; 54 | display: flex; 55 | flex-direction: row; 56 | justify-content: center; 57 | margin-top: 20px; 58 | align-items: center; 59 | height: 50px; 60 | width: 300px; 61 | font-size: 18px; 62 | border: none; 63 | border-radius: 5px; 64 | font-family: 'Poppins'; 65 | color: #FFFFFF; 66 | } 67 | 68 | .alignDiv { 69 | width: 100%; 70 | height: 100%; 71 | display: flex; 72 | flex-direction: row; 73 | justify-content: center; 74 | align-items: center; 75 | cursor: pointer; 76 | } 77 | 78 | .colorLabel { 79 | width: 24px; 80 | height: 24px; 81 | border: 1px solid #121219; 82 | cursor: pointer; 83 | } 84 | 85 | .colorInputText { 86 | background-color: transparent; 87 | font-size: 16px; 88 | color: #fff; 89 | width: 75px; 90 | border: none; 91 | outline: none; 92 | } 93 | 94 | .alignBox { 95 | width: 100%; 96 | display: flex; 97 | flex-direction: row; 98 | justify-content: flex-start; 99 | align-items: center; 100 | border: 1px solid #121219; 101 | border-radius: 5px; 102 | height: 50px; 103 | padding-left: 24px; 104 | gap: 15px; 105 | } -------------------------------------------------------------------------------- /styles/ItemResizer.module.css: -------------------------------------------------------------------------------- 1 | .outerContainer { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | width: 100%; 7 | height: 100vh; 8 | } 9 | 10 | .innerContainer { 11 | border: 1px solid black; 12 | height: 500px; 13 | width: 700px; 14 | position: relative; 15 | box-sizing: border-box; 16 | } 17 | 18 | .item { 19 | border: 1px solid #0097df; 20 | position: absolute; 21 | top: 0px; 22 | left: 0px; 23 | width: 100px; 24 | height: 100px; 25 | z-index: 100; 26 | touch-action: none; 27 | cursor: move; 28 | display: flex; 29 | flex-direction: column; 30 | justify-content: center; 31 | align-items: center; 32 | /* user-select: none; */ 33 | /* overflow: hidden; */ 34 | } 35 | 36 | 37 | .unselectedItem { 38 | border: 1px solid transparent; 39 | position: absolute; 40 | top: 0px; 41 | left: 0px; 42 | width: 100px; 43 | height: 100px; 44 | z-index: 100; 45 | touch-action: none; 46 | cursor: move; 47 | display: flex; 48 | flex-direction: column; 49 | justify-content: center; 50 | align-items: center; 51 | /* user-select: none; */ 52 | /* overflow: hidden; */ 53 | } 54 | 55 | 56 | 57 | .resizer { 58 | position: absolute; 59 | bottom: -4px; 60 | right: -4px; 61 | width: 10px; 62 | height: 10px; 63 | background-color: #fff; 64 | border: 1px solid #0097df; 65 | cursor: nwse-resize; 66 | touch-action: none; 67 | } 68 | 69 | .resizer2 { 70 | position: absolute; 71 | top: -4px; 72 | right: -4px; 73 | width: 10px; 74 | height: 10px; 75 | background-color: #0097df; 76 | cursor: nesw-resize; 77 | touch-action: none; 78 | } 79 | 80 | .resizer3 { 81 | position: absolute; 82 | left: -4px; 83 | bottom: -4px; 84 | width: 10px; 85 | height: 10px; 86 | background-color: #0097df; 87 | cursor: nesw-resize; 88 | touch-action: none; 89 | } 90 | .resizer4 { 91 | position: absolute; 92 | top: -4px; 93 | left: -4px; 94 | width: 10px; 95 | height: 10px; 96 | background-color: #0097df; 97 | cursor: nwse-resize; 98 | touch-action: none; 99 | } 100 | 101 | .square { 102 | width: 100%; 103 | height: 100%; 104 | } 105 | 106 | .circle { 107 | height: 100%; 108 | width: 100%; 109 | border-radius: 50%; 110 | } 111 | 112 | .line { 113 | height: 2px; 114 | width: 100%; 115 | 116 | } 117 | 118 | .triangle { 119 | clip-path: polygon(50% 0, 100% 100%, 0 100%); 120 | width: 100%; 121 | height: 100%; 122 | } -------------------------------------------------------------------------------- /styles/Auth.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | height: 100vh; 4 | display: flex; 5 | flex-direction: row; 6 | justify-content: center; 7 | align-items: center; 8 | position: relative; 9 | background-color: #121219; 10 | } 11 | 12 | .form{ 13 | width: 400px; 14 | height: auto; 15 | padding: 50px 60px 50px 60px; 16 | background-color: #171720; 17 | display: flex; 18 | flex-direction: column; 19 | align-items: flex-start; 20 | gap: 15px; 21 | border-radius: 10px; 22 | z-index: 1; 23 | } 24 | 25 | .heading{ 26 | font-family: 'Poppins'; 27 | font-weight: 700; 28 | font-size: 24px; 29 | color: #FFF; 30 | margin: 0px; 31 | } 32 | 33 | .divider{ 34 | width: 100%; 35 | height: 0px; 36 | border: 0.5px dashed rgba(255,255,255,0.3); 37 | } 38 | 39 | .input{ 40 | width: 100%; 41 | height: 50px; 42 | border: 1px solid #374860; 43 | border-radius: 5px; 44 | padding-left: 15px; 45 | background-color: transparent; 46 | color: #fff; 47 | font-family: 'Poppins'; 48 | font-size: 16px; 49 | } 50 | 51 | .input::placeholder{ 52 | color: #303544; 53 | } 54 | 55 | .submit{ 56 | width: 100%; 57 | height: 50px; 58 | background: #1DAACC; 59 | border-radius: 5px; 60 | font-family: 'Poppins'; 61 | font-weight: 500; 62 | font-size: 18px; 63 | text-align: center; 64 | color: #FFFFFF; 65 | border: none; 66 | user-select: none; 67 | cursor: pointer; 68 | display: flex; 69 | flex-direction: row; 70 | justify-content: center; 71 | align-items: center; 72 | } 73 | 74 | .switch{ 75 | margin: auto; 76 | background-color: transparent; 77 | border: none; 78 | color: #303544; 79 | font-size: 16px; 80 | font-family: "Poppins"; 81 | cursor: pointer; 82 | } 83 | 84 | .switch:hover{ 85 | text-decoration: underline #1DAACC; 86 | } 87 | 88 | .rule{ 89 | font-family: 'Poppins'; 90 | margin: 0; 91 | font-size: 10px; 92 | color: #1da9cc85; 93 | } 94 | 95 | .blur{ 96 | width: 2500px; 97 | height: 2500px; 98 | background: linear-gradient(272.79deg, rgba(0, 0, 0, 0.61) 3.54%, rgba(48, 53, 68, 0.61) 81.55%), linear-gradient(153.79deg, #1DAACC 0.41%, #20222D 50.2%); 99 | filter: blur(45px); 100 | position: absolute; 101 | z-index: 0; 102 | animation-name: rotation; 103 | animation-iteration-count: infinite; 104 | animation-duration: 10s; 105 | animation-timing-function: linear; 106 | } 107 | 108 | @keyframes rotation { 109 | 0% { 110 | transform: rotate(0deg); 111 | } 112 | 100% { 113 | transform: rotate(360deg); 114 | } 115 | 116 | } -------------------------------------------------------------------------------- /styles/ShapesForm.module.css: -------------------------------------------------------------------------------- 1 | .formLayout { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100%; 5 | height: auto; 6 | } 7 | 8 | .shapesLayout { 9 | display: flex; 10 | flex-direction: row; 11 | flex-wrap: wrap; 12 | gap: 10px; 13 | justify-content: space-between; 14 | margin-bottom: 15px; 15 | } 16 | 17 | .shapesContainer { 18 | width: 140px; 19 | height: 100px; 20 | border: 1px solid #171720; 21 | border-radius: 5px; 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | } 26 | 27 | .square { 28 | width: 50px; 29 | height: 50px; 30 | border-color: #303544; 31 | } 32 | 33 | .circle { 34 | height: 50px; 35 | width: 50px; 36 | background-color: #303544; 37 | border-radius: 50%; 38 | } 39 | 40 | .line { 41 | height: 2px; 42 | width: 75px; 43 | background-color: #303544; 44 | transform: rotate(45deg); 45 | 46 | } 47 | 48 | .triangle { 49 | background-color: #303544; 50 | clip-path: polygon(50% 0, 100% 100%, 0 100%); 51 | width: 50px; 52 | height: 50px; 53 | } 54 | 55 | .submitButton { 56 | background-color: #2599FF; 57 | display: flex; 58 | flex-direction: row; 59 | justify-content: center; 60 | margin-top: 20px; 61 | align-items: center; 62 | height: 50px; 63 | width: 300px; 64 | font-size: 18px; 65 | border: none; 66 | border-radius: 5px; 67 | font-family: 'Poppins'; 68 | color: #fff; 69 | } 70 | 71 | .clickButtonUnshaded { 72 | width: 100%; 73 | height: 50px; 74 | background-color: transparent; 75 | display: flex; 76 | flex-direction: row; 77 | justify-content: flex-start; 78 | align-items: center; 79 | border: 1px solid #121219; 80 | border-radius: 5px; 81 | font-family: 'Poppins'; 82 | font-size: 18px; 83 | gap:5px; 84 | padding-left: 24px; 85 | margin-top:10px; 86 | } 87 | 88 | .buttonLabel { 89 | display: flex; 90 | flex-direction: row; 91 | justify-content: center; 92 | align-items: center; 93 | gap:15px; 94 | } 95 | 96 | .alignBox { 97 | width: 100%; 98 | display: flex; 99 | flex-direction: row; 100 | justify-content: flex-start; 101 | align-items: center; 102 | border: 1px solid #121219; 103 | border-radius: 5px; 104 | height: 50px; 105 | padding-left: 24px; 106 | gap: 5px; 107 | margin-top: 15px; 108 | } 109 | 110 | .colorLabel { 111 | width: 24px; 112 | height: 24px; 113 | border: 1px solid #121219; 114 | cursor: pointer; 115 | } 116 | 117 | .colorInputText { 118 | background-color: transparent; 119 | font-size: 16px; 120 | color: #fff; 121 | width: 75px; 122 | border: none; 123 | outline: none; 124 | } -------------------------------------------------------------------------------- /styles/Unsplash.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: absolute; 3 | top: -50%; 4 | bottom: 0; 5 | right: 0; 6 | left: -50%; 7 | width: 200%; 8 | height: 200%; 9 | box-shadow: 0 0 2000px 2000px rgb(0 0 0 / 30%); 10 | z-index: 20000; 11 | background-color: rgba(0, 0, 0, 0.4); 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: center; 15 | align-items: center; 16 | backdrop-filter: blur(100px); 17 | overflow: hidden !important; 18 | animation: blury 1s ease-out; 19 | } 20 | 21 | @keyframes blury { 22 | 0% { backdrop-filter: blur(1px); } 23 | 100% { backdrop-filter: blur(100px); } 24 | } 25 | 26 | 27 | 28 | .innerContainer { 29 | width: 800px; 30 | height: 70vh; 31 | background-color: transparent; 32 | border: 1px solid #20222D; 33 | border-radius: 5px; 34 | padding: 15px; 35 | overflow-y: scroll; 36 | grid-column-gap: 15px; 37 | align-items: flex-start; 38 | display: grid; 39 | grid-template-columns: repeat(3,minmax(0,1fr)); 40 | scroll-behavior: smooth; 41 | animation: appear 1s ease-out; 42 | } 43 | 44 | @keyframes appear { 45 | 0% { height: 0vh; } 46 | 100% { height: 70vh; } 47 | } 48 | 49 | .photo { 50 | max-height: 500px; 51 | max-width: 250px; 52 | border-radius: 5px; 53 | position: relative; 54 | transition: all 0.3s; 55 | cursor:pointer; 56 | } 57 | 58 | .photo:hover{ 59 | filter: grayscale(100%); 60 | } 61 | .column { 62 | display: grid; 63 | grid-template-columns: minmax(0,1fr); 64 | row-gap: 6px; 65 | } 66 | 67 | .inputContainer { 68 | position: relative; 69 | } 70 | 71 | .input { 72 | width: 800px; 73 | height: 50px; 74 | display: flex; 75 | align-items: center; 76 | padding-left: 15px; 77 | font-family: 'Poppins'; 78 | font-size: 18px; 79 | margin-bottom: 15px; 80 | background-color: transparent; 81 | border: 1px solid #20222D; 82 | border-radius: 5px; 83 | background-color: none; 84 | } 85 | 86 | .hint { 87 | position: absolute; 88 | top: -2px; 89 | right: 10px; 90 | color: #3B3D41; 91 | font-family: 'Poppins'; 92 | display: flex; 93 | flex-direction: row; 94 | gap: 5px; 95 | justify-content: center; 96 | align-items: center; 97 | } 98 | 99 | .actionContainer { 100 | width: 800px; 101 | margin-bottom: 15px; 102 | display: flex; 103 | flex-direction: row; 104 | justify-content: flex-start; 105 | gap: 15px; 106 | align-items: center; 107 | height: 25px; 108 | } 109 | 110 | .icon { 111 | color: #505765; 112 | font-size: 14px; 113 | border-radius: 5px; 114 | background-color: #20222D; 115 | height: 28px; 116 | width: auto; 117 | padding: 2px; 118 | cursor: pointer; 119 | display: flex; 120 | flex-direction: row; 121 | padding-left: 10px; 122 | padding-right: 10px; 123 | justify-content: flex-start; 124 | align-items: center; 125 | gap: 5px; 126 | } 127 | 128 | .icon:hover{ 129 | background-color: #3B3D41; 130 | color: #fff; 131 | } -------------------------------------------------------------------------------- /components/ImageForm.js: -------------------------------------------------------------------------------- 1 | import styles from "../styles/ImageForm.module.css"; 2 | 3 | // Icons 4 | import { FiUploadCloud } from "react-icons/fi"; 5 | import { RiUnsplashFill } from "react-icons/ri"; 6 | 7 | // Other Libs 8 | import { nanoid } from "nanoid"; 9 | import { toast } from "react-hot-toast"; 10 | 11 | // Redux 12 | import { addElement } from "../store/elementSlice"; 13 | import { useDispatch, useSelector } from "react-redux"; 14 | 15 | // Firebase 16 | import { push, set, ref as refDatabase } from "firebase/database"; 17 | import { database } from "../firebaseConfig"; 18 | import { getDownloadURL, ref, uploadBytes } from "firebase/storage"; 19 | import { storage } from "../firebaseConfig"; 20 | 21 | const ImageForm = ({ setUnsplashOpen }) => { 22 | const dispatch = useDispatch(); 23 | 24 | const page = useSelector((state) => state.page.current); // Gets the current page number 25 | const roomId = useSelector((state) => state.room.id); // Gets the current roomId 26 | 27 | const uploadHandler = (e) => { 28 | const file = e.target.files[0]; 29 | console.log(file); 30 | 31 | // Creates a image location in the firebase storage 32 | const imageRef = ref(storage, `images/image_${nanoid()}.jpg`); 33 | 34 | // Uploads the file to the above location 35 | const uploadingImage = uploadBytes(imageRef, file).then((snapshot) => { 36 | console.log(snapshot, "Snapshot"); 37 | 38 | // Fetches the URL of the uploaded Image 39 | const loadingImage = getDownloadURL(imageRef).then((url) => { 40 | const data = { 41 | page, 42 | src: url, 43 | width: 200, 44 | height: 100, 45 | x: 15, 46 | y: 15, 47 | type: "image", 48 | id: nanoid(), 49 | roomId, 50 | }; 51 | dispatch(addElement(data)); 52 | const elementRef = refDatabase(database, "elements/" + roomId); 53 | set(push(elementRef), data); 54 | }); 55 | 56 | toast.promise(loadingImage, { 57 | loading: "Loading Image", 58 | success: "Image Loaded", 59 | error: "Error when Loading", 60 | }); 61 | }); 62 | 63 | toast.promise(uploadingImage, { 64 | loading: "Uploading Image", 65 | success: "Image Uploaded", 66 | error: "Error when Uploading", 67 | }); 68 | }; 69 | 70 | return ( 71 |
72 |
73 | 81 | 88 |
89 |
setUnsplashOpen(true)} 93 | > 94 | 95 | Launch Unsplash 96 |
97 | 98 |
99 | ); 100 | }; 101 | 102 | export default ImageForm; 103 | -------------------------------------------------------------------------------- /components/ShapesForm.js: -------------------------------------------------------------------------------- 1 | import styles from "../styles/ShapesForm.module.css"; 2 | 3 | // Other libs 4 | import { nanoid } from "nanoid"; 5 | import toast from "react-hot-toast"; 6 | 7 | // React & Redux 8 | import { useState, useEffect } from "react"; 9 | import { useDispatch, useSelector } from "react-redux"; 10 | import { addElement, updateShapeElement } from "../store/elementSlice"; 11 | 12 | // Firebase 13 | import { push, set, ref } from "firebase/database"; 14 | import { database } from "../firebaseConfig"; 15 | 16 | // Custom Hook 17 | import useElementUpdate from "../hooks/useElementUpdate"; 18 | 19 | // Components 20 | import ColorPicker from "./ColorPicker"; 21 | 22 | const ShapesForm = () => { 23 | const dispatch = useDispatch(); 24 | const elementUpdater = useElementUpdate(); 25 | 26 | const [shapeSelected, setShapeSelected] = useState(null); 27 | const [color, setColor] = useState("#FFFFFF"); 28 | 29 | const page = useSelector((state) => state.page.current); // gets the current page 30 | const roomId = useSelector((state) => state.room.id); // gets the current room id 31 | const selected = useSelector((state) => state.selectedElement); // Gets the selected Element 32 | 33 | useEffect(() => { 34 | if (isShape) { 35 | setColor(selected.color); 36 | } else { 37 | setColor("#FFFFFF"); 38 | } 39 | }, [isShape, selected.color]); 40 | 41 | const submitHandler = () => { 42 | console.log("Entered", selected); 43 | if (isShape) { 44 | const data = { 45 | color, 46 | id: selected.id, 47 | }; 48 | dispatch(updateShapeElement(data)); 49 | elementUpdater(data); 50 | toast.success(`${shapeSelected} color changed!`); 51 | } else { 52 | if (shapeSelected === null) { 53 | return; 54 | } 55 | const data = { 56 | id: nanoid(), 57 | x: 15, 58 | y: 15, 59 | width: 100, 60 | height: 100, 61 | type: "shape", 62 | shape: shapeSelected, 63 | color, 64 | page, 65 | roomId, 66 | }; 67 | dispatch(addElement(data)); 68 | const elementRef = ref(database, "elements/" + roomId); 69 | set(push(elementRef), data); 70 | toast.success(`${shapeSelected} added!`); 71 | } 72 | }; 73 | 74 | // Different types of shapes 75 | const shapes = ["square", "circle", "line", "triangle"]; 76 | const isShape = selected.type === "shape"; 77 | 78 | return ( 79 |
80 | {!isShape && ( 81 |
82 | {shapes.map((shape) => { 83 | return ( 84 |
setShapeSelected(shape)} 87 | style={{ 88 | borderColor: shapeSelected === shape ? "#2599FF" : "#171720", 89 | }} 90 | key={shape} 91 | > 92 |
96 |
97 | ); 98 | })} 99 |
100 | )} 101 | 102 | 107 |
108 | ); 109 | }; 110 | 111 | export default ShapesForm; 112 | -------------------------------------------------------------------------------- /store/elementSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = []; 4 | 5 | export const elementSlice = createSlice({ 6 | name: "elements", 7 | initialState, 8 | reducers: { 9 | addElement(state, action) { 10 | let same = false; 11 | console.log("ID", action.payload.id); 12 | if (!action.payload.id || action.payload.id === undefined) { 13 | return; 14 | } 15 | state.forEach((el) => { 16 | if (el.id === action.payload.id) { 17 | console.log("SAME ID!"); 18 | same = true; 19 | } 20 | }); 21 | if (same) { 22 | return; 23 | } else { 24 | console.log("still pushed!"); 25 | state.push(action.payload); 26 | } 27 | }, 28 | removeElement(state, action) { 29 | const newState = state.filter( 30 | (element) => element.id !== action.payload.id 31 | ); 32 | return newState; 33 | }, 34 | updateElement(state, action) { 35 | const newState = state.map((element) => { 36 | if (element.id === action.payload.id) { 37 | if (element.type === "image") { 38 | return { 39 | ...element, 40 | width: action.payload.width, 41 | height: action.payload.height, 42 | x: action.payload.x, 43 | y: action.payload.y, 44 | loaded: true, 45 | }; 46 | } else { 47 | return { 48 | ...element, 49 | width: action.payload.width, 50 | height: action.payload.height, 51 | x: action.payload.x, 52 | y: action.payload.y, 53 | }; 54 | } 55 | } else { 56 | return element; 57 | } 58 | }); 59 | return newState; 60 | }, 61 | updateTextElement(state, action) { 62 | const newState = state.map((element) => { 63 | if (element.id === action.payload.id) { 64 | return { 65 | ...element, 66 | content: action.payload.content, 67 | color: action.payload.color, 68 | size: action.payload.size, 69 | font: action.payload.font, 70 | weight: action.payload.weight, 71 | align: action.payload.align, 72 | }; 73 | } else { 74 | return element; 75 | } 76 | }); 77 | 78 | return newState; 79 | }, 80 | updateShapeElement(state, action) { 81 | const newState = state.map((element) => { 82 | if (element.id === action.payload.id) { 83 | return { 84 | ...element, 85 | color: action.payload.color, 86 | }; 87 | } else { 88 | return element; 89 | } 90 | }); 91 | 92 | return newState; 93 | }, 94 | updateIconElement(state, action) { 95 | const newState = state.map((element) => { 96 | if (element.id === action.payload.id) { 97 | return { 98 | ...element, 99 | color: action.payload.color, 100 | size: action.payload.size, 101 | }; 102 | } else { 103 | return element; 104 | } 105 | }); 106 | 107 | return newState; 108 | }, 109 | resetElement(state) { 110 | return []; 111 | }, 112 | }, 113 | }); 114 | 115 | export const { 116 | addElement, 117 | removeElement, 118 | updateElement, 119 | updateTextElement, 120 | updateShapeElement, 121 | updateIconElement, 122 | resetElement, 123 | } = elementSlice.actions; 124 | 125 | export default elementSlice.reducer; 126 | -------------------------------------------------------------------------------- /components/IconForm.js: -------------------------------------------------------------------------------- 1 | import styles from "../styles/IconForm.module.css"; 2 | 3 | // Icons 4 | import { FiFeather } from "react-icons/fi"; 5 | 6 | // React 7 | import { useState } from "react"; 8 | 9 | // Redux 10 | import { useDispatch, useSelector } from "react-redux"; 11 | import { addElement, updateIconElement } from "../store/elementSlice"; 12 | 13 | // Other libs 14 | import { nanoid } from "nanoid"; 15 | import { toast } from "react-hot-toast"; 16 | 17 | // Components 18 | import ColorPicker from "./ColorPicker"; 19 | import Icons from "./Icons"; 20 | 21 | // Firebase 22 | import { push, set, ref } from "firebase/database"; 23 | import { database } from "../firebaseConfig"; 24 | 25 | // Custom Hook 26 | import useElementUpdate from "../hooks/useElementUpdate"; 27 | import { useEffect } from "react"; 28 | 29 | const IconForm = () => { 30 | const dispatch = useDispatch(); 31 | const elementUpdater = useElementUpdate(); 32 | 33 | const page = useSelector((state) => state.page.current); // Gets the current page 34 | const roomId = useSelector((state) => state.room.id); // Gets the current room id 35 | const selected = useSelector((state) => state.selectedElement); // Gets the selected Element 36 | 37 | const [iconsOpen, setIconsOpen] = useState(false); // Icons Modal State 38 | const [color, setColor] = useState("#FFFFFF"); 39 | const [size, setSize] = useState(16); 40 | const [name, setName] = useState(null); 41 | 42 | useEffect(() => { 43 | if (selected.type === "icon") { 44 | setColor(selected.color); 45 | setSize(selected.size); 46 | } 47 | }, [selected]); 48 | 49 | const submitHandler = (e) => { 50 | e.preventDefault(); 51 | if (!name && selected.type !== "icon") { 52 | toast.error("Choose an icon!"); 53 | return; 54 | } 55 | if (selected.type === "icon") { 56 | // Updates the selected icon 57 | const data = { 58 | id: selected.id, 59 | size, 60 | color, 61 | }; 62 | dispatch(updateIconElement(data)); 63 | elementUpdater(data); 64 | toast.success("Icon Updated!"); 65 | } else { 66 | // Adds a new icon 67 | const data = { 68 | page, 69 | id: nanoid(), 70 | width: 55, 71 | height: 55, 72 | x: 15, 73 | y: 15, 74 | type: "icon", 75 | name, 76 | set: "feather", 77 | size, 78 | color, 79 | roomId, 80 | }; 81 | dispatch(addElement(data)); 82 | const elementRef = ref(database, "elements/" + roomId); 83 | set(push(elementRef), data); 84 | setIconsOpen(false); 85 | toast.success("Icon Added!"); 86 | } 87 | }; 88 | 89 | return ( 90 | <> 91 | {iconsOpen && } 92 |
93 | {selected.type !== "icon" && ( 94 |
setIconsOpen(true)} 98 | > 99 | 100 | Launch Feather Icons 101 |
102 | )} 103 |
104 | setSize(e.target.value)} 111 | /> 112 | 113 |
114 | 117 |
118 | 119 | ); 120 | }; 121 | 122 | export default IconForm; 123 | -------------------------------------------------------------------------------- /styles/Files.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | height: 100vh; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | position: relative; 9 | background-color: #121219; 10 | gap: 25px; 11 | overflow: hidden; 12 | } 13 | 14 | .innerContainer{ 15 | width: 750px; 16 | height: auto; 17 | max-height: 600px; 18 | padding: 50px 60px 50px 60px; 19 | background-color: #171720; 20 | display: flex; 21 | flex-direction: column; 22 | align-items: flex-start; 23 | gap: 15px; 24 | border-radius: 15px; 25 | z-index: 1; 26 | } 27 | 28 | .heading{ 29 | font-family: 'Inter'; 30 | font-weight: 900; 31 | font-size: 42px; 32 | color: #FFF; 33 | margin: 0px; 34 | z-index: 1; 35 | } 36 | .header{ 37 | width: 725px; 38 | height: auto; 39 | display: flex; 40 | flex-direction: row; 41 | justify-content: space-between; 42 | align-items: center; 43 | margin: 0px; 44 | z-index: 1; 45 | } 46 | 47 | .search { 48 | width: 100%; 49 | height: 50px; 50 | display: flex; 51 | flex-direction: row; 52 | justify-content: space-between; 53 | align-items: center; 54 | gap: 15px; 55 | } 56 | 57 | .searchBar{ 58 | width: auto; 59 | height: 50px; 60 | border: 1px solid #374860; 61 | border-radius: 5px; 62 | padding-left: 15px; 63 | background-color: transparent; 64 | color: #fff; 65 | font-family: 'Poppins'; 66 | font-size: 16px; 67 | flex: 1; 68 | } 69 | 70 | .searchBar::placeholder{ 71 | color: #303544; 72 | } 73 | 74 | .logout{ 75 | width: 60px; 76 | height: 60px; 77 | border-radius: 100px; 78 | background-color: #1717205d; 79 | color: #fff; 80 | border: none; 81 | cursor: pointer; 82 | } 83 | 84 | .logout:hover{ 85 | background-color: #1717209c; 86 | border: 1px solid rgba(255, 255, 255, 0.089); 87 | } 88 | 89 | .submit{ 90 | width: 20%; 91 | height: 50px; 92 | background: #1DAACC; 93 | border-radius: 5px; 94 | font-family: 'Poppins'; 95 | font-weight: 500; 96 | font-size: 18px; 97 | text-align: center; 98 | color: #FFFFFF; 99 | border: none; 100 | user-select: none; 101 | cursor: pointer; 102 | display: flex; 103 | flex-direction: row; 104 | justify-content: center; 105 | align-items: center; 106 | flex: 0.3; 107 | } 108 | 109 | .cancel{ 110 | width: 20%; 111 | height: 50px; 112 | background: transparent; 113 | border-radius: 5px; 114 | font-family: 'Poppins'; 115 | font-weight: 500; 116 | font-size: 18px; 117 | text-align: center; 118 | color: #FFFFFF; 119 | border: 1px solid #20222D; 120 | user-select: none; 121 | cursor: pointer; 122 | display: flex; 123 | flex-direction: row; 124 | justify-content: center; 125 | align-items: center; 126 | flex: 0.3; 127 | } 128 | .cancel:hover{ 129 | background-color: #20222D; 130 | } 131 | 132 | .folderBox { 133 | width: 125px; 134 | height: 125px; 135 | display: flex; 136 | flex-direction: column; 137 | justify-content: center; 138 | align-items: center; 139 | gap: 10px; 140 | color: #FFF; 141 | cursor: pointer; 142 | } 143 | 144 | .folderBox:hover{ 145 | background: rgba(37, 153, 255, 0.212); 146 | border: 1px solid #2599FF; 147 | border-radius: 5px; 148 | } 149 | 150 | .folderName{ 151 | color: #fff; 152 | text-align: center; 153 | font-family: 'Poppins'; 154 | font-size: 14px; 155 | margin: 0; 156 | overflow: hidden; 157 | white-space: nowrap; 158 | text-overflow:clip; 159 | width: 90px; 160 | user-select: none; 161 | } 162 | 163 | .folderContainer{ 164 | width: 100%; 165 | height: auto; 166 | min-height: 350px; 167 | overflow-y: auto; 168 | display: flex; 169 | flex-direction: row; 170 | flex-wrap: wrap; 171 | justify-content: flex-start; 172 | align-items: flex-start; 173 | align-content: flex-start; 174 | } 175 | 176 | .empty{ 177 | width: 100%; 178 | height: auto; 179 | display: flex; 180 | flex-direction: row; 181 | justify-content: center; 182 | align-items: center; 183 | } 184 | 185 | .blur{ 186 | width: 2500px; 187 | height: 2500px; 188 | background: linear-gradient(272.79deg, rgba(0, 0, 0, 0.61) 3.54%, rgba(48, 53, 68, 0.61) 81.55%), linear-gradient(153.79deg, #1DAACC 0.41%, #20222D 50.2%); 189 | filter: blur(45px); 190 | position: absolute; 191 | z-index: 0; 192 | animation-name: rotation; 193 | animation-iteration-count: infinite; 194 | animation-duration: 10s; 195 | animation-timing-function: linear; 196 | } 197 | 198 | @keyframes rotation { 199 | 0% { 200 | transform: rotate(0deg); 201 | } 202 | 100% { 203 | transform: rotate(360deg); 204 | } 205 | 206 | } -------------------------------------------------------------------------------- /components/BackgroundForm.js: -------------------------------------------------------------------------------- 1 | import styles from "../styles/BackgroundForm.module.css"; 2 | 3 | // Icons 4 | import { FiUploadCloud } from "react-icons/fi"; 5 | import { RiUnsplashFill } from "react-icons/ri"; 6 | 7 | // Other libs 8 | import { toast } from "react-hot-toast"; 9 | import { nanoid } from "nanoid"; 10 | 11 | // React & Redux 12 | import { useState } from "react"; 13 | import { 14 | addBackgroundColor, 15 | addBackgroundFirebase, 16 | } from "../store/backgroundSlice"; 17 | import { useDispatch, useSelector } from "react-redux"; 18 | 19 | // Firebase 20 | import { storage } from "../firebaseConfig"; 21 | import { getDownloadURL, ref, uploadBytes } from "firebase/storage"; 22 | import { set, ref as refDatabase } from "firebase/database"; 23 | import { database } from "../firebaseConfig"; 24 | 25 | // Components 26 | import ColorPicker from "./ColorPicker"; 27 | import { useEffect } from "react"; 28 | 29 | const BackgroundForm = ({ setUnsplashOpen }) => { 30 | const dispatch = useDispatch(); 31 | 32 | const background = useSelector((state) => state.background); // Gets all the backgrounds of this room 33 | const page = useSelector((state) => state.page.current); // Gets the currnt page 34 | const roomId = useSelector((state) => state.room.id); // Gets the current room id 35 | const currentBg = background.filter((bg) => bg.page === page)[0]; // Gets the Bg of the Current Page 36 | 37 | const [color, setColor] = useState("#FFFFFF"); 38 | 39 | useEffect(() => { 40 | if (!currentBg) { 41 | setColor("#FFFFFF"); 42 | return; 43 | } 44 | if (currentBg.source === "color") { 45 | setColor(currentBg.background); 46 | } else { 47 | setColor("#FFFFFF"); 48 | } 49 | }, [currentBg]); 50 | 51 | const uploadHandler = (e) => { 52 | const file = e.target.files[0]; 53 | 54 | // This is the reference to the location where the uploaded file will be saved 55 | // Automatically overwrites the existing file if the background image of a page is changed 56 | const imageRef = ref( 57 | storage, 58 | `background/background_${roomId}_${page}.jpg` 59 | ); 60 | 61 | const uploadingImage = uploadBytes(imageRef, file).then(() => { 62 | // Gets the URL of the uploaded Background Image 63 | const loadingImage = getDownloadURL(imageRef).then((url) => { 64 | const data = { 65 | id: nanoid(), 66 | page, 67 | background: url, 68 | roomId, 69 | source: "firebase", 70 | }; 71 | dispatch(addBackgroundFirebase(data)); 72 | // The Reference automatically overwrites the previous background if it is changed 73 | const backgroundRef = refDatabase( 74 | database, 75 | `background/${roomId}/${page}` 76 | ); 77 | set(backgroundRef, data); 78 | }); 79 | 80 | toast.promise(loadingImage, { 81 | loading: "Loading Image", 82 | success: "Image Loaded", 83 | error: "Error when Loading", 84 | }); 85 | }); 86 | 87 | toast.promise(uploadingImage, { 88 | loading: "Uploading Image", 89 | success: "Image Uploaded", 90 | error: "Error when Uploading", 91 | }); 92 | }; 93 | 94 | const submitHandler = () => { 95 | console.log("Submitted!"); 96 | const data = { 97 | id: nanoid(), 98 | page, 99 | background: color, 100 | roomId, 101 | source: "color", 102 | }; 103 | 104 | dispatch(addBackgroundColor(data)); 105 | // The Reference automatically overwrites the previous background if it is changed 106 | const backgroundRef = refDatabase(database, `background/${roomId}/${page}`); 107 | set(backgroundRef, data); 108 | toast.success("Background Color Updated!"); 109 | }; 110 | 111 | return ( 112 |
113 |
114 | 122 | 129 |
130 |
setUnsplashOpen(true)} 134 | > 135 | 136 | Launch Unsplash 137 |
138 | 139 | 146 |
147 | ); 148 | }; 149 | 150 | export default BackgroundForm; 151 | -------------------------------------------------------------------------------- /styles/Edit.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: #121219; 3 | width: 100%; 4 | height: 100%; 5 | margin: 0; 6 | padding: 0; 7 | display: flex; 8 | flex-direction: row; 9 | } 10 | 11 | .nav { 12 | background-color: #08090D; 13 | height: 100vh; 14 | width: 400px; 15 | display: flex; 16 | flex-direction: column; 17 | padding-top: 75px; 18 | padding-left: 50px; 19 | padding-right: 50px; 20 | gap: 10px; 21 | border-right: solid 1px #20222D ;; 22 | } 23 | 24 | .navButton { 25 | background-color: transparent; 26 | padding-left: 24px; 27 | display: flex; 28 | flex-direction: row; 29 | align-items: center; 30 | gap: 15px; 31 | height: 50px; 32 | width: 300px; 33 | font-size: 18px; 34 | border: none; 35 | border-radius: 5px; 36 | font-family: 'Poppins'; 37 | color: #ffffff; 38 | } 39 | 40 | .navButton:hover { 41 | background-color: #171720; 42 | 43 | } 44 | 45 | .navButtonActive { 46 | background-color: #2599FF; 47 | transition: ease-in-out; 48 | } 49 | 50 | .navButtonActive:hover { 51 | background-color: #2599FF; 52 | } 53 | 54 | .inputShaded { 55 | background-color: #303544; 56 | padding-left: 24px; 57 | height: 50px; 58 | width: 300px; 59 | font-size: 18px; 60 | border: none; 61 | border-radius: 5px; 62 | font-family: 'Poppins'; 63 | margin-bottom: 15px; 64 | } 65 | 66 | .fileName { 67 | border-bottom:1.5px solid #20222D; 68 | border-top:1.5px solid #20222D; 69 | display: flex; 70 | flex-direction: row; 71 | justify-content: center; 72 | align-items: center; 73 | height: 50px; 74 | width: 300px; 75 | font-size: 18px; 76 | border-radius: 0px; 77 | font-family: 'Poppins'; 78 | margin-bottom: 15px; 79 | font-weight: 600; 80 | } 81 | 82 | .divider { 83 | background-color: #20222D; 84 | height: 1px; 85 | width: 100%; 86 | margin-top: 10px; 87 | margin-bottom: 10px; 88 | } 89 | 90 | .workspace { 91 | width: 100%; 92 | height: 100vh; 93 | display: flex; 94 | flex-direction: row; 95 | justify-content: center; 96 | align-items: center; 97 | } 98 | 99 | .alignSpace { 100 | width: 960px; 101 | height: 100%; 102 | display: flex; 103 | flex-direction: column; 104 | justify-content: center; 105 | align-items: center; 106 | } 107 | 108 | .action { 109 | width: 100%; 110 | display: flex; 111 | flex-direction: row; 112 | justify-content: space-between; 113 | gap: 20px; 114 | 115 | } 116 | .actionButtons { 117 | display: flex; 118 | flex-direction: row; 119 | justify-content: space-between; 120 | gap: 5px; 121 | align-items: center; 122 | cursor: pointer; 123 | } 124 | 125 | .page { 126 | border: 1px solid #20222D; 127 | border-radius: 10px; 128 | height: 50px; 129 | width: 50px; 130 | display: flex; 131 | justify-content: center; 132 | align-items: center; 133 | font-family: 'Poppins'; 134 | font-weight: bold; 135 | font-size: 18px; 136 | color: #fff; 137 | } 138 | 139 | .actionButton { 140 | width: 100px; 141 | height: 50px; 142 | background-color: #303544; 143 | background-color: transparent; 144 | display: flex; 145 | flex-direction: row; 146 | justify-content: center; 147 | align-items: center; 148 | border: none; 149 | /* border-bottom: 1px solid #20222D; */ 150 | border-radius: 10px; 151 | font-family: 'Poppins'; 152 | font-size: 18px; 153 | gap:5px; 154 | cursor: pointer; 155 | color: #fff; 156 | } 157 | .actionButton:hover{ 158 | background-color: #20222D; 159 | } 160 | 161 | .actionButton2 { 162 | width: 150px; 163 | height: 50px; 164 | background-color: #2599FF; 165 | display: flex; 166 | flex-direction: row; 167 | justify-content: center; 168 | align-items: center; 169 | border: none; 170 | /* border-bottom: 1px solid #20222D; */ 171 | border-radius: 10px; 172 | font-family: 'Poppins'; 173 | font-size: 18px; 174 | gap:5px; 175 | cursor: pointer; 176 | color: #fff; 177 | } 178 | 179 | .pages { 180 | position: relative; 181 | display: flex; 182 | flex-direction: row; 183 | justify-content: flex-start; 184 | align-items: center; 185 | gap: 15px; 186 | height: 150px; 187 | width: 960px; 188 | border:1px solid #20222D; 189 | border-radius: 10px; 190 | padding-left: 25px; 191 | overflow-x: auto; 192 | /* scrollbar-width: 2px; 193 | scrollbar-color: transparent transparent; 194 | scrollbar-gutter: stable both-edges; */ 195 | -ms-overflow-style: none; /* IE and Edge */ 196 | scrollbar-width: none; /* Firefox */ 197 | } 198 | 199 | .pages::-webkit-scrollbar { 200 | display: none; 201 | width: 2px; 202 | height: 8px; 203 | position: absolute; 204 | scrollbar-gutter: stable both-edges; 205 | margin: 0; 206 | padding: 0; 207 | } 208 | /* 209 | 210 | .pages::-webkit-scrollbar-thumb{ 211 | border-radius: 5px; 212 | transition: background 0.3s; 213 | background: rgba(0, 0, 0, .75); 214 | width: 2px; 215 | position: absolute; 216 | scrollbar-gutter: stable both-edges; 217 | margin: 0; 218 | padding: 0; 219 | } */ -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import styles from "../styles/Home.module.css"; 2 | 3 | // Next 4 | import Head from "next/head"; 5 | import Image from "next/image"; 6 | import Link from "next/link"; 7 | import { useRouter } from "next/router"; 8 | 9 | // Firebase 10 | import { database } from "../firebaseConfig"; 11 | import { ref, get, child } from "firebase/database"; 12 | 13 | // React Redxu 14 | import { useState, useEffect } from "react"; 15 | import { useDispatch } from "react-redux"; 16 | import { resetUser } from "../store/userSlice"; 17 | 18 | // Icons 19 | import { FaGithub } from "react-icons/fa"; 20 | import { FiExternalLink } from "react-icons/fi"; 21 | 22 | // Other libs 23 | import toast, { Toaster } from "react-hot-toast"; 24 | 25 | export default function Home() { 26 | const router = useRouter(); 27 | const dispatch = useDispatch(); 28 | 29 | const [roomId, setRoomId] = useState(""); // Stores the entered room id 30 | const [activeForm, setActiveForm] = useState(true); // Switches between forms 31 | 32 | useEffect(() => { 33 | // Resets user or logouts user 34 | dispatch(resetUser()); 35 | }, [dispatch]); 36 | 37 | // Checks whether the room exists and navigates the user to that room 38 | const joinRoom = (e) => { 39 | e.preventDefault(); 40 | const toastId = toast.loading("Waiting..."); 41 | get(child(ref(database), `rooms/${roomId}`)) 42 | .then((snapshot) => { 43 | if (snapshot.exists()) { 44 | router.push(`/edit?q=${roomId}`); 45 | } else { 46 | toast.error("Invalid Room Id", { 47 | id: toastId, 48 | }); 49 | console.log("ERROR!"); 50 | } 51 | }) 52 | .catch((err) => { 53 | console.error(err); 54 | }); 55 | }; 56 | 57 | return ( 58 |
59 | 60 | Sigma 61 | 65 | 66 | 67 | 77 | 95 |
🚧 UNDER CONSTRUCTION
96 |

97 | Real-time Collaboration Design Tool 98 |

99 |

100 | Sigma is a web app where you can make stunning designs with your team 101 | supafast! ⚡ 102 |

103 | {activeForm ? ( 104 | 105 | 108 | 109 | ) : ( 110 |
111 | setRoomId(e.target.value)} 115 | value={roomId} 116 | className={styles.input} 117 | placeholder="Enter Room Code" 118 | > 119 | 122 |
123 | )} 124 |

setActiveForm((prev) => !prev)} 127 | > 128 | {activeForm ? "or, join a room" : "or, create a room"} 129 |

130 | 131 | 135 | 136 |
137 |
138 | preview 144 |
145 | 163 |
164 | ); 165 | } 166 | -------------------------------------------------------------------------------- /components/Unsplash.js: -------------------------------------------------------------------------------- 1 | import styles from "../styles/Unsplash.module.css"; 2 | 3 | // React 4 | import { useEffect, useState } from "react"; 5 | 6 | // Icons 7 | import { AiOutlineEnter } from "react-icons/ai"; 8 | import { RiUnsplashFill } from "react-icons/ri"; 9 | import { MdClose } from "react-icons/md"; 10 | 11 | // Other Libs 12 | import { nanoid } from "nanoid"; 13 | import toast from "react-hot-toast"; 14 | import { createApi } from "unsplash-js"; 15 | 16 | // Redux 17 | import { useDispatch, useSelector } from "react-redux"; 18 | import { addElement } from "../store/elementSlice"; 19 | import { addBackgroundUnsplash } from "../store/backgroundSlice"; 20 | 21 | // Firebase 22 | import { push, set, ref } from "firebase/database"; 23 | import { database } from "../firebaseConfig"; 24 | 25 | const Unsplash = ({ setUnsplashOpen, type }) => { 26 | const [photos, setPhotos] = useState([]); // Stores the fetched pictures 27 | const [query, setQuery] = useState("wallpaper"); // Sets the search parameter for Unsplash 28 | 29 | const dispatch = useDispatch(); 30 | 31 | const page = useSelector((state) => state.page.current); // Gets the current page 32 | const roomId = useSelector((state) => state.room.id); // Gets the current room Id 33 | 34 | // Creates the API & fetches Pictures and stores them in photos 35 | useEffect(() => { 36 | const unsplash = createApi({ 37 | accessKey: process.env.NEXT_PUBLIC_UNSPLASH_API, 38 | }); 39 | 40 | unsplash.search 41 | .getPhotos({ page: 1, perPage: 30, query: query }) 42 | .then((data) => { 43 | setPhotos(data.response.results); 44 | }); 45 | }, [query]); 46 | 47 | const enterHandler = (e) => { 48 | if (e.key !== "Enter") { 49 | return; 50 | } 51 | if (e.target.value === "") { 52 | setQuery("wallpaper"); 53 | return; 54 | } 55 | setQuery(e.target.value); 56 | }; 57 | 58 | const blurHandler = (e) => { 59 | if (e.target.value === "") { 60 | setQuery("wallpaper"); 61 | return; 62 | } 63 | setQuery(e.target.value); 64 | }; 65 | 66 | const imageSelectHandler = (e) => { 67 | // Checks whether it is an image element or a background 68 | if (type === "image") { 69 | const src = e.target.currentSrc; 70 | const data = { 71 | id: nanoid(), 72 | src, 73 | type: "image", 74 | x: 0, 75 | y: 0, 76 | width: e.target.naturalWidth, 77 | height: e.target.naturalHeight, 78 | page, 79 | roomId, 80 | }; 81 | dispatch(addElement(data)); 82 | const elementRef = ref(database, "elements/" + roomId); 83 | set(push(elementRef), data); 84 | toast.success("Image Added!"); 85 | } else if (type === "background") { 86 | const src = e.target.alt; 87 | 88 | const data = { 89 | id: nanoid(), 90 | background: src, 91 | page, 92 | roomId, 93 | source: "unsplash", 94 | }; 95 | dispatch(addBackgroundUnsplash(data)); 96 | 97 | const backgroundRef = ref(database, `background/${roomId}/${page}`); 98 | set(backgroundRef, data); 99 | toast.success("Background Image Updated!"); 100 | } 101 | 102 | setUnsplashOpen(false); 103 | }; 104 | 105 | return ( 106 |
setUnsplashOpen(false)} */ 108 | > 109 |
110 |
setUnsplashOpen(false)}> 111 | 112 | Close 113 |
114 |
115 | 116 | Powered by Unsplash 117 |
118 |
119 |
120 | 127 |

128 | 129 | Press Enter 130 |

131 |
132 |
133 |
134 | {photos.slice(0, 11).map((photo) => { 135 | return ( 136 | {photo.urls.regular} 144 | ); 145 | })} 146 |
147 |
148 | {photos.slice(11, 21).map((photo) => { 149 | return ( 150 | {photo.urls.regular} 158 | ); 159 | })} 160 |
161 |
162 | {photos.slice(21, 31).map((photo) => { 163 | return ( 164 | {photo.urls.regular} 172 | ); 173 | })} 174 |
175 |
176 |
177 | ); 178 | }; 179 | 180 | export default Unsplash; 181 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 50px 200px 100px 200px; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | width: 100%; 7 | height:auto; 8 | } 9 | 10 | .form { 11 | display: flex; 12 | flex-direction: column; 13 | } 14 | 15 | .nav{ 16 | width: 100%; 17 | height: auto; 18 | display: flex; 19 | flex-direction: row; 20 | justify-content: space-between; 21 | align-items: center; 22 | } 23 | 24 | .logo{ 25 | display: flex; 26 | flex-direction: row; 27 | align-items: center; 28 | gap: 15px; 29 | font-family: 'Hustle Bright'; 30 | color: #fff; 31 | font-size: 24px; 32 | width: 300px; 33 | height: 50px; 34 | } 35 | 36 | .github { 37 | display: flex; 38 | flex-direction: row; 39 | align-items: center; 40 | justify-content: center; 41 | gap: 5px; 42 | width: 160px; 43 | height: 40px; 44 | background: #303544; 45 | border: 1px solid #20222D; 46 | border-radius: 7px; 47 | font-family: 'Poppins'; 48 | font-weight: 600; 49 | color: #C1C2C7; 50 | cursor: pointer; 51 | } 52 | 53 | .github:hover{ 54 | background-color: #262a39; 55 | } 56 | 57 | .update{ 58 | width: 200px; 59 | height: 30px; 60 | border: 1px solid #2A2E3B; 61 | border-radius: 30px; 62 | font-family: 'Poppins'; 63 | font-style: normal; 64 | font-weight: 700; 65 | font-size: 10px; 66 | line-height: 15px; 67 | text-align: center; 68 | color: #DED128; 69 | display: flex; 70 | flex-direction: column; 71 | justify-content: center; 72 | margin-top: 75px; 73 | } 74 | 75 | .heading { 76 | width: 1000px; 77 | height: auto; 78 | font-family: 'Inter'; 79 | font-style: normal; 80 | font-weight: 900; 81 | font-size: 72px; 82 | line-height: 87px; 83 | text-align: center; 84 | color: #FFFFFF; 85 | margin-top: 20px; 86 | margin-bottom: 10px; 87 | } 88 | 89 | .heading::selection{ 90 | color: #20222D; 91 | background-color: #1DAACC; 92 | } 93 | .heading span::selection{ 94 | color: #20222D; 95 | background-color: #1DAACC; 96 | } 97 | 98 | .heading span { 99 | background: linear-gradient(173.64deg, rgba(0, 0, 0, 0.61) 1.75%, rgba(48, 53, 68, 0.61) 77.02%), linear-gradient(88.2deg, #1DAACC 14.79%, #20222D 54.91%); 100 | -webkit-background-clip: text; 101 | -webkit-text-fill-color: transparent; 102 | background-clip: text; 103 | } 104 | 105 | .caption{ 106 | width: 700px; 107 | height: auto; 108 | font-family: 'Poppins'; 109 | font-style: normal; 110 | font-weight: 400; 111 | font-size: 24px; 112 | line-height: 34px; 113 | text-align: center; 114 | color: #303544; 115 | margin-top: 10px; 116 | margin-bottom: 60px; 117 | } 118 | 119 | .caption::selection{ 120 | color: #20222D; 121 | background-color: #1DAACC; 122 | } 123 | 124 | .input{ 125 | width: 300px; 126 | height: 50px; 127 | border: 1px solid #374860; 128 | border-radius: 5px; 129 | font-family: 'Poppins'; 130 | font-weight: 400; 131 | font-size: 18px; 132 | text-align: center; 133 | color: #303544; 134 | display: flex; 135 | flex-direction: column; 136 | justify-content: center; 137 | background-color: transparent; 138 | margin-bottom: 15px; 139 | } 140 | 141 | .input::placeholder{ 142 | color: #303544; 143 | } 144 | 145 | .submit{ 146 | width: 300px; 147 | height: 50px; 148 | background: #1DAACC; 149 | border-radius: 5px; 150 | font-family: 'Poppins'; 151 | font-weight: 500; 152 | font-size: 18px; 153 | text-align: center; 154 | color: #FFFFFF; 155 | border: none; 156 | user-select: none; 157 | cursor: pointer; 158 | display: flex; 159 | flex-direction: row; 160 | justify-content: center; 161 | align-items: center; 162 | gap: 10px; 163 | } 164 | 165 | .switchForm{ 166 | font-family: 'Poppins'; 167 | font-weight: 400; 168 | font-size: 16px; 169 | line-height: 24px; 170 | 171 | 172 | color: #303544; 173 | cursor: pointer; 174 | user-select: none; 175 | } 176 | 177 | .switchForm:hover{ 178 | text-decoration: underline; 179 | } 180 | 181 | .preview{ 182 | position: relative; 183 | margin-top: 100px; 184 | margin-bottom: 150px; 185 | } 186 | 187 | .previewBlur{ 188 | position: absolute; 189 | width: 110%; 190 | height: 110%; 191 | 192 | background: linear-gradient(272.79deg, rgba(0, 0, 0, 0.61) 3.54%, rgba(48, 53, 68, 0.61) 81.55%), linear-gradient(153.79deg, #1DAACC 0.41%, #20222D 50.2%); 193 | filter: blur(45px); 194 | } 195 | 196 | .line{ 197 | width: 100%; 198 | height: 1px; 199 | color: #20222D; 200 | background-color: #20222D; 201 | border: #20222D; 202 | } 203 | 204 | .footer{ 205 | width: 100%; 206 | height: auto; 207 | display: flex; 208 | flex-direction: row; 209 | justify-content: space-between; 210 | align-items: center; 211 | } 212 | 213 | .signature{ 214 | font-family: 'Poppins'; 215 | font-style: normal; 216 | font-weight: 400; 217 | font-size: 18px; 218 | line-height: 34px; 219 | text-align: center; 220 | color: #303544; 221 | } 222 | 223 | .signature:hover{ 224 | text-decoration: underline; 225 | } 226 | 227 | .star2{ 228 | display: none; 229 | } 230 | 231 | @media screen and (max-width:1100px) { 232 | .container{ 233 | padding-left: 50px; 234 | padding-right: 50px; 235 | align-items: flex-start; 236 | } 237 | 238 | .heading{ 239 | text-align: left; 240 | width: 100%; 241 | height: auto; 242 | } 243 | 244 | .caption{ 245 | text-align: left; 246 | width: 100%; 247 | height: auto; 248 | } 249 | } 250 | 251 | @media screen and (max-width: 600px) { 252 | .heading{ 253 | font-size: 52px; 254 | line-height: 60px; 255 | 256 | } 257 | .caption{ 258 | font-size: 18px; 259 | line-height: 24px; 260 | margin-bottom: 30px; 261 | } 262 | .form{ 263 | display: none; 264 | } 265 | .switchForm{ 266 | display: none; 267 | } 268 | .footer{ 269 | flex-direction: column-reverse; 270 | align-items: flex-start; 271 | gap:10px; 272 | } 273 | .star{ 274 | display: none; 275 | } 276 | .star2{ 277 | display: flex; 278 | width: 200px; 279 | height: 50px; 280 | font-size: 15px; 281 | } 282 | } 283 | 284 | @media screen and (max-width:450px) { 285 | .container{ 286 | padding-top: 30px; 287 | padding-left: 20px; 288 | padding-right: 20px; 289 | padding-bottom: 75px; 290 | } 291 | .heading{ 292 | margin-top: 10px; 293 | } 294 | .update{ 295 | margin-top: 50px; 296 | } 297 | .name{ 298 | font-size: 18px; 299 | } 300 | .nav{ 301 | width: 120px; 302 | } 303 | } -------------------------------------------------------------------------------- /pages/auth.js: -------------------------------------------------------------------------------- 1 | import styles from "../styles/Auth.module.css"; 2 | 3 | // React Redux Next 4 | import { useState } from "react"; 5 | import { useDispatch } from "react-redux"; 6 | import { setUser } from "../store/userSlice"; 7 | import { useRouter } from "next/router"; 8 | import Head from "next/head"; 9 | 10 | // Other libs 11 | import toast, { Toaster } from "react-hot-toast"; 12 | 13 | // Amplify 14 | import { Auth, Hub } from "aws-amplify"; 15 | 16 | const Authenticate = () => { 17 | const dispatch = useDispatch(); 18 | const router = useRouter(); 19 | 20 | const [login, setLogin] = useState(false); // Used as a switch between login and sign up form 21 | const [username, setUsername] = useState(""); // Stores username 22 | const [password, setPassword] = useState(""); // Stores Password 23 | const [email, setEmail] = useState(""); // Stores Email 24 | const [code, setCode] = useState(""); // Stores the verification code 25 | const [enableVerify, setEnableVerify] = useState(false); // Used to switch from sign up form to verify form 26 | 27 | const submitHandler = async () => { 28 | if (login) { 29 | // User is loging in 30 | // Checks for missing creds 31 | if (!username || !password) { 32 | toast.error("Missing Credentials"); 33 | return; 34 | } 35 | try { 36 | // Signs in the user 37 | const user = await Auth.signIn(username, password); 38 | // Sets the user (this will only be done on login) 39 | dispatch(setUser(user.attributes.email)); 40 | // User is taken to files page 41 | router.push("/files"); 42 | } catch (error) { 43 | console.log("error signing in", error); 44 | } 45 | } else { 46 | // User is signing up 47 | // Checks for missing creds 48 | if (!username || !password || !email) { 49 | toast.error("Missing Credentials"); 50 | return; 51 | } 52 | try { 53 | // Users sign up and receive a verification code 54 | const { user } = await Auth.signUp({ 55 | username, 56 | password, 57 | attributes: { 58 | email, 59 | }, 60 | autoSignIn: { 61 | enabled: true, 62 | }, 63 | }); 64 | 65 | // Switches to verification form 66 | setEnableVerify(true); 67 | } catch (error) { 68 | console.log("Error Signing Up: ", error); 69 | } 70 | } 71 | }; 72 | 73 | function listentoAuthSignInEvent() { 74 | // User actions are caught here 75 | Hub.listen("auth", ({ payload }) => { 76 | const { event } = payload; 77 | if (event === "autoSignIn") { 78 | const user = payload.data; 79 | console.log("Auto Sign In User: " + user); 80 | } else if (event === "autoSignIn_failure") { 81 | console.log("FAILED: " + payload); 82 | } else if (event === "signIn") { 83 | const user = payload.data; 84 | // Sets the user 85 | dispatch(setUser(user.attributes.email)); 86 | router.push("/files"); 87 | } 88 | }); 89 | } 90 | 91 | const verifyHandler = async () => { 92 | try { 93 | // Confirming Verification Code 94 | const a = await Auth.confirmSignUp(username, code); 95 | console.log("Confirmed: " + a); 96 | } catch (error) { 97 | console.log("Error Confirming: ", error); 98 | } 99 | }; 100 | 101 | listentoAuthSignInEvent(); 102 | 103 | return ( 104 | <> 105 | 106 | Auth 107 | 108 | 109 | 119 |
120 | {/* // UI Element */} 121 |
122 |
e.preventDefault()}> 123 |

{login ? "Log In" : "Sign Up"}

124 |
125 | setUsername(e.target.value)} 131 | disabled={enableVerify} 132 | style={{ 133 | backgroundColor: `${enableVerify ? "#81818A" : "transparent"}`, 134 | }} 135 | /> 136 | {/* Form Switching */} 137 | {login ? ( 138 | <> 139 | ) : ( 140 | setEmail(e.target.value)} 146 | disabled={enableVerify} 147 | style={{ 148 | backgroundColor: `${enableVerify ? "#81818A" : "transparent"}`, 149 | }} 150 | /> 151 | )} 152 | setPassword(e.target.value)} 158 | disabled={enableVerify} 159 | style={{ 160 | backgroundColor: `${enableVerify ? "#81818A" : "transparent"}`, 161 | }} 162 | /> 163 | {!login && ( 164 |

165 | Password length must be atleast 8 characters 166 |

167 | )} 168 | {/* Verification form switch */} 169 | {enableVerify ? ( 170 | setCode(e.target.value)} 176 | /> 177 | ) : ( 178 | <> 179 | )} 180 | 181 | {enableVerify ? ( 182 | 185 | ) : ( 186 | 189 | )} 190 | 200 |
201 |
202 | 203 | ); 204 | }; 205 | 206 | export default Authenticate; 207 | -------------------------------------------------------------------------------- /pages/files.js: -------------------------------------------------------------------------------- 1 | import styles from "../styles/Files.module.css"; 2 | 3 | // React Next 4 | import { useEffect, useState } from "react"; 5 | import Image from "next/image"; 6 | import Head from "next/head"; 7 | import { useRouter } from "next/router"; 8 | 9 | // Redux 10 | import { useDispatch, useSelector } from "react-redux"; 11 | import { setInitialRoom } from "../store/roomSlice"; 12 | import { resetBackground } from "../store/backgroundSlice"; 13 | import { resetElement } from "../store/elementSlice"; 14 | import { resetSelected } from "../store/selectedElementSlice"; 15 | import { resetPage } from "../store/pageSlice"; 16 | 17 | // Firebase 18 | import { database } from "../firebaseConfig"; 19 | import { set, ref, get, child } from "firebase/database"; 20 | 21 | // Other libs 22 | import { nanoid } from "nanoid"; 23 | import { FiLogOut } from "react-icons/fi"; 24 | 25 | const Files = () => { 26 | const user = useSelector((state) => state.user.user); // Gets the current logged in user's email 27 | const router = useRouter(); 28 | const dispatch = useDispatch(); 29 | const [query, setQuery] = useState(""); // Sets the search query 30 | const [name, setName] = useState(""); // Sets the name of the new folder 31 | const [projects, setProjects] = useState([]); // Stores all the projects of the logged in user 32 | const [newProject, setNewProject] = useState(false); // State for checking whether user is adding a new project 33 | 34 | // Resets the Redux Store so that the state of multiple rooms don't clash 35 | const resetHandler = () => { 36 | dispatch(resetBackground()); 37 | dispatch(resetElement()); 38 | dispatch(resetPage()); 39 | dispatch(resetSelected()); 40 | }; 41 | 42 | // Runs when a new folder is clicked 43 | const openFolderHandler = (id, name, pages) => { 44 | if (!id) { 45 | return; 46 | } 47 | dispatch( 48 | setInitialRoom({ 49 | admin: user, 50 | name, 51 | pages, 52 | id, 53 | }) 54 | ); 55 | resetHandler(); 56 | router.push(`/edit?q=${id}`); 57 | }; 58 | 59 | useEffect(() => { 60 | // Checks if someone is logged or not 61 | console.log(user); 62 | if (!user) { 63 | router.push("/auth"); 64 | } 65 | 66 | // Gets all the rooms and stores the ones created by the user 67 | get(child(ref(database), `rooms`)).then((snapshot) => { 68 | if (snapshot.exists()) { 69 | // Loops over each available room 70 | snapshot.forEach((childSnapshot) => { 71 | const childValue = childSnapshot.val(); 72 | console.log(childValue); 73 | // Checks all the available rooms for the one owned by the user 74 | if (childValue.admin === user) { 75 | setProjects((prev) => [ 76 | ...prev, 77 | { 78 | id: childSnapshot.key, 79 | name: childValue.name, 80 | }, 81 | ]); 82 | } 83 | }); 84 | } else { 85 | // Prints this if there are no elements 86 | console.log("No data available"); 87 | } 88 | }); 89 | }, [user, router]); 90 | 91 | // Runs when creates a new project 92 | const addProjectHandler = () => { 93 | if (!user) { 94 | console.log("No User Found!"); 95 | return; 96 | } 97 | if (!name) { 98 | console.log("No User Found!"); 99 | setName("Untitled"); 100 | } 101 | //Creates a room Id 102 | const room = nanoid(); 103 | // Adds the user as the admin of the room 104 | set(ref(database, "rooms/" + room), { 105 | admin: user, 106 | name, 107 | pages: 1, 108 | }); 109 | 110 | // Sets the initial room status 111 | dispatch( 112 | setInitialRoom({ 113 | admin: user, 114 | name, 115 | pages: 1, 116 | id: room, 117 | }) 118 | ); 119 | // Resets all the previous Redux state 120 | resetHandler(); 121 | router.push(`/edit?q=${room}`); 122 | }; 123 | 124 | const logoutHandler = () => { 125 | resetHandler(); 126 | router.push("/"); 127 | }; 128 | 129 | return ( 130 | <> 131 | 132 | Files 133 | 134 | 135 |
136 |
137 |
138 |

Projects

139 | 142 |
143 |
144 | {/* Conditionally switches between the search bar and the projects */} 145 |
146 | {newProject ? ( 147 | setName(e.target.value)} 152 | placeholder="Name the folder" 153 | /> 154 | ) : ( 155 | setQuery(e.target.value)} 160 | placeholder="Search for files" 161 | /> 162 | )} 163 | {!newProject && ( 164 | 170 | )} 171 | {newProject && ( 172 | 175 | )} 176 | {newProject && ( 177 | 183 | )} 184 |
185 |
186 | {projects.length !== 0 ? ( 187 | // Filters the files by matching the file names with the entered query 188 | // Turns both the file name and query to lowercase to remove case-sensitivity 189 | // Maps over the filtered array 190 | // Creates a file for each which onClick takes the user to the respective project 191 | projects 192 | .filter((file) => 193 | file.name.toLowerCase().startsWith(query.toLowerCase()) 194 | ) 195 | .map((file, index) => { 196 | return ( 197 |
201 | openFolderHandler(file.id, file.name, file.pages) 202 | } 203 | > 204 | folder 210 |

{file.name}

211 |
212 | ); 213 | }) 214 | ) : ( 215 | /* Renders Image to convey that the folder is empty */ 216 |
217 | folder 223 |
224 | )} 225 |
226 |
227 |
228 | 229 | ); 230 | }; 231 | 232 | export default Files; 233 | -------------------------------------------------------------------------------- /components/TextForm.js: -------------------------------------------------------------------------------- 1 | import styles from "../styles/TextForm.module.css"; 2 | 3 | // Icons 4 | import { TbAlignLeft, TbAlignRight, TbAlignCenter } from "react-icons/tb"; 5 | 6 | // Other Libs 7 | import { nanoid } from "nanoid"; 8 | import toast from "react-hot-toast"; 9 | 10 | // React & Redux 11 | import { useEffect, useState } from "react"; 12 | import { useDispatch, useSelector } from "react-redux"; 13 | import { addElement, updateTextElement } from "../store/elementSlice"; 14 | 15 | // Firebase 16 | import { push, set, ref } from "firebase/database"; 17 | import { database } from "../firebaseConfig"; 18 | 19 | // Custom Hook 20 | import useElementUpdate from "../hooks/useElementUpdate"; 21 | 22 | // Components 23 | import ColorPicker from "./ColorPicker"; 24 | 25 | const TextForm = () => { 26 | const dispatch = useDispatch(); 27 | const elementUpdater = useElementUpdate(); 28 | 29 | const selected = useSelector((state) => state.selectedElement); // Gets the selected Element 30 | const page = useSelector((state) => state.page.current); // gets the current page 31 | const roomId = useSelector((state) => state.room.id); // gets the room id 32 | 33 | // Different states for each text property 34 | const [content, setContent] = useState(""); 35 | const [color, setColor] = useState("#FFFFFF"); 36 | const [size, setSize] = useState(16); 37 | const [font, setFont] = useState("Roboto"); 38 | const [weight, setWeight] = useState("400"); 39 | const [align, setAlign] = useState("center"); 40 | 41 | // Sets the form 42 | useEffect(() => { 43 | if (selected.type === "text") { 44 | setContent(selected.content); 45 | setColor(selected.color); 46 | setSize(selected.size); 47 | setFont(selected.font); 48 | setWeight(selected.weight); 49 | setAlign(selected.align); 50 | } else { 51 | setContent(""); 52 | setColor("#FFFFFF"); 53 | setSize(16); 54 | setFont("Roboto"); 55 | setWeight("400"); 56 | setAlign("center"); 57 | } 58 | }, [selected]); 59 | 60 | // Updates or Inserts Text 61 | const clickHandler = (e) => { 62 | e.preventDefault(); 63 | if (content === "") { 64 | toast.error("Can't Insert Empty Text!"); 65 | return; 66 | } 67 | 68 | if (selected.type === "text") { 69 | const data = { 70 | id: selected.id, 71 | content, 72 | color, 73 | size, 74 | font, 75 | weight, 76 | align, 77 | }; 78 | dispatch(updateTextElement(data)); 79 | elementUpdater(data); 80 | toast.success("Text Updated!"); 81 | } else { 82 | const data = { 83 | id: nanoid(), 84 | content, 85 | color, 86 | size, 87 | font, 88 | weight, 89 | align, 90 | x: 0, 91 | y: 0, 92 | width: 100, 93 | height: 100, 94 | type: "text", 95 | page, 96 | roomId, 97 | }; 98 | 99 | dispatch(addElement(data)); 100 | const elementRef = ref(database, "elements/" + roomId); 101 | set(push(elementRef), data); 102 | toast.success("Text Added!"); 103 | setContent(""); 104 | } 105 | }; 106 | 107 | // Contains all the font data 108 | const fontsList = [ 109 | { 110 | fontName: "Roboto", 111 | weights: [100, 300, 400, 500, 600, 700, 900], 112 | }, 113 | { 114 | fontName: "Inter", 115 | weights: [100, 300, 400, 500, 600, 700, 800, 900], 116 | }, 117 | { 118 | fontName: "Poppins", 119 | weights: [100, 300, 400, 500, 600, 700, 800, 900], 120 | }, 121 | { 122 | fontName: "Montserrat", 123 | weights: [100, 300, 400, 500, 600, 700, 800, 900], 124 | }, 125 | ]; 126 | 127 | // Conatins font weight and its equivalent text form 128 | const weightDefine = { 129 | 100: "Thin", 130 | 200: "Extra Light", 131 | 300: "Light", 132 | 400: "Regular", 133 | 500: "Medium", 134 | 600: "Semi-Bold", 135 | 700: "Bold", 136 | 800: "Extra Bold", 137 | 900: "Black", 138 | }; 139 | 140 | return ( 141 |
142 | setContent(e.target.value)} 148 | > 149 | 150 | 175 |
176 | 202 | setSize(e.target.value)} 209 | /> 210 |
211 |
212 |
213 |
setAlign("left")} 221 | > 222 | 223 |
224 |
setAlign("center")} 232 | > 233 | 234 |
235 | 236 |
setAlign("right")} 244 | > 245 | 246 |
247 |
248 | 249 |
250 | 253 |
254 | ); 255 | }; 256 | 257 | export default TextForm; 258 | -------------------------------------------------------------------------------- /components/ItemResizer.js: -------------------------------------------------------------------------------- 1 | import styles from "../styles/ItemResizer.module.css"; 2 | 3 | // Animation Library 4 | import { useDrag } from "@use-gesture/react"; 5 | import { animated, useSpring } from "react-spring"; 6 | 7 | // React & Redux 8 | import { useRef, useEffect } from "react"; 9 | import { useDispatch, useSelector } from "react-redux"; 10 | import { updateElement } from "../store/elementSlice"; 11 | import { setSelectedElement } from "../store/selectedElementSlice"; 12 | 13 | // Feather Icons 14 | import * as FeatherIcons from "react-icons/fi"; 15 | 16 | // Custom Hook 17 | import useElementUpdate from "../hooks/useElementUpdate"; 18 | 19 | const ItemResizer = ({ info, disable, updated }) => { 20 | const dispatch = useDispatch(); 21 | const elementUpdater = useElementUpdate(); 22 | 23 | const selectedId = useSelector((state) => state.selectedElement.id); // ID of the selected element 24 | 25 | let selected = false; 26 | if (selectedId === info.id) { 27 | selected = true; 28 | } 29 | 30 | // Reference to the corner-piece which increases the size of the element 31 | const dragElRef = useRef(null); 32 | 33 | // Sets the initial size & position of an element 34 | const [{ x, y, width, height }, api] = useSpring(() => ({ 35 | x: info ? info.x : 0, 36 | y: info ? info.y : 0, 37 | width: info ? info.width : 100, 38 | height: info ? info.height : 100, 39 | })); 40 | 41 | // Updates the size of an element that has been changed 42 | useEffect(() => { 43 | if (!updated) { 44 | return; 45 | } 46 | api.set({ 47 | x: info.x, 48 | y: info.y, 49 | height: info.height, 50 | width: info.width, 51 | }); 52 | }, [updated, api, info]); 53 | 54 | // Changes the size of the image before loading to fit the board 55 | const imageLoadHandler = (e) => { 56 | // Checks if the image has been loaded before 57 | if (info.loaded) { 58 | return; 59 | } 60 | console.log("NOT LOADED!: " + info); 61 | const naturalHeight = e.target.naturalHeight; 62 | const naturalWidth = e.target.naturalWidth; 63 | 64 | if (info.height < 500 && info.width < 800) { 65 | return; 66 | } 67 | 68 | // Proportionately reduces the size of the image to fit inside the board 69 | if (naturalHeight > 500) { 70 | naturalWidth = naturalWidth * (500 / naturalHeight); 71 | naturalHeight = 500; 72 | } 73 | if (naturalWidth > 800) { 74 | naturalHeight = naturalHeight * (800 / naturalWidth); 75 | naturalWidth = 800; 76 | } 77 | 78 | const newData = { 79 | id: info.id, 80 | width: naturalWidth, 81 | height: naturalHeight, 82 | x: x.get(), 83 | y: y.get(), 84 | }; 85 | 86 | // Updates the image data with new size 87 | dispatch(updateElement(newData)); 88 | api.set({ 89 | width: naturalWidth, 90 | height: naturalHeight, 91 | }); 92 | 93 | elementUpdater(newData); 94 | }; 95 | 96 | // Uses use-gesture to update the element size and position according to user interaction 97 | const bind = useDrag( 98 | (state) => { 99 | if (disable) { 100 | return; 101 | } 102 | if (selectedId !== info.id) { 103 | dispatch( 104 | setSelectedElement({ 105 | id: info.id, 106 | ...info, 107 | }) 108 | ); 109 | } 110 | if (!state.dragging) { 111 | const data = { 112 | height: height.get(), 113 | width: width.get(), 114 | x: x.get(), 115 | y: y.get(), 116 | }; 117 | elementUpdater(data); 118 | return; 119 | } 120 | 121 | // Checks whether the element is being resized instead of being dragged 122 | const Resizing = state.event.target === dragElRef.current; 123 | 124 | if (Resizing) { 125 | api.set({ 126 | width: state.offset[0], 127 | height: state.offset[1], 128 | }); 129 | } else { 130 | api.set({ 131 | x: state.offset[0], 132 | y: state.offset[1], 133 | }); 134 | dispatch( 135 | updateElement({ 136 | id: info.id, 137 | width: width.get(), 138 | height: height.get(), 139 | x: x.get(), 140 | y: y.get(), 141 | }) 142 | ); 143 | } 144 | }, 145 | { 146 | from: (event) => { 147 | const isResizing = event.target === dragElRef.current; 148 | if (isResizing) { 149 | return [width.get(), height.get()]; 150 | } else { 151 | return [x.get(), y.get()]; 152 | } 153 | }, 154 | bounds: (state) => { 155 | const isResizing = state?.event.target === dragElRef.current; 156 | if (isResizing) { 157 | return { 158 | top: 50, 159 | left: 50, 160 | right: 960 - x.get(), 161 | bottom: 540 - y.get(), 162 | }; 163 | } else { 164 | return { 165 | top: 0, 166 | left: 0, 167 | right: 960 - width.get(), 168 | bottom: 540 - height.get(), 169 | }; 170 | } 171 | }, 172 | } 173 | ); 174 | 175 | let element; 176 | // Sets the property of each element according to element type 177 | if (info?.type === "text") { 178 | let flex = "center"; 179 | if (info.align === "left") { 180 | flex = "flex-start"; 181 | } else if (info.align === "right") { 182 | flex = "flex-end"; 183 | } 184 | 185 | element = ( 186 |

201 | {info.content} 202 |

203 | ); 204 | } else if (info?.type === "image") { 205 | element = ( 206 | img 219 | ); 220 | } else if (info?.type === "icon") { 221 | if (!info.name) { 222 | return; 223 | } 224 | const icon = FeatherIcons[info.name]; 225 | element = ( 226 |
232 | {icon()} 233 |
234 | ); 235 | } else if (info?.type === "shape") { 236 | // Sets the element according to their shape type 237 | if (info.shape === "square") { 238 | element = ( 239 |
243 | ); 244 | } else if (info.shape === "circle") { 245 | element = ( 246 |
250 | ); 251 | } else if (info.shape === "triangle") { 252 | element = ( 253 |
257 | ); 258 | } else if (info.shape === "line") { 259 | element = ( 260 |
264 | ); 265 | } 266 | } 267 | 268 | const itemSelected = selected && !disable; 269 | 270 | return ( 271 | { 281 | dispatch( 282 | setSelectedElement({ 283 | id: info.id, 284 | ...info, 285 | }) 286 | ); 287 | }} 288 | {...bind()} 289 | > 290 | {element} 291 |
296 | 297 | ); 298 | }; 299 | 300 | export default ItemResizer; 301 | -------------------------------------------------------------------------------- /components/Board.js: -------------------------------------------------------------------------------- 1 | import ItemResizer from "./ItemResizer"; 2 | import styles from "../styles/Board.module.css"; 3 | 4 | // Redux & Store 5 | import { useDispatch, useSelector } from "react-redux"; 6 | import { useEffect, useRef, useState } from "react"; 7 | import { 8 | addElement, 9 | removeElement, 10 | updateElement, 11 | updateIconElement, 12 | updateShapeElement, 13 | updateTextElement, 14 | } from "../store/elementSlice"; 15 | import { resetSelected } from "../store/selectedElementSlice"; 16 | import { setCurrentPage } from "../store/pageSlice"; 17 | import { addBackground } from "../store/backgroundSlice"; 18 | 19 | // Firebase 20 | import { 21 | onChildAdded, 22 | onChildChanged, 23 | onValue, 24 | ref, 25 | get, 26 | child, 27 | remove, 28 | onChildRemoved, 29 | } from "firebase/database"; 30 | import { database } from "../firebaseConfig"; 31 | 32 | const Board = ({ page }) => { 33 | const board = useRef(); 34 | const dispatch = useDispatch(); 35 | 36 | const selected = useSelector((state) => state.selectedElement.id); // Gets the selected element 37 | const background = useSelector((state) => state.background); // Gets all the backgrounds of the room 38 | const roomId = useSelector((state) => state.room.id); // Gets the current Room id 39 | const elements = useSelector((state) => state.elements); // Gets all the elements 40 | 41 | const pageElements = elements.filter((element) => element.page === page); // Gets all the elements of the current page 42 | const pageBackground = background.filter((bg) => bg.page === page); // Gets the background of the current page 43 | 44 | const [updated, setUpdated] = useState(); 45 | 46 | // Fetches the latest elements and background of the room and initialises it in the Redux Store 47 | useEffect(() => { 48 | console.log("RANN"); 49 | if (roomId === undefined || roomId === null || roomId === "") { 50 | return; 51 | } 52 | onValue( 53 | ref(database, `elements/${roomId}`), 54 | (snapshot) => { 55 | if (!snapshot.exists()) { 56 | return; 57 | } 58 | // Loops through the element list received from the databse and initialises it in Redux 59 | snapshot.forEach((childSnapshot) => { 60 | const childValue = childSnapshot.val(); 61 | dispatch(addElement(childValue)); 62 | }); 63 | }, 64 | { 65 | onlyOnce: true, 66 | } 67 | ); 68 | 69 | onValue( 70 | ref(database, "background/" + roomId), 71 | (snapshot) => { 72 | // Loops through the background list received from the database and initialises it in Redux 73 | snapshot.forEach((childSnapshot) => { 74 | const childValue = childSnapshot.val(); 75 | dispatch(addBackground(childValue)); 76 | }); 77 | }, 78 | { 79 | onlyOnce: true, 80 | } 81 | ); 82 | }, [roomId, dispatch]); 83 | 84 | // UPDATE ELEMENT 85 | useEffect(() => { 86 | const elementsRef = ref(database, "elements/" + roomId); 87 | // Listens to changes in the child elements 88 | onChildChanged(elementsRef, (snapshot) => { 89 | const updatedElement = snapshot.val(); 90 | if (!updatedElement.id) { 91 | return; 92 | } 93 | 94 | // Updates the position and size of the element 95 | const data = { 96 | height: updatedElement.height, 97 | width: updatedElement.width, 98 | x: updatedElement.x, 99 | y: updatedElement.y, 100 | id: updatedElement.id, 101 | }; 102 | dispatch(updateElement(data)); 103 | 104 | // Checks the element type & updates its specific properties 105 | if (updatedElement.type === "text") { 106 | const newText = { 107 | id: updatedElement.id, 108 | content: updatedElement.content, 109 | color: updatedElement.color, 110 | size: updatedElement.size, 111 | font: updatedElement.font, 112 | weight: updatedElement.weight, 113 | align: updatedElement.align, 114 | }; 115 | dispatch(updateTextElement(newText)); 116 | } else if (updatedElement.type === "shape") { 117 | const newShape = { 118 | color: updatedElement.color, 119 | id: updatedElement.id, 120 | }; 121 | dispatch(updateShapeElement(newShape)); 122 | } else if (updatedElement.type === "icon") { 123 | const newIcon = { 124 | color: updatedElement.color, 125 | size: updatedElement.size, 126 | id: updatedElement.id, 127 | }; 128 | dispatch(updateIconElement(newIcon)); 129 | } 130 | setUpdated(data.id); 131 | }); 132 | }, [roomId, dispatch]); 133 | 134 | // ADDS NEW ELEMENT & SYNCS ADDITIONS MADE BY MULTIPLE USERS 135 | useEffect(() => { 136 | const elementsRef = ref(database, "elements/" + roomId); 137 | // Listens to additions in the element list 138 | onChildAdded(elementsRef, (snapshot) => { 139 | const dbRef = ref(database); 140 | get(child(dbRef, `elements/${roomId}/${snapshot.key}`)).then( 141 | (childSnapshot) => { 142 | if (childSnapshot.exists()) { 143 | const data = childSnapshot.val(); 144 | dispatch(addElement(data)); 145 | } else { 146 | console.log("No data available"); 147 | } 148 | } 149 | ); 150 | }); 151 | }, [roomId, dispatch]); 152 | 153 | // REMOVES ELEMENT & SYNCS IT WITH MULTIPLE USERS 154 | useEffect(() => { 155 | const elementsRef = ref(database, "elements/" + roomId); 156 | // Listens to any removal in the elements list 157 | onChildRemoved(elementsRef, (snapshot) => { 158 | const removedElement = snapshot.val(); 159 | const removedId = removedElement.id; 160 | console.log("removed el: " + removedId); 161 | dispatch(removeElement({ id: removedId })); 162 | dispatch(resetSelected()); 163 | }); 164 | }, [roomId, dispatch]); 165 | 166 | // UPDATE BACKGROUND & SYNCS IT WITH MULTIPLE USERS 167 | useEffect(() => { 168 | const bgRef = ref(database, "background/" + roomId); 169 | // Listens to updates in the background list 170 | onChildChanged(bgRef, (snapshot) => { 171 | const updatedBackground = snapshot.val(); 172 | if (!updatedBackground.id) { 173 | return; 174 | } 175 | console.log("UP BG:" + updatedBackground); 176 | const data = { 177 | id: updatedBackground.id, 178 | page: updatedBackground.page, 179 | background: updatedBackground.background, 180 | type: updatedBackground.type, 181 | source: updatedBackground.source, 182 | }; 183 | console.log(data); 184 | dispatch(addBackground(data)); 185 | }); 186 | }, [roomId, dispatch]); 187 | 188 | // If a user clicks on the board then it unselects the "selected" element 189 | const selectHandler = (e) => { 190 | if (e.target !== board.current) { 191 | return; 192 | } 193 | dispatch(resetSelected()); 194 | }; 195 | 196 | const detectKeyDown = (e) => { 197 | console.log(e.key); 198 | 199 | if (e.key !== "Delete" && e.key !== "Backspace") { 200 | return; 201 | } 202 | // Returns if there no selected element 203 | if (!selected) { 204 | console.log("No ID to Delete!"); 205 | return; 206 | } 207 | const deletedId = selected; 208 | console.log("Id to be deleted: ", deletedId); 209 | 210 | const dbRef = ref(database); 211 | // Finds the element in the element list and removes it using it's ID 212 | get(child(dbRef, `elements/${roomId}`)).then((snapshot) => { 213 | if (snapshot.exists()) { 214 | snapshot.forEach((childSnapshot) => { 215 | const childValue = childSnapshot.val(); 216 | if (childValue.id === deletedId) { 217 | console.log("REMOVED!!", childValue.id); 218 | remove(ref(database, `elements/${roomId}/${childSnapshot.key}`)); 219 | } 220 | }); 221 | } else { 222 | console.log("No data available"); 223 | } 224 | }); 225 | 226 | // Removed from Redux 227 | dispatch( 228 | removeElement({ 229 | id: deletedId, 230 | }) 231 | ); 232 | // No selected element 233 | dispatch(resetSelected()); 234 | }; 235 | 236 | return ( 237 |
249 | {pageElements.map((element) => { 250 | return ( 251 | 269 | ); 270 | })} 271 |
272 | ); 273 | }; 274 | 275 | export default Board; 276 | -------------------------------------------------------------------------------- /pages/edit.js: -------------------------------------------------------------------------------- 1 | // Styles 2 | import styles from "../styles/Edit.module.css"; 3 | 4 | // Icons 5 | import { TbSquaresFilled, TbTextResize, TbLayoutGridAdd } from "react-icons/tb"; 6 | import { IoMdImage } from "react-icons/io"; 7 | import { AiOutlineUserAdd, AiOutlinePlus } from "react-icons/ai"; 8 | import { FaShapes } from "react-icons/fa"; 9 | import { FiDownload, FiLogIn } from "react-icons/fi"; 10 | import { RiFolder3Fill } from "react-icons/ri"; 11 | 12 | // React, Next & Redux 13 | import { useRouter } from "next/router"; 14 | import Link from "next/link"; 15 | import Head from "next/head"; 16 | import { useCallback, useEffect, useState } from "react"; 17 | import { useDispatch, useSelector } from "react-redux"; 18 | import { resetSelected } from "../store/selectedElementSlice"; 19 | import { addPage, setPage } from "../store/pageSlice"; 20 | import { 21 | addPageRoom, 22 | setPageRoom, 23 | setRoom, 24 | setRoomName, 25 | } from "../store/roomSlice"; 26 | import { addBackgroundColor } from "../store/backgroundSlice"; 27 | 28 | // Firebase 29 | import { 30 | child, 31 | get, 32 | onChildChanged, 33 | ref, 34 | update, 35 | set, 36 | } from "firebase/database"; 37 | import { database } from "../firebaseConfig"; 38 | 39 | // Other Libraries 40 | import toast, { Toaster } from "react-hot-toast"; 41 | import html2canvas from "html2canvas"; 42 | import { nanoid } from "nanoid"; 43 | 44 | //Components 45 | import Board from "../components/Board"; 46 | import TextForm from "../components/TextForm"; 47 | import BackgroundForm from "../components/BackgroundForm"; 48 | import ImageForm from "../components/ImageForm"; 49 | import ShapesForm from "../components/ShapesForm"; 50 | import CodeModal from "../components/CodeModal"; 51 | import Unsplash from "../components/Unsplash"; 52 | import SmallBoard from "../components/SmallBoard"; 53 | import IconForm from "../components/IconForm"; 54 | 55 | const Edit = () => { 56 | const dispatch = useDispatch(); 57 | const router = useRouter(); 58 | 59 | const [activeNav, setActiveNav] = useState(); // Stores the Active Navigation Components 60 | const [activeButton, setActiveButton] = useState("textNav"); // Stores the id of the Active Nav Component 61 | const [unsplashOpen, setUnsplashOpen] = useState(false); // Unplash Modal State 62 | const [modalOpen, setModalOpen] = useState(false); // Share Code Modal State 63 | const [roomId, setRoomId] = useState(); // Current Room Id 64 | 65 | const selectedElement = useSelector((state) => state.selectedElement); // Current Selected Element By the User 66 | const page = useSelector((state) => state.page); // Active Page and List of Pages 67 | const room = useSelector((state) => state.room); // Current Room info 68 | const user = useSelector((state) => state.user.user); // Current Logged in user 69 | 70 | const navChangeHandler = useCallback( 71 | (element, id) => { 72 | setActiveNav(element); 73 | const button = document.getElementById(id); 74 | const prevButton = document.getElementById(activeButton); 75 | prevButton.classList.remove(`${styles.navButtonActive}`); 76 | button.classList.add(`${styles.navButtonActive}`); 77 | setActiveButton(id); 78 | }, 79 | [activeButton] 80 | ); 81 | 82 | useEffect(() => { 83 | toast("Please Zoom Out on Screen Distortion", { 84 | icon: "🛠", 85 | duration: 4000, 86 | }); 87 | console.log(router.query.q); 88 | // Sets the current room id 89 | dispatch( 90 | setRoom({ 91 | id: router.query.q, 92 | }) 93 | ); 94 | 95 | // Sets the total number of pages of that particular room 96 | const dbRef = ref(database); 97 | get(child(dbRef, `rooms/${router.query.q}`)).then((snapshot) => { 98 | if (snapshot.exists()) { 99 | console.log("PAGES!", snapshot.val().pages); 100 | const numberOfPages = snapshot.val().pages; 101 | const roomName = snapshot.val().name; 102 | dispatch( 103 | setPageRoom({ 104 | pages: numberOfPages, 105 | }) 106 | ); 107 | dispatch( 108 | setPage({ 109 | pages: numberOfPages, 110 | }) 111 | ); 112 | dispatch( 113 | setRoomName({ 114 | name: roomName, 115 | }) 116 | ); 117 | } 118 | }); 119 | 120 | setRoomId({ id: router.query.q }); 121 | }, [dispatch, router.query.q]); 122 | 123 | // Sets the Active Nav according to the Selected Element 124 | useEffect(() => { 125 | if (selectedElement.id === "") { 126 | return; 127 | } else if (selectedElement.type === "text") { 128 | navChangeHandler(, "textNav"); 129 | } else if (selectedElement.type === "shape") { 130 | navChangeHandler(, "shapeNav"); 131 | } else if (selectedElement.type === "image") { 132 | navChangeHandler( 133 | , 134 | "imageNav" 135 | ); 136 | } else if (selectedElement.type === "icon") { 137 | navChangeHandler(, "iconNav"); 138 | } 139 | }, [selectedElement, navChangeHandler]); 140 | 141 | // UPDATE PAGES 142 | useEffect(() => { 143 | const elementsRef = ref(database, `rooms/${router.query.q}`); 144 | onChildChanged(elementsRef, (snapshot) => { 145 | const updatedPage = snapshot.val(); 146 | console.log("Updated Page!: ", updatedPage); 147 | dispatch( 148 | setPage({ 149 | pages: updatedPage, 150 | }) 151 | ); 152 | }); 153 | }, [router.query.q, dispatch, room.pages, page.pages.length]); 154 | 155 | // Adds and syncs new pages across all users of the same room 156 | // Sets the Background of the New Page to white (#FFFFFF) 157 | const addPageHandler = () => { 158 | console.log("ADD PAGE RAN!"); 159 | console.log(room); 160 | const latestPages = room.pages + 1; 161 | console.log("LatestPages: ", latestPages); 162 | dispatch(addPageRoom()); 163 | dispatch(addPage()); 164 | update(ref(database, `rooms/${roomId.id}`), { 165 | pages: latestPages, 166 | }); 167 | const data = { 168 | id: nanoid(), 169 | page: latestPages, 170 | background: "#FFFFFF", 171 | roomId: roomId.id, 172 | source: "color", 173 | }; 174 | 175 | dispatch(addBackgroundColor(data)); 176 | const backgroundRef = ref( 177 | database, 178 | `background/${roomId.id}/${latestPages}` 179 | ); 180 | set(backgroundRef, data); 181 | }; 182 | 183 | // Downloads the Current Page 184 | const downloadHandler = useCallback(async () => { 185 | const board = document.querySelector("#board"); 186 | 187 | const canvas = await html2canvas(board, { 188 | useCORS: true, 189 | allowTaint: true, 190 | }); 191 | const dataURL = canvas.toDataURL("image/png"); 192 | const tmpLink = document.createElement("a"); 193 | tmpLink.download = "image.png"; 194 | tmpLink.href = dataURL; 195 | document.body.appendChild(tmpLink); 196 | tmpLink.click(); 197 | document.body.removeChild(tmpLink); 198 | }, []); 199 | 200 | // Contains info and functions for the all Navigation Buttons 201 | const navButtonsList = [ 202 | { 203 | class: `${styles.navButton} ${styles.navButtonActive}`, 204 | click: () => { 205 | dispatch(resetSelected()); 206 | navChangeHandler(, "textNav"); 207 | }, 208 | id: "textNav", 209 | icon: , 210 | text: "Text", 211 | }, 212 | { 213 | class: `${styles.navButton}`, 214 | click: () => { 215 | dispatch(resetSelected()); 216 | navChangeHandler( 217 | , 218 | "backgroundNav" 219 | ); 220 | }, 221 | id: "backgroundNav", 222 | icon: , 223 | text: "Background", 224 | }, 225 | { 226 | class: `${styles.navButton}`, 227 | click: () => { 228 | dispatch(resetSelected()); 229 | navChangeHandler( 230 | , 231 | "imageNav" 232 | ); 233 | }, 234 | id: "imageNav", 235 | icon: , 236 | text: "Image", 237 | }, 238 | { 239 | class: `${styles.navButton}`, 240 | click: () => { 241 | dispatch(resetSelected()); 242 | navChangeHandler(, "iconNav"); 243 | }, 244 | id: "iconNav", 245 | icon: , 246 | text: "Icons", 247 | }, 248 | { 249 | class: `${styles.navButton}`, 250 | click: () => { 251 | dispatch(resetSelected()); 252 | navChangeHandler(, "shapeNav"); 253 | }, 254 | id: "shapeNav", 255 | icon: , 256 | text: "Shapes", 257 | }, 258 | ]; 259 | 260 | return ( 261 | <> 262 | 263 | {room.name} 264 | 265 | 266 | 276 | 277 |
278 | {unsplashOpen && ( 279 | 283 | )} 284 | {modalOpen && } 285 | 310 |
311 |
312 |
313 |
314 |

#{page.current}

315 | 322 | 329 |
330 | {user ? ( 331 |
332 | 333 | 337 | 338 |
339 | ) : ( 340 |
341 | 342 | 346 | 347 |
348 | )} 349 |
350 | 351 |
352 | {/* Displays all the pages of the room, by displaying its background image */} 353 | {page.pages.map((page) => { 354 | return ; 355 | })} 356 |
357 |
358 |
359 |
360 | 361 | ); 362 | }; 363 | 364 | export default Edit; 365 | --------------------------------------------------------------------------------