├── 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 | 
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 |
78 |
79 | Upload Image
80 |
81 |
88 |
89 |
setUnsplashOpen(true)}
93 | >
94 |
95 | Launch Unsplash
96 |
97 |
Insert Image
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 |
103 | {isShape
104 | ? "Change Color"
105 | : `Insert ${shapeSelected === null ? "" : shapeSelected}`}
106 |
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 |
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 |
119 |
120 | Upload Image
121 |
122 |
129 |
130 |
setUnsplashOpen(true)}
134 | >
135 |
136 | Launch Unsplash
137 |
138 |
139 |
144 | Change Background Color
145 |
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 |
78 |
79 |
86 |
sigma
87 |
88 |
89 |
90 |
91 | Star on GitHub
92 |
93 |
94 |
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 |
106 | Sign Up
107 |
108 |
109 | ) : (
110 |
123 | )}
124 |
setActiveForm((prev) => !prev)}
127 | >
128 | {activeForm ? "or, join a room" : "or, create a room"}
129 |
130 |
131 |
132 |
133 | Star on GitHub
134 |
135 |
136 |
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 |
144 | );
145 | })}
146 |
147 |
148 | {photos.slice(11, 21).map((photo) => {
149 | return (
150 |
158 | );
159 | })}
160 |
161 |
162 | {photos.slice(21, 31).map((photo) => {
163 | return (
164 |
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 |
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 |
140 |
141 |
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 | setNewProject(true)}
167 | >
168 | + Project
169 |
170 | )}
171 | {newProject && (
172 |
173 | Add
174 |
175 | )}
176 | {newProject && (
177 | setNewProject(false)}
180 | >
181 | Cancel
182 |
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 |
210 |
{file.name}
211 |
212 | );
213 | })
214 | ) : (
215 | /* Renders Image to convey that the folder is empty */
216 |
217 |
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 |
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 |
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 | detectKeyDown(e)}
253 | style={{
254 | border: "none",
255 | backgroundColor: "none",
256 | color: "transparent",
257 | height: "0",
258 | width: "0",
259 | padding: "0",
260 | }}
261 | key={element.id}
262 | >
263 |
268 |
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 |
286 | {room.name}
287 | {/* Goes through the nav buttons array and creates a button for each */}
288 | {navButtonsList.map((nav) => {
289 | return (
290 |
296 | {nav.icon}
297 | {nav.text}
298 |
299 | );
300 | })}
301 |
302 |
303 | {activeNav}
304 |
305 |
306 |
307 | Download Page
308 |
309 |
310 |
311 |
312 |
313 |
314 |
#{page.current}
315 |
319 |
320 | Page
321 |
322 |
setModalOpen(true)}
325 | >
326 |
327 | Invite
328 |
329 |
330 | {user ? (
331 |
332 |
333 |
334 |
335 | Folder
336 |
337 |
338 |
339 | ) : (
340 |
341 |
342 |
343 |
344 | Sign Up
345 |
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 |
--------------------------------------------------------------------------------