├── src ├── data │ ├── viewTypes.js │ ├── statusMessages.js │ ├── Theme │ │ └── ThemeOptions.js │ ├── theme │ │ └── themeOptions.js │ ├── OptionTypes.js │ ├── optionTypes.js │ ├── Templates.js │ └── templates.js ├── assets │ ├── bg-1.png │ ├── dots.png │ ├── logo.png │ ├── rsvp.png │ ├── sort.png │ ├── newForm.png │ ├── animation.gif │ ├── eventReg.png │ ├── partyInv.png │ ├── contactInfo.png │ ├── fonts │ │ ├── ProductSans-Black.woff │ │ ├── ProductSans-Bold.woff │ │ ├── ProductSans-Light.woff │ │ ├── ProductSans-Thin.woff │ │ ├── ProductSans-Italic.woff │ │ ├── ProductSans-Medium.woff │ │ ├── ProductSans-Regular.woff │ │ ├── ProductSans-BoldItalic.woff │ │ ├── ProductSans-ThinItalic.woff │ │ ├── ProductSans-BlackItalic.woff │ │ ├── ProductSans-LightItalic.woff │ │ ├── ProductSans-MediumItalic.woff │ │ ├── ThemeFonts.css │ │ └── fonts.css │ ├── listView.svg │ ├── logo2.svg │ └── colors │ │ └── Colors.css ├── components │ ├── form │ │ ├── Settings.js │ │ ├── Responses.js │ │ ├── Edit.js │ │ ├── FormTile │ │ │ └── FormTile.js │ │ └── formTile │ │ │ └── FormTile.js │ ├── helpers │ │ ├── GenerateKey.js │ │ ├── generateKey.js │ │ ├── openInNewTab.js │ │ ├── CreateOption.js │ │ ├── createOption.js │ │ ├── validations.js │ │ ├── GenerateForm.js │ │ ├── generateForm.js │ │ ├── CreateQuestion.js │ │ └── createQuestion.js │ ├── loaders │ │ └── page.loader.js │ ├── buttons │ │ ├── OutLinedButton.js │ │ ├── outLinedButton.js │ │ ├── FilledButton.js │ │ └── filledButton.js │ ├── routes │ │ ├── private.routes.js │ │ └── public.routes.js │ ├── layout │ │ ├── Headers │ │ │ ├── LandingHeader.js │ │ │ ├── HomeHeader.js │ │ │ └── FormHeader.js │ │ ├── headers │ │ │ ├── LandingHeader.js │ │ │ ├── HomeHeader.js │ │ │ └── FormHeader.js │ │ ├── Navigation │ │ │ └── NavBar.js │ │ └── navigation │ │ │ └── NavBar.js │ ├── dropdown │ │ ├── useOutsideAlert.js │ │ ├── Dropdown.js │ │ ├── DropdownwithIcon.js │ │ └── CustomDropdown.js │ ├── slider │ │ ├── Slider.js │ │ ├── slider.js │ │ └── slider.css │ ├── theme │ │ ├── BackgroundColorComponent.js │ │ ├── backgroundColorComponent.js │ │ ├── ColorComponent.js │ │ ├── colorComponent.js │ │ ├── ThemeEditor.js │ │ └── themeEditor.js │ ├── icon │ │ └── Icon.js │ ├── cards │ │ ├── Options │ │ │ ├── displayOptions.js │ │ │ ├── option.js │ │ │ └── individualOption.js │ │ ├── options │ │ │ ├── DisplayOptions.js │ │ │ ├── Option.js │ │ │ └── IndividualOption.js │ │ ├── TitleCard.js │ │ └── QuestionCard.js │ ├── toolbar │ │ ├── ToolBar.js │ │ └── toolBar.js │ └── modals │ │ └── RenameModal.js ├── setupTests.js ├── App.test.js ├── reportWebVitals.js ├── store │ ├── index.js │ ├── data │ │ ├── allForms.slice.js │ │ └── form.slice.js │ └── authentication │ │ └── authentication.slice.js ├── views │ ├── 404.js │ ├── CreateForm.js │ ├── Landing.js │ └── Home.js ├── services │ └── firebase │ │ ├── config.firebase.js │ │ ├── auth.firebase.js │ │ └── firestore.firebase.js ├── index.js ├── index.css └── App.js ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── logo2.svg ├── manifest.json └── index.html ├── jsconfig.json ├── postcss.config.js ├── .env.example ├── .gitignore ├── tailwind.config.js ├── README.md └── package.json /src/data/viewTypes.js: -------------------------------------------------------------------------------- 1 | export const LIST = "List"; 2 | export const GRID = "Grid"; 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/assets/bg-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/src/assets/bg-1.png -------------------------------------------------------------------------------- /src/assets/dots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/src/assets/dots.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/rsvp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/src/assets/rsvp.png -------------------------------------------------------------------------------- /src/assets/sort.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/src/assets/sort.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src" 4 | }, 5 | "include": ["src"] 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/newForm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/src/assets/newForm.png -------------------------------------------------------------------------------- /src/assets/animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/src/assets/animation.gif -------------------------------------------------------------------------------- /src/assets/eventReg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/src/assets/eventReg.png -------------------------------------------------------------------------------- /src/assets/partyInv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/src/assets/partyInv.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/contactInfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/src/assets/contactInfo.png -------------------------------------------------------------------------------- /src/components/form/Settings.js: -------------------------------------------------------------------------------- 1 | const Settings = () => { 2 | return
I am settings
; 3 | }; 4 | 5 | export default Settings; 6 | -------------------------------------------------------------------------------- /src/assets/fonts/ProductSans-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/src/assets/fonts/ProductSans-Black.woff -------------------------------------------------------------------------------- /src/assets/fonts/ProductSans-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/src/assets/fonts/ProductSans-Bold.woff -------------------------------------------------------------------------------- /src/assets/fonts/ProductSans-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/src/assets/fonts/ProductSans-Light.woff -------------------------------------------------------------------------------- /src/assets/fonts/ProductSans-Thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/src/assets/fonts/ProductSans-Thin.woff -------------------------------------------------------------------------------- /src/components/form/Responses.js: -------------------------------------------------------------------------------- 1 | const Responses = () => { 2 | return
I am a response
; 3 | }; 4 | 5 | export default Responses; 6 | -------------------------------------------------------------------------------- /src/assets/fonts/ProductSans-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/src/assets/fonts/ProductSans-Italic.woff -------------------------------------------------------------------------------- /src/assets/fonts/ProductSans-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/src/assets/fonts/ProductSans-Medium.woff -------------------------------------------------------------------------------- /src/assets/fonts/ProductSans-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/src/assets/fonts/ProductSans-Regular.woff -------------------------------------------------------------------------------- /src/assets/fonts/ProductSans-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/src/assets/fonts/ProductSans-BoldItalic.woff -------------------------------------------------------------------------------- /src/assets/fonts/ProductSans-ThinItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/src/assets/fonts/ProductSans-ThinItalic.woff -------------------------------------------------------------------------------- /src/assets/fonts/ProductSans-BlackItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/src/assets/fonts/ProductSans-BlackItalic.woff -------------------------------------------------------------------------------- /src/assets/fonts/ProductSans-LightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/src/assets/fonts/ProductSans-LightItalic.woff -------------------------------------------------------------------------------- /src/assets/fonts/ProductSans-MediumItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thezaeemaanwar/google-forms-clone/HEAD/src/assets/fonts/ProductSans-MediumItalic.woff -------------------------------------------------------------------------------- /src/components/helpers/GenerateKey.js: -------------------------------------------------------------------------------- 1 | const generateKey = (pre) => { 2 | return `${pre}_${new Date().getTime()}`; 3 | }; 4 | 5 | export default generateKey; 6 | -------------------------------------------------------------------------------- /src/components/helpers/generateKey.js: -------------------------------------------------------------------------------- 1 | const generateKey = (pre) => { 2 | return `${pre}_${new Date().getTime()}`; 3 | }; 4 | 5 | export default generateKey; 6 | -------------------------------------------------------------------------------- /src/components/helpers/openInNewTab.js: -------------------------------------------------------------------------------- 1 | const openInNewTab = (url) => { 2 | const win = window.open(url, "_blank"); 3 | win.focus(); 4 | }; 5 | 6 | export default openInNewTab; 7 | -------------------------------------------------------------------------------- /src/assets/listView.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/fonts/ThemeFonts.css: -------------------------------------------------------------------------------- 1 | .basic-text { 2 | @apply font-basic; 3 | } 4 | .decorative-text { 5 | @apply font-decorative; 6 | } 7 | .playful-text { 8 | @apply font-playful; 9 | } 10 | .formal-text { 11 | @apply font-formal; 12 | } 13 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/components/helpers/CreateOption.js: -------------------------------------------------------------------------------- 1 | import generateKey from "components/helpers/generateKey"; 2 | 3 | const createOption = (pre) => { 4 | const option = { id: generateKey("option" + pre), text: "Option" }; 5 | return option; 6 | }; 7 | 8 | export default createOption; 9 | -------------------------------------------------------------------------------- /src/components/helpers/createOption.js: -------------------------------------------------------------------------------- 1 | import generateKey from "components/helpers/generateKey"; 2 | 3 | const createOption = (pre) => { 4 | const option = { id: generateKey("option" + pre), text: "Option" }; 5 | return option; 6 | }; 7 | 8 | export default createOption; 9 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | import App from "App"; 3 | 4 | test("renders learn react link", () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /public/logo2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/logo2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/components/loaders/page.loader.js: -------------------------------------------------------------------------------- 1 | import anim from "assets/animation.gif"; 2 | 3 | const Loading = () => { 4 | return ( 5 |
6 | loading... 7 |
8 | ); 9 | }; 10 | 11 | export default Loading; 12 | -------------------------------------------------------------------------------- /src/components/buttons/OutLinedButton.js: -------------------------------------------------------------------------------- 1 | const OutLinedButton = ({ color, text, onClick }) => { 2 | return ( 3 | 9 | ); 10 | }; 11 | 12 | export default OutLinedButton; 13 | -------------------------------------------------------------------------------- /src/components/buttons/outLinedButton.js: -------------------------------------------------------------------------------- 1 | const OutLinedButton = ({ color, text, onClick }) => { 2 | return ( 3 | 9 | ); 10 | }; 11 | 12 | export default OutLinedButton; 13 | -------------------------------------------------------------------------------- /src/data/statusMessages.js: -------------------------------------------------------------------------------- 1 | export const ERR_NOT_AUTHORISED = "User Not Authorised"; 2 | export const ERR_NOT_FOUND = "Data not found"; 3 | export const ERR_INVALID_DATA = "Invalid Data"; 4 | export const SUCCESS_SAVED = "All changes saved in Drive"; 5 | export const PROGRESS_SAVING = "Saving..."; 6 | export const ERR_SAVING_FAILED = "Couldn't update in Drive"; 7 | -------------------------------------------------------------------------------- /src/components/buttons/FilledButton.js: -------------------------------------------------------------------------------- 1 | const FilledButton = ({ color, background, text, onClick }) => { 2 | return ( 3 | 9 | ); 10 | }; 11 | 12 | export default FilledButton; 13 | -------------------------------------------------------------------------------- /src/components/buttons/filledButton.js: -------------------------------------------------------------------------------- 1 | const FilledButton = ({ color, background, text, onClick }) => { 2 | return ( 3 | 9 | ); 10 | }; 11 | 12 | export default FilledButton; 13 | -------------------------------------------------------------------------------- /src/components/helpers/validations.js: -------------------------------------------------------------------------------- 1 | import * as Yup from "yup"; 2 | 3 | const headerSchema = Yup.object().shape({ 4 | title: Yup.string().required("Title is required"), 5 | }); 6 | 7 | const questionSchema = Yup.object().shape({ 8 | title: Yup.string().required("Question title is required"), 9 | }); 10 | 11 | export { questionSchema, headerSchema }; 12 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_API_KEY=your_key_given_by_firebase 2 | REACT_APP_AUTH_DOMAIN=your_key_given_by_firebase 3 | REACT_APP_PROJECT_ID=your_key_given_by_firebase 4 | REACT_APP_STORAGE_BUCKET=your_key_given_by_firebase 5 | REACT_APP_MESSAGING_SENDER_ID=your_key_given_by_firebase 6 | REACT_APP_APP_ID=your_key_given_by_firebase 7 | REACT_APP_MEASUREMENT_ID=your_key_given_by_firebase 8 | -------------------------------------------------------------------------------- /.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import authReducer from "store/authentication/authentication.slice"; 3 | import formReducer from "store/data/form.slice"; 4 | import allFormSReducder from "store/data/allForms.slice"; 5 | 6 | export default configureStore({ 7 | reducer: { 8 | authentication: authReducer, 9 | form: formReducer, 10 | allForms: allFormSReducder, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /src/components/helpers/GenerateForm.js: -------------------------------------------------------------------------------- 1 | const generateFormPreview = (id, title, img, date, shared) => { 2 | return { id, title, img: img ? img : "", date, shared }; 3 | }; 4 | 5 | const generateForm = ( 6 | id, 7 | theme, 8 | title, 9 | description, 10 | questions, 11 | date, 12 | shared 13 | ) => { 14 | return { id, theme, title, description, questions, date, shared }; 15 | }; 16 | 17 | export { generateForm, generateFormPreview }; 18 | -------------------------------------------------------------------------------- /src/components/helpers/generateForm.js: -------------------------------------------------------------------------------- 1 | const generateFormPreview = (id, title, img, date, shared) => { 2 | return { id, title, img: img ? img : "", date, shared }; 3 | }; 4 | 5 | const generateForm = ( 6 | id, 7 | theme, 8 | title, 9 | description, 10 | questions, 11 | date, 12 | shared 13 | ) => { 14 | return { id, theme, title, description, questions, date, shared }; 15 | }; 16 | 17 | export { generateForm, generateFormPreview }; 18 | -------------------------------------------------------------------------------- /src/components/routes/private.routes.js: -------------------------------------------------------------------------------- 1 | import { Navigate, useLocation } from "react-router-dom"; 2 | import { useSelector } from "react-redux"; 3 | 4 | const PrivateRoute = ({ children }) => { 5 | const location = useLocation(); 6 | const { logged } = useSelector((state) => state.authentication); 7 | return logged ? ( 8 | children 9 | ) : ( 10 | 11 | ); 12 | }; 13 | 14 | export default PrivateRoute; 15 | -------------------------------------------------------------------------------- /src/data/Theme/ThemeOptions.js: -------------------------------------------------------------------------------- 1 | export const colors = [ 2 | "red", 3 | "purple", 4 | "indigo", 5 | "blue", 6 | "lightblue", 7 | "cyan", 8 | "redorange", 9 | "orange", 10 | "teal", 11 | "green", 12 | "bluegray", 13 | "gray", 14 | ]; 15 | 16 | export const opacities = [10, 20, 30, 0]; 17 | 18 | export const fonts = [ 19 | { id: 1, text: "basic" }, 20 | { id: 2, text: "decorative" }, 21 | { id: 3, text: "formal" }, 22 | { id: 4, text: "playful" }, 23 | ]; 24 | -------------------------------------------------------------------------------- /src/data/theme/themeOptions.js: -------------------------------------------------------------------------------- 1 | export const colors = [ 2 | "red", 3 | "purple", 4 | "indigo", 5 | "blue", 6 | "lightblue", 7 | "cyan", 8 | "redorange", 9 | "orange", 10 | "teal", 11 | "green", 12 | "bluegray", 13 | "gray", 14 | ]; 15 | 16 | export const opacities = [10, 20, 30, 0]; 17 | 18 | export const fonts = [ 19 | { id: 1, text: "basic" }, 20 | { id: 2, text: "decorative" }, 21 | { id: 3, text: "formal" }, 22 | { id: 4, text: "playful" }, 23 | ]; 24 | -------------------------------------------------------------------------------- /src/views/404.js: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | const NotFound = () => { 4 | return ( 5 |
6 |
Page not found
7 |
8 | Click{" "} 9 | 10 | here 11 | {" "} 12 | to return to homepage. 13 |
14 |
15 | ); 16 | }; 17 | 18 | export default NotFound; 19 | -------------------------------------------------------------------------------- /src/components/helpers/CreateQuestion.js: -------------------------------------------------------------------------------- 1 | import generateKey from "components/helpers/generateKey"; 2 | import { MULTIPLE_CHOICE } from "data/optionTypes"; 3 | import createOption from "components/helpers/createOption"; 4 | 5 | const createQuestion = (len) => { 6 | const question = { 7 | id: generateKey("question" + len), 8 | title: "Question", 9 | optionType: MULTIPLE_CHOICE, 10 | options: [createOption(0)], 11 | required: false, 12 | }; 13 | return question; 14 | }; 15 | export default createQuestion; 16 | -------------------------------------------------------------------------------- /src/components/helpers/createQuestion.js: -------------------------------------------------------------------------------- 1 | import generateKey from "components/helpers/generateKey"; 2 | import { MULTIPLE_CHOICE } from "data/optionTypes"; 3 | import createOption from "components/helpers/createOption"; 4 | 5 | const createQuestion = (len) => { 6 | const question = { 7 | id: generateKey("question" + len), 8 | title: "Question", 9 | optionType: MULTIPLE_CHOICE, 10 | options: [createOption(0)], 11 | required: false, 12 | }; 13 | return question; 14 | }; 15 | export default createQuestion; 16 | -------------------------------------------------------------------------------- /src/components/layout/Headers/LandingHeader.js: -------------------------------------------------------------------------------- 1 | import logo from "assets/logo.png"; 2 | 3 | const LandingHeader = () => { 4 | return ( 5 |
6 |
7 | logo 8 |
Google
9 |
Forms
10 |
11 |
12 | ); 13 | }; 14 | 15 | export default LandingHeader; 16 | -------------------------------------------------------------------------------- /src/components/layout/headers/LandingHeader.js: -------------------------------------------------------------------------------- 1 | import logo from "assets/logo.png"; 2 | 3 | const LandingHeader = () => { 4 | return ( 5 |
6 |
7 | logo 8 |
Google
9 |
Forms
10 |
11 |
12 | ); 13 | }; 14 | 15 | export default LandingHeader; 16 | -------------------------------------------------------------------------------- /src/components/dropdown/useOutsideAlert.js: -------------------------------------------------------------------------------- 1 | const { useEffect } = require("react"); 2 | 3 | const useOutsideAlert = (ref, actionCallback) => { 4 | useEffect(() => { 5 | function handleClickOutside(event) { 6 | if (ref.current && !ref.current.contains(event.target)) { 7 | actionCallback(); 8 | } 9 | } 10 | document.addEventListener("mousedown", handleClickOutside); 11 | return () => { 12 | document.removeEventListener("mousedown", handleClickOutside); 13 | }; 14 | }, [ref, actionCallback]); 15 | }; 16 | 17 | export default useOutsideAlert; 18 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/components/slider/Slider.js: -------------------------------------------------------------------------------- 1 | import "components/slider/slider.css"; 2 | import { PropTypes } from "prop-types"; 3 | 4 | const Slider = ({ theme, required, toggleRequired }) => { 5 | return ( 6 | 14 | ); 15 | }; 16 | 17 | Slider.propTypes = { 18 | toggleRequired: PropTypes.func.isRequired, 19 | }; 20 | 21 | export default Slider; 22 | -------------------------------------------------------------------------------- /src/components/slider/slider.js: -------------------------------------------------------------------------------- 1 | import "components/slider/slider.css"; 2 | import { PropTypes } from "prop-types"; 3 | 4 | const Slider = ({ theme, required, toggleRequired }) => { 5 | return ( 6 | 14 | ); 15 | }; 16 | 17 | Slider.propTypes = { 18 | toggleRequired: PropTypes.func.isRequired, 19 | }; 20 | 21 | export default Slider; 22 | -------------------------------------------------------------------------------- /src/store/data/allForms.slice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | export const allFormsSlice = createSlice({ 4 | name: "allForms", 5 | initialState: { 6 | loading: true, 7 | forms: [], 8 | error: null, 9 | }, 10 | reducers: { 11 | setForms: (state, action) => { 12 | state.forms = action.payload.forms; 13 | state.error = action.payload.error; 14 | }, 15 | setLoading: (state, action) => { 16 | state.loading = action.payload.loading; 17 | }, 18 | }, 19 | }); 20 | 21 | export const { setForms, setLoading, forms, loading } = allFormsSlice.actions; 22 | export default allFormsSlice.reducer; 23 | -------------------------------------------------------------------------------- /src/components/theme/BackgroundColorComponent.js: -------------------------------------------------------------------------------- 1 | import { opacities } from "data/theme/themeOptions"; 2 | import ColorComponent from "components/theme/colorComponent"; 3 | 4 | const BackgroundColorComponent = ({ color, opacity, setOpacity }) => { 5 | return ( 6 |
7 | {opacities.map((op) => ( 8 | setOpacity(op)} 11 | color={`${color + op}`} 12 | selected={opacity === op} 13 | border={op === 0 || op === 10} 14 | /> 15 | ))} 16 |
17 | ); 18 | }; 19 | 20 | export default BackgroundColorComponent; 21 | -------------------------------------------------------------------------------- /src/components/theme/backgroundColorComponent.js: -------------------------------------------------------------------------------- 1 | import { opacities } from "data/theme/themeOptions"; 2 | import ColorComponent from "components/theme/colorComponent"; 3 | 4 | const BackgroundColorComponent = ({ color, opacity, setOpacity }) => { 5 | return ( 6 |
7 | {opacities.map((op) => ( 8 | setOpacity(op)} 11 | color={`${color + op}`} 12 | selected={opacity === op} 13 | border={op === 0 || op === 10} 14 | /> 15 | ))} 16 |
17 | ); 18 | }; 19 | 20 | export default BackgroundColorComponent; 21 | -------------------------------------------------------------------------------- /src/components/icon/Icon.js: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 2 | import { PropTypes } from "prop-types"; 3 | 4 | const Icon = ({ label, icon, onClick }) => { 5 | return ( 6 |
11 | 12 |
13 | ); 14 | }; 15 | 16 | Icon.propTypes = { 17 | label: PropTypes.string, 18 | icon: PropTypes.object.isRequired, 19 | onClick: PropTypes.func, 20 | }; 21 | 22 | Icon.defaultProps = { 23 | label: "icon", 24 | }; 25 | export default Icon; 26 | -------------------------------------------------------------------------------- /src/services/firebase/config.firebase.js: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | import { getFirestore } from "firebase/firestore"; 3 | 4 | const firebaseConfig = { 5 | apiKey: process.env.REACT_APP_API_KEY, 6 | authDomain: process.env.REACT_APP_AUTH_DOMAIN, 7 | projectId: process.env.REACT_APP_PROJECT_ID, 8 | storageBucket: process.env.REACT_APP_STORAGE_BUCKET, 9 | messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID, 10 | appId: process.env.REACT_APP_APP_ID, 11 | measurementId: process.env.REACT_APP_MEASUREMENT_ID, 12 | }; 13 | 14 | // Initialize Firebase 15 | const firebase_app = initializeApp(firebaseConfig); 16 | export const db = getFirestore(firebase_app); 17 | export default firebase_app; 18 | -------------------------------------------------------------------------------- /src/components/cards/Options/displayOptions.js: -------------------------------------------------------------------------------- 1 | import { CHECKBOX, MULTIPLE_CHOICE } from "data/optionTypes"; 2 | 3 | const DisplayOptions = ({ type, options }) => { 4 | if (type === MULTIPLE_CHOICE || type === CHECKBOX) 5 | return ( 6 |
7 | {options.map((op) => ( 8 |
9 | 14 |
15 | {op.text} 16 |
17 |
18 | ))} 19 |
20 | ); 21 | else return
; 22 | }; 23 | 24 | export default DisplayOptions; 25 | -------------------------------------------------------------------------------- /src/components/cards/options/DisplayOptions.js: -------------------------------------------------------------------------------- 1 | import { CHECKBOX, MULTIPLE_CHOICE } from "data/optionTypes"; 2 | 3 | const DisplayOptions = ({ type, options }) => { 4 | if (type === MULTIPLE_CHOICE || type === CHECKBOX) 5 | return ( 6 |
7 | {options.map((op) => ( 8 |
9 | 14 |
15 | {op.text} 16 |
17 |
18 | ))} 19 |
20 | ); 21 | else return
; 22 | }; 23 | 24 | export default DisplayOptions; 25 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "index.css"; 4 | import App from "App"; 5 | import reportWebVitals from "reportWebVitals"; 6 | import { Provider } from "react-redux"; 7 | import { BrowserRouter as Router } from "react-router-dom"; 8 | import store from "store/index"; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | 14 | 15 | 16 | 17 | , 18 | document.getElementById("root") 19 | ); 20 | 21 | // If you want to start measuring performance in your app, pass a function 22 | // to log results (for example: reportWebVitals(console.log)) 23 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 24 | reportWebVitals(); 25 | -------------------------------------------------------------------------------- /src/store/authentication/authentication.slice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | export const authSlice = createSlice({ 4 | name: "authentication", 5 | initialState: { 6 | loading: true, 7 | logged: false, 8 | user: null, 9 | error: null, 10 | }, 11 | reducers: { 12 | setUser: (state, action) => { 13 | state.loading = false; 14 | action.payload.user ? (state.logged = true) : (state.logged = false); 15 | state.user = action.payload.user; 16 | state.error = action.payload.error; 17 | }, 18 | startLoading: (state) => { 19 | state.loading = true; 20 | }, 21 | stopLoading: (state) => { 22 | state.loading = false; 23 | }, 24 | }, 25 | }); 26 | 27 | export const { startLoading, stopLoading, setUser } = authSlice.actions; 28 | export default authSlice.reducer; 29 | -------------------------------------------------------------------------------- /src/components/toolbar/ToolBar.js: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 2 | import { toolBarActions } from "data/templates"; 3 | import PropTypes from "prop-types"; 4 | 5 | const ToolBar = ({ addQuestion }) => { 6 | return ( 7 |
10 | {toolBarActions.map((action) => ( 11 |
12 | {}} 16 | /> 17 |
18 | ))} 19 |
20 | ); 21 | }; 22 | 23 | ToolBar.propTypes = { 24 | addQuestion: PropTypes.func.isRequired, 25 | }; 26 | 27 | export default ToolBar; 28 | -------------------------------------------------------------------------------- /src/components/toolbar/toolBar.js: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 2 | import { toolBarActions } from "data/templates"; 3 | import PropTypes from "prop-types"; 4 | 5 | const ToolBar = ({ addQuestion }) => { 6 | return ( 7 |
10 | {toolBarActions.map((action) => ( 11 |
12 | {}} 16 | /> 17 |
18 | ))} 19 |
20 | ); 21 | }; 22 | 23 | ToolBar.propTypes = { 24 | addQuestion: PropTypes.func.isRequired, 25 | }; 26 | 27 | export default ToolBar; 28 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400;1,500;1,600&display=swap"); 2 | @import url("https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@300&family=Parisienne&family=Patrick+Hand&display=swap"); 3 | @import "assets/fonts/fonts.css"; 4 | @import "assets/colors/Colors.css"; 5 | @import "assets/fonts/ThemeFonts.css"; 6 | 7 | @tailwind base; 8 | @tailwind components; 9 | @tailwind utilities; 10 | 11 | @layer base { 12 | body { 13 | @apply text-fontGrey; 14 | @apply font-regular; 15 | font-weight: 500; 16 | } 17 | } 18 | 19 | input[type="checkbox"], 20 | input[type="radio"] { 21 | /* Double-sized Checkboxes */ 22 | -ms-transform: scale(1.5); /* IE */ 23 | -moz-transform: scale(1.5); /* FF */ 24 | -webkit-transform: scale(1.5); /* Safari and Chrome */ 25 | -o-transform: scale(1.5); /* Opera */ 26 | transform: scale(1.5); 27 | padding: 10px; 28 | } 29 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 3 | theme: { 4 | extend: { 5 | colors: { 6 | blue: "#1A73E8", 7 | grey: "#F1F3F4", 8 | hoverGrey: "#DDE1E2", 9 | fontGrey: "#5f6368", 10 | purple: "#7248B9", 11 | indigo: "#3F51B5", 12 | red: "#DB4437", 13 | lightblue: "#03A9F4", 14 | cyan: "#00BCD4", 15 | redorange: "#FF5722", 16 | orange: "#FF9800", 17 | teal: "#009688", 18 | green: "#4CAF50", 19 | bluegray: "#607D8B", 20 | gray: "#9E9E9E", 21 | }, 22 | fontFamily: { 23 | regular: ["Product Sans Regular"], 24 | bold: ["Product Sans Bold"], 25 | thin: ["Product Sans Thin Regular"], 26 | basic: ["Product Sans Regular"], 27 | decorative: ["Parisienne", "cursive"], 28 | playful: ["Patrick Hand", "cursive"], 29 | formal: ["Cormorant Garamond", "serif"], 30 | }, 31 | }, 32 | }, 33 | plugins: [], 34 | }; 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Forms Clone 2 | This is an attempt of creating google forms clone using React.js and Firebase. 3 | 4 | ## Getting Started 5 | ### 1. Clone the repository: 6 | To clone the repository, run the command 7 |
git clone https://github.com/thezaeemaanwar/google-forms-clone
8 | 9 | ### 2. Install the dependencies: 10 | To install the dependencies, run 11 |
npm install
12 | OR 13 |
yarn install
14 | Based on whichever package manager suits you 15 | 16 | ### 3. Set up Firebase: 17 | - Create an account on firebase, if already, create a web project. 18 | - Copy all the keys from the firebase project config. 19 | 20 | ### 4. Set Up environment variables: 21 | - Create a file with name ```.env``` in the root directory 22 | - Copy the contents of ```.env.example``` and paste into ```.env``` file. 23 | - Replace all the keys with your keys obtained by firebase 24 | 25 | ### 5. Start the development Server: 26 | To start the development server, run 27 |
npm start
28 | OR 29 |
yarn start
30 | Whichever you used before for installing dependencies. 31 | -------------------------------------------------------------------------------- /src/services/firebase/auth.firebase.js: -------------------------------------------------------------------------------- 1 | import firebase_app from "services/firebase/config.firebase"; 2 | import { getAuth, onAuthStateChanged } from "firebase/auth"; 3 | import { 4 | signInWithRedirect, 5 | GoogleAuthProvider, 6 | signOut as firebaseSignOut, 7 | } from "firebase/auth"; 8 | 9 | const SignIn = (dispatchCallback) => { 10 | const auth = getAuth(); 11 | const provider = new GoogleAuthProvider(); 12 | signInWithRedirect(auth, provider) 13 | .then((re) => { 14 | dispatchCallback({ user: re.user }); 15 | }) 16 | .catch((err) => { 17 | console.error(err); 18 | dispatchCallback({ error: err }); 19 | }); 20 | }; 21 | 22 | const checkLogged = (dispatchCallback) => { 23 | const auth = getAuth(); 24 | onAuthStateChanged(auth, (user) => { 25 | if (user) { 26 | dispatchCallback({ 27 | uid: user.uid, 28 | displayName: user.displayName, 29 | profileImage: user.photoURL, 30 | }); 31 | } else { 32 | dispatchCallback(null); 33 | } 34 | }); 35 | }; 36 | 37 | const SignOut = (dispatchCallback) => { 38 | const auth = getAuth(); 39 | firebaseSignOut(auth); 40 | dispatchCallback(null); 41 | }; 42 | 43 | export { SignIn, SignOut, checkLogged }; 44 | -------------------------------------------------------------------------------- /src/components/theme/ColorComponent.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import { faCheck } from "@fortawesome/free-solid-svg-icons"; 4 | 5 | const ColorComponent = ({ selected, color, selectColor, border }) => { 6 | return ( 7 |
selectColor(color)} 9 | onClick={selectColor} 10 | className={`cursor-pointer ${ 11 | selected ? "h-8 w-8" : "h-7 w-7" 12 | } rounded-full ${color}-bg flex items-center justify-center ${ 13 | border ? "border" : null 14 | } hover:shadow-lg hover:w-8 hover:h-8`} 15 | > 16 | {selected ? ( 17 | 28 | ) : null} 29 |
30 | ); 31 | }; 32 | 33 | ColorComponent.defaultProps = { 34 | selected: false, 35 | }; 36 | ColorComponent.propTypes = { 37 | selected: PropTypes.bool, 38 | color: PropTypes.string.isRequired, 39 | }; 40 | 41 | export default ColorComponent; 42 | -------------------------------------------------------------------------------- /src/components/theme/colorComponent.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import { faCheck } from "@fortawesome/free-solid-svg-icons"; 4 | 5 | const ColorComponent = ({ selected, color, selectColor, border }) => { 6 | return ( 7 |
selectColor(color)} 9 | onClick={selectColor} 10 | className={`cursor-pointer ${ 11 | selected ? "h-8 w-8" : "h-7 w-7" 12 | } rounded-full ${color}-bg flex items-center justify-center ${ 13 | border ? "border" : null 14 | } hover:shadow-lg hover:w-8 hover:h-8`} 15 | > 16 | {selected ? ( 17 | 28 | ) : null} 29 |
30 | ); 31 | }; 32 | 33 | ColorComponent.defaultProps = { 34 | selected: false, 35 | }; 36 | ColorComponent.propTypes = { 37 | selected: PropTypes.bool, 38 | color: PropTypes.string.isRequired, 39 | }; 40 | 41 | export default ColorComponent; 42 | -------------------------------------------------------------------------------- /src/components/slider/slider.css: -------------------------------------------------------------------------------- 1 | .switch { 2 | position: relative; 3 | /* display: inline-block; */ 4 | width: 37px; 5 | height: 20px; 6 | } 7 | 8 | .switch input { 9 | opacity: 0; 10 | width: 0; 11 | height: 0; 12 | } 13 | 14 | .slider { 15 | height: 14px; 16 | position: absolute; 17 | cursor: pointer; 18 | border-radius: 34px; 19 | top: 0; 20 | left: 0; 21 | right: 0; 22 | bottom: 0; 23 | background-color: #b9b9b9; 24 | -webkit-transition: 0.4s; 25 | transition: 0.4s; 26 | /* @apply bg-grey */ 27 | } 28 | 29 | .slider:before { 30 | position: absolute; 31 | content: ""; 32 | top: -3px; 33 | height: 20px; 34 | width: 20px; 35 | /* left: -4px; */ 36 | bottom: 4px; 37 | background-color: white; 38 | box-shadow: 0px 1px 3px rgb(0 0 0 / 40%); 39 | -webkit-transition: 0.4s; 40 | border-radius: 50%; 41 | transition: 0.4s; 42 | } 43 | 44 | input:checked + .slider { 45 | background-color: #8650ee; 46 | } 47 | 48 | input:focus + .slider { 49 | box-shadow: 0 0 1px #3196f3; 50 | } 51 | 52 | input:checked + .slider:before { 53 | -webkit-transform: translateX(17px); 54 | -ms-transform: translateX(17px); 55 | transform: translateX(17px); 56 | } 57 | 58 | /* Rounded sliders */ 59 | .slider.round { 60 | border-radius: 34px; 61 | } 62 | 63 | .slider.round:before { 64 | border-radius: 50%; 65 | } 66 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from "react-redux"; 2 | import PublicRoutes from "components/routes/public.routes"; 3 | import Loading from "components/loaders/page.loader"; 4 | import { useEffect } from "react"; 5 | import { checkLogged } from "services/firebase/auth.firebase"; 6 | import { setUser } from "store/authentication/authentication.slice"; 7 | import { getFormsFromFirebase } from "services/firebase/firestore.firebase"; 8 | import { setForms, setLoading } from "store/data/allForms.slice"; 9 | 10 | const App = () => { 11 | const dispatch = useDispatch(); 12 | const { logged, loading, user } = useSelector( 13 | (state) => state.authentication 14 | ); 15 | 16 | useEffect(() => { 17 | const dispatchCallback = (user) => { 18 | dispatch(setUser({ user })); 19 | }; 20 | checkLogged(dispatchCallback); 21 | }, [dispatch]); 22 | 23 | useEffect(() => { 24 | dispatch(setLoading(true)); 25 | const dispatchCallback = (forms) => { 26 | dispatch(setForms({ forms })); 27 | }; 28 | const loadDispatch = () => dispatch(setLoading(false)); 29 | if (user) { 30 | getFormsFromFirebase(user.uid, dispatchCallback, loadDispatch); 31 | } 32 | }, [user, dispatch]); 33 | 34 | if (loading) return ; 35 | else 36 | return ( 37 |
38 | 39 |
40 | ); 41 | }; 42 | 43 | export default App; 44 | -------------------------------------------------------------------------------- /src/data/OptionTypes.js: -------------------------------------------------------------------------------- 1 | import { 2 | faAlignLeft, 3 | faCalendarDay, 4 | faCheckSquare, 5 | faChevronDown, 6 | faCloudUploadAlt, 7 | faEllipsisH, 8 | faGripHorizontal, 9 | } from "@fortawesome/free-solid-svg-icons"; 10 | import { faDotCircle, faClock } from "@fortawesome/free-regular-svg-icons"; 11 | 12 | export const SHORT_ANSWER = "Short answer"; 13 | export const PARAGRAPH = "Paragraph"; 14 | export const MULTIPLE_CHOICE = "Multiple choice"; 15 | export const CHECKBOX = "Checkbox"; 16 | export const DROPDOWN = "Dropdown"; 17 | export const FILE_UPLOAD = "File upload"; 18 | export const LINEAR_SCALE = "Linear scale"; 19 | export const MULTIPLE_CHOICE_GRID = "Multiple choice grid"; 20 | export const CHECKBOX_GRID = "Checkbox grid"; 21 | export const DATE = "Date"; 22 | export const TIME = "Time"; 23 | export const dropdownOptions = [ 24 | { id: 1, text: SHORT_ANSWER, icon: faAlignLeft }, 25 | { id: 2, text: PARAGRAPH, icon: faAlignLeft }, 26 | { id: 3, text: MULTIPLE_CHOICE, icon: faDotCircle }, 27 | { id: 4, text: CHECKBOX, icon: faCheckSquare }, 28 | { id: 5, text: DROPDOWN, icon: faChevronDown }, 29 | { id: 6, text: FILE_UPLOAD, icon: faCloudUploadAlt }, 30 | { id: 7, text: LINEAR_SCALE, icon: faEllipsisH }, 31 | { id: 8, text: MULTIPLE_CHOICE_GRID, icon: faGripHorizontal }, 32 | { id: 9, text: CHECKBOX_GRID, icon: faGripHorizontal }, 33 | { id: 10, text: DATE, icon: faCalendarDay }, 34 | { id: 11, text: TIME, icon: faClock }, 35 | ]; 36 | -------------------------------------------------------------------------------- /src/data/optionTypes.js: -------------------------------------------------------------------------------- 1 | import { 2 | faAlignLeft, 3 | faCalendarDay, 4 | faCheckSquare, 5 | faChevronDown, 6 | faCloudUploadAlt, 7 | faEllipsisH, 8 | faGripHorizontal, 9 | } from "@fortawesome/free-solid-svg-icons"; 10 | import { faDotCircle, faClock } from "@fortawesome/free-regular-svg-icons"; 11 | 12 | export const SHORT_ANSWER = "Short answer"; 13 | export const PARAGRAPH = "Paragraph"; 14 | export const MULTIPLE_CHOICE = "Multiple choice"; 15 | export const CHECKBOX = "Checkbox"; 16 | export const DROPDOWN = "Dropdown"; 17 | export const FILE_UPLOAD = "File upload"; 18 | export const LINEAR_SCALE = "Linear scale"; 19 | export const MULTIPLE_CHOICE_GRID = "Multiple choice grid"; 20 | export const CHECKBOX_GRID = "Checkbox grid"; 21 | export const DATE = "Date"; 22 | export const TIME = "Time"; 23 | export const dropdownOptions = [ 24 | { id: 1, text: SHORT_ANSWER, icon: faAlignLeft }, 25 | { id: 2, text: PARAGRAPH, icon: faAlignLeft }, 26 | { id: 3, text: MULTIPLE_CHOICE, icon: faDotCircle }, 27 | { id: 4, text: CHECKBOX, icon: faCheckSquare }, 28 | { id: 5, text: DROPDOWN, icon: faChevronDown }, 29 | { id: 6, text: FILE_UPLOAD, icon: faCloudUploadAlt }, 30 | { id: 7, text: LINEAR_SCALE, icon: faEllipsisH }, 31 | { id: 8, text: MULTIPLE_CHOICE_GRID, icon: faGripHorizontal }, 32 | { id: 9, text: CHECKBOX_GRID, icon: faGripHorizontal }, 33 | { id: 10, text: DATE, icon: faCalendarDay }, 34 | { id: 11, text: TIME, icon: faClock }, 35 | ]; 36 | -------------------------------------------------------------------------------- /src/components/layout/Navigation/NavBar.js: -------------------------------------------------------------------------------- 1 | import { NavLink } from "react-router-dom"; 2 | import { PropTypes } from "prop-types"; 3 | import { useSelector } from "react-redux"; 4 | 5 | const NavBar = ({ type }) => { 6 | const { theme } = useSelector((state) => state.form); 7 | return ( 8 |
9 | 12 | nav.isActive 13 | ? `${theme.color}-border ${theme.color}-text border-b-4 p-2 px-4` 14 | : "border-b-4 p-2 px-4 border-white" 15 | } 16 | > 17 | Questions 18 | 19 | 22 | nav.isActive 23 | ? `${theme.color}-border ${theme.color}-text border-b-4 p-2 px-4 ` 24 | : "border-b-4 p-2 px-4 border-white" 25 | } 26 | > 27 | Responses 28 | 29 | 32 | nav.isActive 33 | ? `${theme.color}-border ${theme.color}-text border-b-4 p-2 px-4` 34 | : "border-b-4 p-2 px-4 border-white" 35 | } 36 | > 37 | Settings 38 | 39 |
40 | ); 41 | }; 42 | 43 | NavBar.defaultProps = { 44 | type: "blank", 45 | }; 46 | NavBar.propTypes = { 47 | type: PropTypes.string, 48 | }; 49 | export default NavBar; 50 | -------------------------------------------------------------------------------- /src/components/layout/navigation/NavBar.js: -------------------------------------------------------------------------------- 1 | import { NavLink } from "react-router-dom"; 2 | import { PropTypes } from "prop-types"; 3 | import { useSelector } from "react-redux"; 4 | 5 | const NavBar = ({ type }) => { 6 | const { theme } = useSelector((state) => state.form); 7 | return ( 8 |
9 | 12 | nav.isActive 13 | ? `${theme.color}-border ${theme.color}-text border-b-4 p-2 px-4` 14 | : "border-b-4 p-2 px-4 border-white" 15 | } 16 | > 17 | Questions 18 | 19 | 22 | nav.isActive 23 | ? `${theme.color}-border ${theme.color}-text border-b-4 p-2 px-4 ` 24 | : "border-b-4 p-2 px-4 border-white" 25 | } 26 | > 27 | Responses 28 | 29 | 32 | nav.isActive 33 | ? `${theme.color}-border ${theme.color}-text border-b-4 p-2 px-4` 34 | : "border-b-4 p-2 px-4 border-white" 35 | } 36 | > 37 | Settings 38 | 39 |
40 | ); 41 | }; 42 | 43 | NavBar.defaultProps = { 44 | type: "blank", 45 | }; 46 | NavBar.propTypes = { 47 | type: PropTypes.string, 48 | }; 49 | export default NavBar; 50 | -------------------------------------------------------------------------------- /src/components/modals/RenameModal.js: -------------------------------------------------------------------------------- 1 | import FilledButton from "components/buttons/FilledButton"; 2 | import OutLinedButton from "components/buttons/OutLinedButton"; 3 | import { useFormik } from "formik"; 4 | import PropTypes from "prop-types"; 5 | 6 | const RenameModal = ({ closeModal, renameCallBack }) => { 7 | const { handleChange, handleSubmit, values } = useFormik({ 8 | initialValues: { name: "" }, 9 | onSubmit: (values) => { 10 | renameCallBack(values.name); 11 | }, 12 | }); 13 | return ( 14 |
15 |
19 |

Rename

20 |

Please enter a new name for the item

21 | 28 |
29 | 34 | 40 |
41 |
42 |
43 | ); 44 | }; 45 | 46 | RenameModal.propTypes = { 47 | closeModal: PropTypes.func.isRequired, 48 | renameCallBack: PropTypes.func.isRequired 49 | }; 50 | 51 | export default RenameModal; 52 | -------------------------------------------------------------------------------- /src/components/cards/Options/option.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import IndividualOption from "components/cards/options/IndividualOption"; 3 | import { SHORT_ANSWER, PARAGRAPH, MULTIPLE_CHOICE } from "data/optionTypes"; 4 | import createOption from "components/helpers/createOption"; 5 | 6 | const OptionCard = ({ type, options, setOptions }) => { 7 | const addNewOption = () => { 8 | const ops = [...options]; 9 | ops.push(createOption(options.length)); 10 | setOptions(ops); 11 | }; 12 | const saveOptions = (option) => { 13 | const ops = [...options]; 14 | const i = ops.findIndex((o) => o.id === option.id); 15 | ops[i] = option; 16 | setOptions(ops); 17 | }; 18 | const deleteOption = (opId) => { 19 | const temp = [...options]; 20 | const ind = temp.findIndex((e) => e.id === opId); 21 | temp.splice(ind, 1); 22 | setOptions(temp); 23 | }; 24 | 25 | if (type === SHORT_ANSWER || type === PARAGRAPH) 26 | return ( 27 |
28 | 29 |
30 | ); 31 | else 32 | return ( 33 |
34 | {options.map((option) => ( 35 | 42 | ))} 43 |
addNewOption()} 46 | > 47 | Add Options 48 |
49 |
50 | ); 51 | }; 52 | 53 | OptionCard.defaultProps = { 54 | type: MULTIPLE_CHOICE, 55 | options: [], 56 | }; 57 | OptionCard.propTypes = { 58 | type: PropTypes.string, 59 | options: PropTypes.array, 60 | setOptions: PropTypes.func, 61 | }; 62 | export default OptionCard; 63 | -------------------------------------------------------------------------------- /src/components/cards/options/Option.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import IndividualOption from "components/cards/options/IndividualOption"; 3 | import { SHORT_ANSWER, PARAGRAPH, MULTIPLE_CHOICE } from "data/optionTypes"; 4 | import createOption from "components/helpers/createOption"; 5 | 6 | const OptionCard = ({ type, options, setOptions }) => { 7 | const addNewOption = () => { 8 | const ops = [...options]; 9 | ops.push(createOption(options.length)); 10 | setOptions(ops); 11 | }; 12 | const saveOptions = (option) => { 13 | const ops = [...options]; 14 | const i = ops.findIndex((o) => o.id === option.id); 15 | ops[i] = option; 16 | setOptions(ops); 17 | }; 18 | const deleteOption = (opId) => { 19 | const temp = [...options]; 20 | const ind = temp.findIndex((e) => e.id === opId); 21 | temp.splice(ind, 1); 22 | setOptions(temp); 23 | }; 24 | 25 | if (type === SHORT_ANSWER || type === PARAGRAPH) 26 | return ( 27 |
28 | 29 |
30 | ); 31 | else 32 | return ( 33 |
34 | {options.map((option) => ( 35 | 42 | ))} 43 |
addNewOption()} 46 | > 47 | Add Options 48 |
49 |
50 | ); 51 | }; 52 | 53 | OptionCard.defaultProps = { 54 | type: MULTIPLE_CHOICE, 55 | options: [], 56 | }; 57 | OptionCard.propTypes = { 58 | type: PropTypes.string, 59 | options: PropTypes.array, 60 | setOptions: PropTypes.func, 61 | }; 62 | export default OptionCard; 63 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Google Forms 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google-forms-clone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fortawesome/fontawesome-svg-core": "^1.2.36", 7 | "@fortawesome/free-regular-svg-icons": "^5.15.4", 8 | "@fortawesome/free-solid-svg-icons": "^5.15.4", 9 | "@fortawesome/react-fontawesome": "^0.1.16", 10 | "@reduxjs/toolkit": "^1.7.1", 11 | "@testing-library/jest-dom": "^5.16.1", 12 | "@testing-library/react": "^12.1.2", 13 | "@testing-library/user-event": "^13.5.0", 14 | "firebase": "^9.6.4", 15 | "formik": "^2.2.9", 16 | "prop-types": "^15.8.1", 17 | "react": "^17.0.2", 18 | "react-beautiful-dnd": "^13.1.0", 19 | "react-dom": "^17.0.2", 20 | "react-redux": "^7.2.6", 21 | "react-router-dom": "^6.2.1", 22 | "react-scripts": "5.0.0", 23 | "web-vitals": "^2.1.3", 24 | "yup": "^0.32.11" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject" 31 | }, 32 | "eslintConfig": { 33 | "extends": [ 34 | "react-app", 35 | "react-app/jest" 36 | ] 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | }, 50 | "devDependencies": { 51 | "autoprefixer": "^10.4.2", 52 | "eslint": "^7.32.0", 53 | "eslint-config-react-app": "^7.0.0", 54 | "eslint-config-standard": "^16.0.3", 55 | "eslint-plugin-import": "^2.25.4", 56 | "eslint-plugin-node": "^11.1.0", 57 | "eslint-plugin-promise": "^5.2.0", 58 | "eslint-plugin-react": "^7.28.0", 59 | "postcss": "^8.4.5", 60 | "tailwindcss": "^3.0.15" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/views/CreateForm.js: -------------------------------------------------------------------------------- 1 | import ThemeEditor from "components/theme/themeEditor"; 2 | import FormHeader from "components/layout/headers/FormHeader"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { useParams, Outlet } from "react-router-dom"; 5 | import { useEffect, useState } from "react"; 6 | import { getForm } from "services/firebase/firestore.firebase"; 7 | import { setForm, setLoading } from "store/data/form.slice"; 8 | import Loading from "components/loaders/page.loader"; 9 | 10 | const CreateForm = () => { 11 | const { type } = useParams(); 12 | const { theme, title } = useSelector((state) => state.form); 13 | const [themeEditorVisibility, setThemeEditorVisibility] = useState(false); 14 | const toggleThemeEditor = () => { 15 | setThemeEditorVisibility(!themeEditorVisibility); 16 | }; 17 | 18 | const { user } = useSelector((state) => state.authentication); 19 | const { loading } = useSelector((state) => state.form); 20 | const dispatch = useDispatch(); 21 | 22 | useEffect(() => { 23 | if (type !== "blank") { 24 | const dispatchCallback = (formData) => { 25 | dispatch(setForm(formData.form)); 26 | }; 27 | dispatch(setLoading(true)); 28 | getForm(user.uid, type, dispatchCallback); 29 | } 30 | }, [user.uid, type, dispatch]); 31 | if (loading) return ; 32 | else 33 | return ( 34 |
35 | 40 |
45 | 46 |
47 | {themeEditorVisibility ? ( 48 | 49 | ) : null} 50 |
51 | ); 52 | }; 53 | 54 | export default CreateForm; 55 | -------------------------------------------------------------------------------- /src/components/dropdown/Dropdown.js: -------------------------------------------------------------------------------- 1 | import { 2 | faCaretDown, 3 | faCaretUp, 4 | faCheck, 5 | } from "@fortawesome/free-solid-svg-icons"; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | import { useState } from "react"; 8 | 9 | const Dropdown = ({ options, setSelected, defaultSelected }) => { 10 | const [selectedOption, setSelectedOption] = useState( 11 | defaultSelected ? defaultSelected : options[0].text 12 | ); 13 | const [openDropdown, setOpenDropdown] = useState(false); 14 | const toggleDropdownOptions = () => { 15 | setOpenDropdown(!openDropdown); 16 | }; 17 | const handleSelectOption = (op) => { 18 | setSelectedOption(op); 19 | setSelected(op); 20 | toggleDropdownOptions(); 21 | }; 22 | return ( 23 |
24 |
28 |
{selectedOption}
29 | {openDropdown ? ( 30 | 31 | ) : ( 32 | 33 | )} 34 |
35 | {openDropdown ? ( 36 |
    37 | {options.map((op) => ( 38 |
  • { 41 | handleSelectOption(op.text); 42 | }} 43 | > 44 | {op.icon ? ( 45 | 46 | ) : op.text === selectedOption ? ( 47 | 48 | ) : ( 49 |
    50 | )} 51 |
    {op.text}
    52 |
  • 53 | ))} 54 |
55 | ) : null} 56 |
57 | ); 58 | }; 59 | export default Dropdown; 60 | -------------------------------------------------------------------------------- /src/components/routes/public.routes.js: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from "react-router-dom"; 2 | import CreateForm from "views/CreateForm"; 3 | import Home from "views/Home"; 4 | import { useSelector } from "react-redux"; 5 | import Responses from "components/form/Responses"; 6 | import Settings from "components/form/Settings"; 7 | import NotFound from "views/404"; 8 | import Edit from "components/form/Edit"; 9 | import PropTypes from "prop-types"; 10 | import Landing from "views/Landing"; 11 | import PrivateRoute from "components/routes/private.routes"; 12 | 13 | const PublicRoutes = ({ logged }) => { 14 | const { theme } = useSelector((state) => state.form); 15 | 16 | return ( 17 | 18 | 22 | 23 | 24 | } 25 | /> 26 | 30 | 31 | 32 | } 33 | > 34 | 35 | 40 | 41 | 42 | } 43 | /> 44 | 48 | 49 | 50 | } 51 | /> 52 | 56 | 57 | 58 | } 59 | /> 60 | 61 | 62 | } /> 63 | } /> 64 | 65 | ); 66 | }; 67 | 68 | PublicRoutes.defaultProps = { 69 | logged: false, 70 | }; 71 | PublicRoutes.propTypes = { 72 | logged: PropTypes.bool, 73 | }; 74 | export default PublicRoutes; 75 | -------------------------------------------------------------------------------- /src/components/cards/Options/individualOption.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import { useState } from "react"; 3 | import { CHECKBOX, MULTIPLE_CHOICE } from "data/optionTypes"; 4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 5 | import { faTimes } from "@fortawesome/free-solid-svg-icons"; 6 | import { useSelector } from "react-redux"; 7 | 8 | const IndividualOption = ({ type, option, deleteOption, saveOption }) => { 9 | const [iOption, setIOption] = useState(option.text); 10 | 11 | const handleInputChange = (e) => { 12 | setIOption(e.target.value); 13 | }; 14 | 15 | const handleFocusOut = () => { 16 | saveOption({ id: option.id, text: iOption }); 17 | }; 18 | const { theme } = useSelector((state) => state.form); 19 | 20 | return ( 21 |
22 |
23 | 34 | handleInputChange(e)} 39 | onBlur={() => handleFocusOut()} 40 | /> 41 |
42 | 43 | deleteOption(option.id)} 45 | className="text-fontGrey hover:cursor-pointer" 46 | icon={faTimes} 47 | /> 48 |
49 | ); 50 | }; 51 | 52 | IndividualOption.propTypes = { 53 | type: PropTypes.string, 54 | option: PropTypes.object, 55 | deleteOption: PropTypes.func.isRequired, 56 | saveOption: PropTypes.func.isRequired, 57 | }; 58 | 59 | IndividualOption.defaultProps = { 60 | option: { id: undefined, text: "option" }, 61 | type: MULTIPLE_CHOICE, 62 | }; 63 | 64 | export default IndividualOption; 65 | -------------------------------------------------------------------------------- /src/components/cards/options/IndividualOption.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import { useState } from "react"; 3 | import { CHECKBOX, MULTIPLE_CHOICE } from "data/optionTypes"; 4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 5 | import { faTimes } from "@fortawesome/free-solid-svg-icons"; 6 | import { useSelector } from "react-redux"; 7 | 8 | const IndividualOption = ({ type, option, deleteOption, saveOption }) => { 9 | const [iOption, setIOption] = useState(option.text); 10 | 11 | const handleInputChange = (e) => { 12 | setIOption(e.target.value); 13 | }; 14 | 15 | const handleFocusOut = () => { 16 | saveOption({ id: option.id, text: iOption }); 17 | }; 18 | const { theme } = useSelector((state) => state.form); 19 | 20 | return ( 21 |
22 |
23 | 34 | handleInputChange(e)} 39 | onBlur={() => handleFocusOut()} 40 | /> 41 |
42 | 43 | deleteOption(option.id)} 45 | className="text-fontGrey hover:cursor-pointer" 46 | icon={faTimes} 47 | /> 48 |
49 | ); 50 | }; 51 | 52 | IndividualOption.propTypes = { 53 | type: PropTypes.string, 54 | option: PropTypes.object, 55 | deleteOption: PropTypes.func.isRequired, 56 | saveOption: PropTypes.func.isRequired, 57 | }; 58 | 59 | IndividualOption.defaultProps = { 60 | option: { id: undefined, text: "option" }, 61 | type: MULTIPLE_CHOICE, 62 | }; 63 | 64 | export default IndividualOption; 65 | -------------------------------------------------------------------------------- /src/views/Landing.js: -------------------------------------------------------------------------------- 1 | import FilledButton from "components/buttons/FilledButton"; 2 | import OutLinedButton from "components/buttons/OutLinedButton"; 3 | import LandingHeader from "components/layout/headers/LandingHeader"; 4 | import bg from "assets/bg-1.png"; 5 | import { useDispatch } from "react-redux"; 6 | import { 7 | setUser, 8 | startLoading, 9 | } from "store/authentication/authentication.slice"; 10 | import { SignIn } from "services/firebase/auth.firebase"; 11 | import { useNavigate } from "react-router-dom"; 12 | 13 | const Landing = () => { 14 | const dispatch = useDispatch(); 15 | const navigate = useNavigate(); 16 | 17 | const dispatchCallback = (payload) => { 18 | dispatch(setUser(payload)); 19 | }; 20 | const signIn = () => { 21 | dispatch(startLoading()); 22 | SignIn(dispatchCallback); 23 | navigate("/"); 24 | }; 25 | 26 | return ( 27 |
28 | 29 |
30 |
31 |
32 |
36 | Get insights quickly, with Google Forms 37 |
38 |
39 | Easily create and share online forms and surveys, and analyze 40 | responses in real-time. 41 |
42 |
43 | 48 | signIn()} 52 | /> 53 |
54 |
55 |
56 | Google Forms 61 |
62 |
63 |
64 |
65 | ); 66 | }; 67 | 68 | export default Landing; 69 | -------------------------------------------------------------------------------- /src/components/dropdown/DropdownwithIcon.js: -------------------------------------------------------------------------------- 1 | import { faEllipsisV } from "@fortawesome/free-solid-svg-icons"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import { useRef, useState } from "react"; 4 | import useOutsideAlert from "components/dropdown/useOutsideAlert"; 5 | import PropTypes from "prop-types"; 6 | 7 | const DropdownWithIcon = ({ 8 | options, 9 | renameEvent, 10 | removeEvent, 11 | openInNewTab, 12 | }) => { 13 | const dropDownRef = useRef(null); 14 | const [openDropdown, setOpenDropdown] = useState(false); 15 | const toggleDropdownOptions = () => { 16 | setOpenDropdown(!openDropdown); 17 | }; 18 | const handleSelectOption = (op) => { 19 | toggleDropdownOptions(); 20 | if (op === "Rename") renameEvent(); 21 | else if (op === "Remove") removeEvent(); 22 | else openInNewTab(); 23 | }; 24 | const actionCallback = () => { 25 | setOpenDropdown(false); 26 | }; 27 | useOutsideAlert(dropDownRef, actionCallback); 28 | return ( 29 |
30 |
34 |
35 | 36 |
37 |
38 | {openDropdown ? ( 39 |
    43 | {options.map((op) => ( 44 |
  • { 48 | handleSelectOption(op.text); 49 | }} 50 | > 51 | {op.icon ? ( 52 | 53 | ) : ( 54 |
    55 | )} 56 |
    {op.text}
    57 |
  • 58 | ))} 59 |
60 | ) : null} 61 |
62 | ); 63 | }; 64 | 65 | DropdownWithIcon.propTypes = { 66 | options: PropTypes.array, 67 | renameEvent: PropTypes.func, 68 | removeEvent: PropTypes.func, 69 | openInNewTab: PropTypes.func.isRequired, 70 | }; 71 | 72 | export default DropdownWithIcon; 73 | -------------------------------------------------------------------------------- /src/data/Templates.js: -------------------------------------------------------------------------------- 1 | import eventReg from "assets/eventReg.png"; 2 | import newForm from "assets/newForm.png"; 3 | import partyInv from "assets/partyInv.png"; 4 | import rsvp from "assets/rsvp.png"; 5 | import contactInfo from "assets/contactInfo.png"; 6 | import { 7 | faImage, 8 | faPlayCircle, 9 | faPlusSquare, 10 | } from "@fortawesome/free-regular-svg-icons"; 11 | import { dropdownOptions } from "data/optionTypes"; 12 | import { 13 | faFileImport, 14 | faPlusCircle, 15 | faTextHeight, 16 | } from "@fortawesome/free-solid-svg-icons"; 17 | 18 | export const formTemplates = [ 19 | { name: "Blank", img: newForm, url: "/create/blank/edit" }, 20 | { 21 | name: "Event Registration", 22 | img: eventReg, 23 | url: "/create/event-registeration", 24 | }, 25 | { 26 | name: "Contact Information", 27 | img: contactInfo, 28 | url: "/create/contact-information", 29 | }, 30 | { name: "Party Invite", img: partyInv, url: "/create/party-invite" }, 31 | { name: "RSVP", img: rsvp, url: "/create/rsvp" }, 32 | ]; 33 | 34 | export const ownershipFilters = [ 35 | { text: "Owned by anyone" }, 36 | { text: "Owned by me" }, 37 | { text: "Not owned by me" }, 38 | ]; 39 | 40 | export const formSamples = [ 41 | { 42 | id: "nudrigb43754398", 43 | title: "CN Lab Groups", 44 | img: contactInfo, 45 | date: "Jul 10, 2021", 46 | lastAction: "modified", 47 | shared: false, 48 | }, 49 | { 50 | id: "dghejrhgkeergh", 51 | title: "Party Invite", 52 | img: partyInv, 53 | date: "Jan 11, 2022", 54 | lastAction: "created", 55 | shared: true, 56 | }, 57 | { 58 | id: "hrejbdyugdgerhg", 59 | title: "Event Registeration", 60 | img: eventReg, 61 | date: "Apr 23, 2020", 62 | lastAction: "created", 63 | shared: false, 64 | }, 65 | ]; 66 | 67 | export const questionTemplate = [ 68 | { 69 | id: "gyusegvybct", 70 | title: "Question", 71 | options: [{ id: 0, text: "Option 1" }], 72 | optionType: dropdownOptions[2], 73 | required: true, 74 | }, 75 | { 76 | id: "hv4nu5huy45nh", 77 | title: "Question", 78 | options: [{ id: 0, text: "Option 1" }], 79 | optionType: dropdownOptions[3], 80 | required: false, 81 | }, 82 | ]; 83 | 84 | export const toolBarActions = [ 85 | { id: 1, icon: faPlusCircle, label: "Add Question" }, 86 | { id: 2, icon: faFileImport, label: "Import Question" }, 87 | { id: 3, icon: faTextHeight, label: "Add Title and Description" }, 88 | { id: 4, icon: faImage, label: "Add Image" }, 89 | { id: 5, icon: faPlayCircle, label: "Add Video" }, 90 | { id: 6, icon: faPlusSquare, label: "Add Section" }, 91 | ]; 92 | -------------------------------------------------------------------------------- /src/data/templates.js: -------------------------------------------------------------------------------- 1 | import eventReg from "assets/eventReg.png"; 2 | import newForm from "assets/newForm.png"; 3 | import partyInv from "assets/partyInv.png"; 4 | import rsvp from "assets/rsvp.png"; 5 | import contactInfo from "assets/contactInfo.png"; 6 | import { 7 | faImage, 8 | faPlayCircle, 9 | faPlusSquare, 10 | } from "@fortawesome/free-regular-svg-icons"; 11 | import { dropdownOptions } from "data/optionTypes"; 12 | import { 13 | faFileImport, 14 | faPlusCircle, 15 | faTextHeight, 16 | } from "@fortawesome/free-solid-svg-icons"; 17 | 18 | export const formTemplates = [ 19 | { name: "Blank", img: newForm, url: "/create/blank/edit" }, 20 | { 21 | name: "Event Registration", 22 | img: eventReg, 23 | url: "/create/event-registeration", 24 | }, 25 | { 26 | name: "Contact Information", 27 | img: contactInfo, 28 | url: "/create/contact-information", 29 | }, 30 | { name: "Party Invite", img: partyInv, url: "/create/party-invite" }, 31 | { name: "RSVP", img: rsvp, url: "/create/rsvp" }, 32 | ]; 33 | 34 | export const ownershipFilters = [ 35 | { text: "Owned by anyone" }, 36 | { text: "Owned by me" }, 37 | { text: "Not owned by me" }, 38 | ]; 39 | 40 | export const formSamples = [ 41 | { 42 | id: "nudrigb43754398", 43 | title: "CN Lab Groups", 44 | img: contactInfo, 45 | date: "Jul 10, 2021", 46 | lastAction: "modified", 47 | shared: false, 48 | }, 49 | { 50 | id: "dghejrhgkeergh", 51 | title: "Party Invite", 52 | img: partyInv, 53 | date: "Jan 11, 2022", 54 | lastAction: "created", 55 | shared: true, 56 | }, 57 | { 58 | id: "hrejbdyugdgerhg", 59 | title: "Event Registeration", 60 | img: eventReg, 61 | date: "Apr 23, 2020", 62 | lastAction: "created", 63 | shared: false, 64 | }, 65 | ]; 66 | 67 | export const questionTemplate = [ 68 | { 69 | id: "gyusegvybct", 70 | title: "Question", 71 | options: [{ id: 0, text: "Option 1" }], 72 | optionType: dropdownOptions[2], 73 | required: true, 74 | }, 75 | { 76 | id: "hv4nu5huy45nh", 77 | title: "Question", 78 | options: [{ id: 0, text: "Option 1" }], 79 | optionType: dropdownOptions[3], 80 | required: false, 81 | }, 82 | ]; 83 | 84 | export const toolBarActions = [ 85 | { id: 1, icon: faPlusCircle, label: "Add Question" }, 86 | { id: 2, icon: faFileImport, label: "Import Question" }, 87 | { id: 3, icon: faTextHeight, label: "Add Title and Description" }, 88 | { id: 4, icon: faImage, label: "Add Image" }, 89 | { id: 5, icon: faPlayCircle, label: "Add Video" }, 90 | { id: 6, icon: faPlusSquare, label: "Add Section" }, 91 | ]; 92 | -------------------------------------------------------------------------------- /src/components/layout/Headers/HomeHeader.js: -------------------------------------------------------------------------------- 1 | import { useSelector, useDispatch } from "react-redux"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import { faBars, faSearch } from "@fortawesome/free-solid-svg-icons"; 4 | import Logo from "assets/logo.png"; 5 | import dots from "assets/dots.png"; 6 | import { useState } from "react"; 7 | import { SignOut } from "services/firebase/auth.firebase"; 8 | import { 9 | startLoading, 10 | setUser, 11 | } from "store/authentication/authentication.slice"; 12 | import Icon from "components/icon/Icon"; 13 | 14 | const HomeHeader = () => { 15 | const { user } = useSelector((state) => state.authentication); 16 | const dispatch = useDispatch(); 17 | const [ddState, setDdState] = useState(false); 18 | const toggleDropdown = () => { 19 | setDdState(!ddState); 20 | }; 21 | 22 | const signOut = () => { 23 | dispatch(startLoading()); 24 | SignOut((user) => { 25 | dispatch(setUser({user})); 26 | }); 27 | }; 28 | 29 | return ( 30 |
31 |
32 | 33 | logo 34 |
Forms
35 |
36 |
37 |
38 | 39 |
40 | 45 |
46 |
47 |
48 | menu 49 |
50 |
51 | user 56 | {ddState ? ( 57 |
61 | Logout 62 |
63 | ) : null} 64 |
65 |
66 |
67 | ); 68 | }; 69 | export default HomeHeader; 70 | -------------------------------------------------------------------------------- /src/components/layout/headers/HomeHeader.js: -------------------------------------------------------------------------------- 1 | import { useSelector, useDispatch } from "react-redux"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import { faBars, faSearch } from "@fortawesome/free-solid-svg-icons"; 4 | import Logo from "assets/logo.png"; 5 | import dots from "assets/dots.png"; 6 | import { useState } from "react"; 7 | import { SignOut } from "services/firebase/auth.firebase"; 8 | import { 9 | startLoading, 10 | setUser, 11 | } from "store/authentication/authentication.slice"; 12 | import Icon from "components/icon/Icon"; 13 | 14 | const HomeHeader = () => { 15 | const { user } = useSelector((state) => state.authentication); 16 | const dispatch = useDispatch(); 17 | const [ddState, setDdState] = useState(false); 18 | const toggleDropdown = () => { 19 | setDdState(!ddState); 20 | }; 21 | 22 | const signOut = () => { 23 | dispatch(startLoading()); 24 | SignOut((user) => { 25 | dispatch(setUser({user})); 26 | }); 27 | }; 28 | 29 | return ( 30 |
31 |
32 | 33 | logo 34 |
Forms
35 |
36 |
37 |
38 | 39 |
40 | 45 |
46 |
47 |
48 | menu 49 |
50 |
51 | user 56 | {ddState ? ( 57 |
61 | Logout 62 |
63 | ) : null} 64 |
65 |
66 |
67 | ); 68 | }; 69 | export default HomeHeader; 70 | -------------------------------------------------------------------------------- /src/assets/fonts/fonts.css: -------------------------------------------------------------------------------- 1 | /* #### Generated By: http://www.cufonfonts.com #### */ 2 | 3 | @font-face { 4 | font-family: "Product Sans Regular"; 5 | font-style: normal; 6 | font-weight: normal; 7 | src: local("Product Sans Regular"), 8 | url("ProductSans-Regular.woff") format("woff"); 9 | } 10 | 11 | @font-face { 12 | font-family: "Product Sans Italic"; 13 | font-style: normal; 14 | font-weight: normal; 15 | src: local("Product Sans Italic"), 16 | url("ProductSans-Italic.woff") format("woff"); 17 | } 18 | 19 | @font-face { 20 | font-family: "Product Sans Thin Regular"; 21 | font-style: normal; 22 | font-weight: normal; 23 | src: local("Product Sans Thin Regular"), 24 | url("ProductSans-Thin.woff") format("woff"); 25 | } 26 | 27 | @font-face { 28 | font-family: "Product Sans Light Regular"; 29 | font-style: normal; 30 | font-weight: normal; 31 | src: local("Product Sans Light Regular"), 32 | url("ProductSans-Light.woff") format("woff"); 33 | } 34 | 35 | @font-face { 36 | font-family: "Product Sans Medium Regular"; 37 | font-style: normal; 38 | font-weight: normal; 39 | src: local("Product Sans Medium Regular"), 40 | url("ProductSans-Medium.woff") format("woff"); 41 | } 42 | 43 | @font-face { 44 | font-family: "Product Sans Black Regular"; 45 | font-style: normal; 46 | font-weight: normal; 47 | src: local("Product Sans Black Regular"), 48 | url("ProductSans-Black.woff") format("woff"); 49 | } 50 | 51 | @font-face { 52 | font-family: "Product Sans Thin Italic"; 53 | font-style: normal; 54 | font-weight: normal; 55 | src: local("Product Sans Thin Italic"), 56 | url("ProductSans-ThinItalic.woff") format("woff"); 57 | } 58 | 59 | @font-face { 60 | font-family: "Product Sans Light Italic"; 61 | font-style: normal; 62 | font-weight: normal; 63 | src: local("Product Sans Light Italic"), 64 | url("ProductSans-LightItalic.woff") format("woff"); 65 | } 66 | 67 | @font-face { 68 | font-family: "Product Sans Medium Italic"; 69 | font-style: normal; 70 | font-weight: normal; 71 | src: local("Product Sans Medium Italic"), 72 | url("ProductSans-MediumItalic.woff") format("woff"); 73 | } 74 | 75 | @font-face { 76 | font-family: "Product Sans Bold"; 77 | font-style: normal; 78 | font-weight: normal; 79 | src: local("Product Sans Bold"), url("ProductSans-Bold.woff") format("woff"); 80 | } 81 | 82 | @font-face { 83 | font-family: "Product Sans Bold Italic"; 84 | font-style: normal; 85 | font-weight: normal; 86 | src: local("Product Sans Bold Italic"), 87 | url("ProductSans-BoldItalic.woff") format("woff"); 88 | } 89 | 90 | @font-face { 91 | font-family: "Product Sans Black Italic"; 92 | font-style: normal; 93 | font-weight: normal; 94 | src: local("Product Sans Black Italic"), 95 | url("ProductSans-BlackItalic.woff") format("woff"); 96 | } 97 | -------------------------------------------------------------------------------- /src/components/dropdown/CustomDropdown.js: -------------------------------------------------------------------------------- 1 | import { 2 | faCaretDown, 3 | faCaretUp, 4 | faCheck, 5 | } from "@fortawesome/free-solid-svg-icons"; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | import { useState } from "react"; 8 | import PropTypes from "prop-types"; 9 | 10 | const CustomDropdown = ({ options, setSelected, defaultSelected, type }) => { 11 | const [selectedOption, setSelectedOption] = useState( 12 | defaultSelected ? defaultSelected : options[0] 13 | ); 14 | const [openDropdown, setOpenDropdown] = useState(false); 15 | const toggleDropdownOptions = () => { 16 | setOpenDropdown(!openDropdown); 17 | }; 18 | const handleSelectOption = (op) => { 19 | setSelectedOption(op); 20 | setSelected(op.text); 21 | toggleDropdownOptions(); 22 | }; 23 | 24 | return ( 25 |
26 |
30 |
31 | {selectedOption.icon ? ( 32 | 33 | ) : null} 34 | 35 |
40 | {selectedOption.text} 41 |
42 |
43 | {openDropdown ? ( 44 | 45 | ) : ( 46 | 47 | )} 48 |
49 | {openDropdown ? ( 50 |
    51 | {options.map((op) => ( 52 |
  • { 58 | handleSelectOption(op); 59 | }} 60 | > 61 | {op.icon ? ( 62 | 63 | ) : op.text === selectedOption.text ? ( 64 | 65 | ) : ( 66 |
    67 | )} 68 |
    {op.text}
    69 |
  • 70 | ))} 71 |
72 | ) : null} 73 |
74 | ); 75 | }; 76 | 77 | CustomDropdown.propTypes = { 78 | options: PropTypes.array.isRequired, 79 | setSelected: PropTypes.func.isRequired, 80 | defaultSelected: PropTypes.object, 81 | type: PropTypes.string, 82 | }; 83 | 84 | CustomDropdown.defaultProps = { 85 | defaultSelected: null, 86 | type: undefined, 87 | }; 88 | 89 | export default CustomDropdown; 90 | -------------------------------------------------------------------------------- /src/components/cards/TitleCard.js: -------------------------------------------------------------------------------- 1 | import { PROGRESS_SAVING } from "data/statusMessages"; 2 | import PropTypes from "prop-types"; 3 | import { useState } from "react"; 4 | import { useDispatch, useSelector } from "react-redux"; 5 | import { 6 | setFormDescriptionInDB, 7 | setFormTitleInDB, 8 | } from "services/firebase/firestore.firebase"; 9 | import { setTitle, setDescription, setSaved } from "store/data/form.slice"; 10 | import { useFormik } from "formik"; 11 | import { headerSchema as validationSchema } from "components/helpers/validations"; 12 | 13 | const TitleCard = ({ selected }) => { 14 | const dispatch = useDispatch(); 15 | const { id, theme, title, description } = useSelector((state) => state.form); 16 | const [formDescription, setFormDescription] = useState(description); 17 | 18 | const { handleChange, values, errors } = useFormik({ 19 | initialValues: { title }, 20 | validationSchema, 21 | }); 22 | 23 | const savedCallBack = (msg) => { 24 | dispatch(setSaved(msg)); 25 | }; 26 | 27 | const handleDescriptionChange = (e) => { 28 | setFormDescription(e.target.value); 29 | }; 30 | const saveTitle = (e) => { 31 | if (!errors.title) { 32 | savedCallBack(PROGRESS_SAVING); 33 | setTitle(values.title); 34 | setFormTitleInDB(id, values.title, savedCallBack); 35 | } 36 | }; 37 | const saveDescription = (e) => { 38 | setDescription(formDescription); 39 | savedCallBack(PROGRESS_SAVING); 40 | setFormDescriptionInDB(id, formDescription, savedCallBack); 41 | }; 42 | 43 | return ( 44 |
49 |
50 |
51 | 59 |

60 | {errors.title ? errors.title : null} 61 |

62 | { 69 | handleDescriptionChange(e); 70 | }} 71 | onBlur={saveDescription} 72 | /> 73 |
74 |
75 | ); 76 | }; 77 | 78 | TitleCard.defaultProps = { 79 | title: "Untitled Form", 80 | description: "", 81 | color: "purple", 82 | selected: true, 83 | }; 84 | 85 | TitleCard.propTypes = { 86 | title: PropTypes.string, 87 | description: PropTypes.string, 88 | color: PropTypes.string, 89 | }; 90 | 91 | export default TitleCard; 92 | -------------------------------------------------------------------------------- /src/components/layout/Headers/FormHeader.js: -------------------------------------------------------------------------------- 1 | import { faFolder, faStar, faEye } from "@fortawesome/free-regular-svg-icons"; 2 | import { 3 | faEllipsisV, 4 | faPalette, 5 | faRedo, 6 | faUndo, 7 | } from "@fortawesome/free-solid-svg-icons"; 8 | import logo from "assets/logo.png"; 9 | import FilledButton from "components/buttons/FilledButton"; 10 | import { useSelector, useDispatch } from "react-redux"; 11 | import { useState } from "react"; 12 | import { SignOut } from "services/firebase/auth.firebase"; 13 | import { 14 | startLoading, 15 | setUser, 16 | } from "store/authentication/authentication.slice"; 17 | import { Link } from "react-router-dom"; 18 | import Icon from "components/icon/Icon"; 19 | import NavBar from "components/layout/navigation/NavBar"; 20 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 21 | 22 | const FormHeader = ({ id, title, toggleThemeEditor }) => { 23 | const dispatch = useDispatch(); 24 | const [ddState, setDdState] = useState(false); 25 | const toggleDropdown = () => { 26 | setDdState(!ddState); 27 | }; 28 | const signOut = () => { 29 | dispatch(startLoading()); 30 | SignOut((user) => { 31 | dispatch(setUser({ user })); 32 | }); 33 | }; 34 | const { user } = useSelector((state) => state.authentication); 35 | const { saved } = useSelector((state) => state.form); 36 | return ( 37 |
38 |
39 |
40 | 41 | logo 42 | 43 |
{title}
44 | 45 | 46 | 47 |
{saved}
48 |
49 |
50 | 51 |
52 | 53 | 54 | 55 | 56 | 61 | 62 |
63 |
64 | user 69 | {ddState ? ( 70 |
74 | Logout 75 |
76 | ) : null} 77 |
78 |
79 |
80 | 81 |
82 | ); 83 | }; 84 | 85 | FormHeader.defaultProps = { 86 | title: "Untitled Form", 87 | }; 88 | 89 | export default FormHeader; 90 | -------------------------------------------------------------------------------- /src/components/layout/headers/FormHeader.js: -------------------------------------------------------------------------------- 1 | import { faFolder, faStar, faEye } from "@fortawesome/free-regular-svg-icons"; 2 | import { 3 | faEllipsisV, 4 | faPalette, 5 | faRedo, 6 | faUndo, 7 | } from "@fortawesome/free-solid-svg-icons"; 8 | import logo from "assets/logo.png"; 9 | import FilledButton from "components/buttons/FilledButton"; 10 | import { useSelector, useDispatch } from "react-redux"; 11 | import { useState } from "react"; 12 | import { SignOut } from "services/firebase/auth.firebase"; 13 | import { 14 | startLoading, 15 | setUser, 16 | } from "store/authentication/authentication.slice"; 17 | import { Link } from "react-router-dom"; 18 | import Icon from "components/icon/Icon"; 19 | import NavBar from "components/layout/navigation/NavBar"; 20 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 21 | 22 | const FormHeader = ({ id, title, toggleThemeEditor }) => { 23 | const dispatch = useDispatch(); 24 | const [ddState, setDdState] = useState(false); 25 | const toggleDropdown = () => { 26 | setDdState(!ddState); 27 | }; 28 | const signOut = () => { 29 | dispatch(startLoading()); 30 | SignOut((user) => { 31 | dispatch(setUser({ user })); 32 | }); 33 | }; 34 | const { user } = useSelector((state) => state.authentication); 35 | const { saved } = useSelector((state) => state.form); 36 | return ( 37 |
38 |
39 |
40 | 41 | logo 42 | 43 |
{title}
44 | 45 | 46 | 47 |
{saved}
48 |
49 |
50 | 51 |
52 | 53 | 54 | 55 | 56 | 61 | 62 |
63 |
64 | user 69 | {ddState ? ( 70 |
74 | Logout 75 |
76 | ) : null} 77 |
78 |
79 |
80 | 81 |
82 | ); 83 | }; 84 | 85 | FormHeader.defaultProps = { 86 | title: "Untitled Form", 87 | }; 88 | 89 | export default FormHeader; 90 | -------------------------------------------------------------------------------- /src/components/form/Edit.js: -------------------------------------------------------------------------------- 1 | import TitleCard from "components/cards/TitleCard"; 2 | import QuestionCard from "components/cards/QuestionCard"; 3 | import { useState } from "react"; 4 | import { useDispatch, useSelector } from "react-redux"; 5 | import ToolBar from "components/toolbar/toolBar"; 6 | import createQuestion from "components/helpers/createQuestion"; 7 | import { 8 | addQuestion, 9 | setDraggedQuestion, 10 | setQuestion, 11 | setSaved, 12 | } from "store/data/form.slice"; 13 | import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; 14 | import { addQuestionInDB } from "services/firebase/firestore.firebase"; 15 | import { PROGRESS_SAVING } from "data/statusMessages"; 16 | 17 | const Edit = () => { 18 | const { questions, id } = useSelector((state) => state.form); 19 | const [selected, setSelected] = useState(questions[0].id); 20 | const dispatch = useDispatch(); 21 | 22 | const savedCallBack = (saved) => { 23 | dispatch(setSaved(saved)); 24 | }; 25 | const setFormQuestion = (qid, question) => { 26 | dispatch(setQuestion({ id: qid, question })); 27 | }; 28 | 29 | const addNewQuestion = () => { 30 | const quest = { question: createQuestion(questions.length) }; 31 | dispatch(addQuestion(quest)); 32 | dispatch(setSaved(PROGRESS_SAVING)); 33 | addQuestionInDB(id, quest.question, savedCallBack); 34 | }; 35 | 36 | const selectQuestionCard = (qid) => { 37 | setSelected(qid); 38 | }; 39 | 40 | const handleOnDragEnd = (result) => { 41 | if (!result.destination) return; 42 | dispatch(setDraggedQuestion({ result })); 43 | }; 44 | 45 | return ( 46 |
47 |
48 |
49 |
50 | 51 | 52 | 53 | 54 | {(provided) => ( 55 |
60 | {questions.map((question, idx) => ( 61 | 66 | {(provided) => ( 67 |
72 | 79 |
80 | )} 81 |
82 | ))} 83 | {provided.placeholder} 84 |
85 | )} 86 |
87 |
88 |
89 | 90 |
91 |
92 | ); 93 | }; 94 | 95 | export default Edit; 96 | -------------------------------------------------------------------------------- /src/store/data/form.slice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | import createQuestion from "components/helpers/createQuestion"; 3 | import generateKey from "components/helpers/generateKey"; 4 | 5 | export const formSlice = createSlice({ 6 | name: "Untitled Form", 7 | initialState: { 8 | loading: false, 9 | saved: "", 10 | id: "", 11 | theme: { 12 | color: "purple", 13 | font: "basic", 14 | backgroundOpacity: 10, 15 | }, 16 | title: "Untitled Form", 17 | description: "", 18 | questions: [createQuestion(0)], 19 | date: "", 20 | shared: true, 21 | error: null, 22 | }, 23 | reducers: { 24 | addQuestion: (state, action) => { 25 | const temp = [...state.questions]; 26 | const question = action.payload.question; 27 | temp.push(question); 28 | state.questions = temp; 29 | }, 30 | setQuestion: (state, action) => { 31 | const temp = [...state.questions]; 32 | const ind = temp.findIndex((x) => x.id === action.payload.id); 33 | temp[ind] = action.payload.question; 34 | state.questions = temp; 35 | }, 36 | removeQuestion: (state, action) => { 37 | const temp = state.questions; 38 | const index = temp.findIndex((e) => e.id === action.payload.id); 39 | temp.splice(index, 1); 40 | state.questions = temp; 41 | }, 42 | setDraggedQuestion: (state, action) => { 43 | const result = action.payload.result; 44 | const temp = [...state.questions]; 45 | const [reorderedItem] = temp.splice(result.source.index, 1); 46 | temp.splice(result.destination.index, 0, reorderedItem); 47 | state.questions = temp; 48 | }, 49 | duplicateQuestion: (state, action) => { 50 | const temp = state.questions; 51 | const index = temp.findIndex((e) => e.id === action.payload.id); 52 | const question = { ...action.payload.question }; 53 | question.id = generateKey("question" + index); 54 | temp.splice(index, 0, question); 55 | state.questions = temp; 56 | }, 57 | setColor: (state, action) => { 58 | state.theme.color = action.payload.color; 59 | }, 60 | setFont: (state, action) => { 61 | state.theme.font = action.payload.font; 62 | }, 63 | setBackgroundOpacity: (state, action) => { 64 | state.theme.backgroundOpacity = action.payload.opacity; 65 | }, 66 | setTitle: (state, action) => { 67 | state.title = action.payload.title; 68 | }, 69 | setDescription: (state, action) => { 70 | state.description = action.payload.description; 71 | }, 72 | setForm: (state, action) => { 73 | if (action.payload.error) state.error = action.payload.error; 74 | else { 75 | state.id = action.payload.id; 76 | state.theme = action.payload.theme; 77 | state.title = action.payload.title; 78 | state.description = action.payload.description; 79 | state.questions = action.payload.questions; 80 | state.loading = false; 81 | state.date = action.payload.date; 82 | state.shared = action.payload.shared; 83 | } 84 | }, 85 | setLoading: (state, action) => { 86 | state.loading = action.payload; 87 | }, 88 | setSaved: (state, action) => { 89 | state.saved = action.payload; 90 | }, 91 | }, 92 | }); 93 | 94 | export const { 95 | addQuestion, 96 | setQuestion, 97 | removeQuestion, 98 | theme, 99 | setColor, 100 | setFont, 101 | setBackgroundOpacity, 102 | setTitle, 103 | setDescription, 104 | duplicateQuestion, 105 | setDraggedQuestion, 106 | setForm, 107 | setLoading, 108 | setSaved, 109 | } = formSlice.actions; 110 | export default formSlice.reducer; 111 | -------------------------------------------------------------------------------- /src/components/form/FormTile/FormTile.js: -------------------------------------------------------------------------------- 1 | import { 2 | faTextHeight, 3 | faTrashAlt, 4 | faExternalLinkAlt, 5 | } from "@fortawesome/free-solid-svg-icons"; 6 | import slogo from "assets/logo2.svg"; 7 | import DropdownWithIcon from "components/dropdown/DropdownwithIcon"; 8 | import openInNewTab from "components/helpers/openInNewTab"; 9 | import { LIST } from "data/viewTypes"; 10 | import PropTypes from "prop-types"; 11 | import { deleteFormFromDB } from "services/firebase/firestore.firebase"; 12 | import RenameModal from "components/modals/RenameModal"; 13 | import { useState } from "react"; 14 | 15 | const FormTile = ({ formData, gridView, onClick, removeForm, renameForm }) => { 16 | const [renameModalVisibility, setRenameModalVisibility] = useState(false); 17 | const actions = [ 18 | { id: 1, icon: faTextHeight, text: "Rename" }, 19 | { id: 2, icon: faTrashAlt, text: "Remove" }, 20 | { id: 3, icon: faExternalLinkAlt, text: "Open in new tab" }, 21 | ]; 22 | const renameCallBack = (name) => { 23 | renameForm(formData.id, name); 24 | }; 25 | const toggleRenameModal = () => { 26 | setRenameModalVisibility(!renameModalVisibility); 27 | }; 28 | const removeEvent = () => { 29 | deleteFormFromDB(formData.id); 30 | removeForm(formData.id); 31 | }; 32 | const openLinkInNewTab = () => { 33 | openInNewTab(`/create/${formData.id}/edit`); 34 | }; 35 | 36 | if (gridView) 37 | return ( 38 |
  • 39 |
    40 |
    41 |
    42 | form 43 |
    44 |
    {formData.title}
    45 |
    46 |
    47 |
    {formData.date}
    48 | 54 | {renameModalVisibility && ( 55 | 59 | )} 60 |
  • 61 | ); 62 | else 63 | return ( 64 |
    68 |
    69 | {formData.title} 70 |
    71 |
    72 |
    {formData.title}
    73 |
    74 |
    75 | Logo 76 | {formData.lastAction === "modified" ? ( 77 |
    78 | Modified{" "} 79 |
    80 | ) : null} 81 |
    82 | {formData.date} 83 |
    84 |
    85 | 90 |
    91 |
    92 |
    93 | ); 94 | }; 95 | 96 | FormTile.defaultProps = { 97 | type: LIST, 98 | }; 99 | FormTile.propTypes = { 100 | type: PropTypes.string, 101 | formData: PropTypes.object.isRequired, 102 | onClick: PropTypes.func.isRequired, 103 | }; 104 | 105 | export default FormTile; 106 | -------------------------------------------------------------------------------- /src/components/form/formTile/FormTile.js: -------------------------------------------------------------------------------- 1 | import { 2 | faTextHeight, 3 | faTrashAlt, 4 | faExternalLinkAlt, 5 | } from "@fortawesome/free-solid-svg-icons"; 6 | import slogo from "assets/logo2.svg"; 7 | import DropdownWithIcon from "components/dropdown/DropdownwithIcon"; 8 | import openInNewTab from "components/helpers/openInNewTab"; 9 | import { LIST } from "data/viewTypes"; 10 | import PropTypes from "prop-types"; 11 | import { deleteFormFromDB } from "services/firebase/firestore.firebase"; 12 | import RenameModal from "components/modals/RenameModal"; 13 | import { useState } from "react"; 14 | 15 | const FormTile = ({ formData, gridView, onClick, removeForm, renameForm }) => { 16 | const [renameModalVisibility, setRenameModalVisibility] = useState(false); 17 | const actions = [ 18 | { id: 1, icon: faTextHeight, text: "Rename" }, 19 | { id: 2, icon: faTrashAlt, text: "Remove" }, 20 | { id: 3, icon: faExternalLinkAlt, text: "Open in new tab" }, 21 | ]; 22 | const renameCallBack = (name) => { 23 | renameForm(formData.id, name); 24 | }; 25 | const toggleRenameModal = () => { 26 | setRenameModalVisibility(!renameModalVisibility); 27 | }; 28 | const removeEvent = () => { 29 | deleteFormFromDB(formData.id); 30 | removeForm(formData.id); 31 | }; 32 | const openLinkInNewTab = () => { 33 | openInNewTab(`/create/${formData.id}/edit`); 34 | }; 35 | 36 | if (gridView) 37 | return ( 38 |
  • 39 |
    40 |
    41 |
    42 | form 43 |
    44 |
    {formData.title}
    45 |
    46 |
    47 |
    {formData.date}
    48 | 54 | {renameModalVisibility && ( 55 | 59 | )} 60 |
  • 61 | ); 62 | else 63 | return ( 64 |
    68 |
    69 | {formData.title} 70 |
    71 |
    72 |
    {formData.title}
    73 |
    74 |
    75 | Logo 76 | {formData.lastAction === "modified" ? ( 77 |
    78 | Modified{" "} 79 |
    80 | ) : null} 81 |
    82 | {formData.date} 83 |
    84 |
    85 | 90 |
    91 |
    92 |
    93 | ); 94 | }; 95 | 96 | FormTile.defaultProps = { 97 | type: LIST, 98 | }; 99 | FormTile.propTypes = { 100 | type: PropTypes.string, 101 | formData: PropTypes.object.isRequired, 102 | onClick: PropTypes.func.isRequired, 103 | }; 104 | 105 | export default FormTile; 106 | -------------------------------------------------------------------------------- /src/components/theme/ThemeEditor.js: -------------------------------------------------------------------------------- 1 | import { faImage } from "@fortawesome/free-regular-svg-icons"; 2 | import { faPalette, faTimes } from "@fortawesome/free-solid-svg-icons"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | import Icon from "components/icon/Icon"; 5 | import { useSelector } from "react-redux"; 6 | import { colors, fonts } from "data/theme/themeOptions"; 7 | import ColorComponent from "components/theme/colorComponent"; 8 | import BackgroundColorComponent from "components/theme/backgroundColorComponent"; 9 | import { useDispatch } from "react-redux"; 10 | import { 11 | setBackgroundOpacity, 12 | setColor, 13 | setFont, 14 | setSaved, 15 | } from "store/data/form.slice"; 16 | import PropTypes from "prop-types"; 17 | import CustomDropdown from "components/dropdown/CustomDropdown"; 18 | import { PROGRESS_SAVING } from "data/statusMessages"; 19 | import { setThemeInDB } from "services/firebase/firestore.firebase"; 20 | 21 | const ThemeEditor = ({ toggleThemeEditor }) => { 22 | const { id, theme } = useSelector((state) => state.form); 23 | const dispatch = useDispatch(); 24 | 25 | const savedCallBack = (msg) => { 26 | dispatch(setSaved(msg)); 27 | }; 28 | const selectColor = (color) => { 29 | dispatch(setColor({ color })); 30 | savedCallBack(PROGRESS_SAVING); 31 | const temp = { ...theme }; 32 | temp.color = color; 33 | setThemeInDB(id, temp, savedCallBack); 34 | }; 35 | const setBGOpacity = (opacity) => { 36 | dispatch(setBackgroundOpacity({ opacity })); 37 | savedCallBack(PROGRESS_SAVING); 38 | const temp = { ...theme }; 39 | temp.backgroundOpacity = opacity; 40 | setThemeInDB(id, temp, savedCallBack); 41 | }; 42 | const setFormFont = (font) => { 43 | dispatch(setFont({ font: font })); 44 | savedCallBack(PROGRESS_SAVING); 45 | const temp = { ...theme }; 46 | temp.font = font; 47 | setThemeInDB(id, temp, savedCallBack); 48 | }; 49 | return ( 50 |
    51 |
    52 |
    53 | 57 |
    Theme Options
    58 |
    59 | 60 |
    61 |
    62 |
    HEADER
    63 | 79 |
    80 |
    81 |
    THEME COLOR
    82 |
    83 | {colors.map((color) => ( 84 | selectColor(color)} 89 | /> 90 | ))} 91 |
    92 |
    93 |
    94 |
    BACKGROUND COLOR
    95 | 100 |
    101 |
    102 |
    FONT STYLE
    103 |
    104 | 110 |
    111 |
    112 |
    113 | ); 114 | }; 115 | 116 | ThemeEditor.propTypes = { 117 | toggleThemeEditor: PropTypes.func.isRequired, 118 | }; 119 | 120 | export default ThemeEditor; 121 | -------------------------------------------------------------------------------- /src/components/theme/themeEditor.js: -------------------------------------------------------------------------------- 1 | import { faImage } from "@fortawesome/free-regular-svg-icons"; 2 | import { faPalette, faTimes } from "@fortawesome/free-solid-svg-icons"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | import Icon from "components/icon/Icon"; 5 | import { useSelector } from "react-redux"; 6 | import { colors, fonts } from "data/theme/themeOptions"; 7 | import ColorComponent from "components/theme/colorComponent"; 8 | import BackgroundColorComponent from "components/theme/backgroundColorComponent"; 9 | import { useDispatch } from "react-redux"; 10 | import { 11 | setBackgroundOpacity, 12 | setColor, 13 | setFont, 14 | setSaved, 15 | } from "store/data/form.slice"; 16 | import PropTypes from "prop-types"; 17 | import CustomDropdown from "components/dropdown/CustomDropdown"; 18 | import { PROGRESS_SAVING } from "data/statusMessages"; 19 | import { setThemeInDB } from "services/firebase/firestore.firebase"; 20 | 21 | const ThemeEditor = ({ toggleThemeEditor }) => { 22 | const { id, theme } = useSelector((state) => state.form); 23 | const dispatch = useDispatch(); 24 | 25 | const savedCallBack = (msg) => { 26 | dispatch(setSaved(msg)); 27 | }; 28 | const selectColor = (color) => { 29 | dispatch(setColor({ color })); 30 | savedCallBack(PROGRESS_SAVING); 31 | const temp = { ...theme }; 32 | temp.color = color; 33 | setThemeInDB(id, temp, savedCallBack); 34 | }; 35 | const setBGOpacity = (opacity) => { 36 | dispatch(setBackgroundOpacity({ opacity })); 37 | savedCallBack(PROGRESS_SAVING); 38 | const temp = { ...theme }; 39 | temp.backgroundOpacity = opacity; 40 | setThemeInDB(id, temp, savedCallBack); 41 | }; 42 | const setFormFont = (font) => { 43 | dispatch(setFont({ font: font })); 44 | savedCallBack(PROGRESS_SAVING); 45 | const temp = { ...theme }; 46 | temp.font = font; 47 | setThemeInDB(id, temp, savedCallBack); 48 | }; 49 | return ( 50 |
    51 |
    52 |
    53 | 57 |
    Theme Options
    58 |
    59 | 60 |
    61 |
    62 |
    HEADER
    63 | 79 |
    80 |
    81 |
    THEME COLOR
    82 |
    83 | {colors.map((color) => ( 84 | selectColor(color)} 89 | /> 90 | ))} 91 |
    92 |
    93 |
    94 |
    BACKGROUND COLOR
    95 | 100 |
    101 |
    102 |
    FONT STYLE
    103 |
    104 | 110 |
    111 |
    112 |
    113 | ); 114 | }; 115 | 116 | ThemeEditor.propTypes = { 117 | toggleThemeEditor: PropTypes.func.isRequired, 118 | }; 119 | 120 | export default ThemeEditor; 121 | -------------------------------------------------------------------------------- /src/assets/colors/Colors.css: -------------------------------------------------------------------------------- 1 | .gray-bg { 2 | @apply bg-gray; 3 | } 4 | .gray-border { 5 | @apply border-gray; 6 | } 7 | .gray-text { 8 | @apply text-gray; 9 | } 10 | .grayTextField:focus { 11 | @apply border-gray; 12 | } 13 | .gray10-bg { 14 | @apply bg-gray/10; 15 | } 16 | .gray20-bg { 17 | @apply bg-gray/20; 18 | } 19 | .gray30-bg { 20 | @apply bg-gray/30; 21 | } 22 | .grey0-bg { 23 | background-color: #f6f6f6; 24 | } 25 | 26 | .bluegray-bg { 27 | @apply bg-bluegray; 28 | } 29 | .bluegray-border { 30 | @apply border-bluegray; 31 | } 32 | .bluegray-text { 33 | @apply text-bluegray; 34 | } 35 | .bluegrayTextField:focus { 36 | @apply border-bluegray; 37 | } 38 | .bluegray10-bg { 39 | @apply bg-bluegray/10; 40 | } 41 | .bluegray20-bg { 42 | @apply bg-bluegray/20; 43 | } 44 | .bluegray30-bg { 45 | @apply bg-bluegray/30; 46 | } 47 | .bluegrey0-bg { 48 | background-color: #f6f6f6; 49 | } 50 | 51 | .green-bg { 52 | @apply bg-green; 53 | } 54 | .green-border { 55 | @apply border-green; 56 | } 57 | .green-text { 58 | @apply text-green; 59 | } 60 | .greenTextField:focus { 61 | @apply border-green; 62 | } 63 | .green10-bg { 64 | @apply bg-green/10; 65 | } 66 | .green20-bg { 67 | @apply bg-green/20; 68 | } 69 | .green30-bg { 70 | @apply bg-green/30; 71 | } 72 | .green0-bg { 73 | background-color: #f6f6f6; 74 | } 75 | 76 | .teal-bg { 77 | @apply bg-teal; 78 | } 79 | .teal-border { 80 | @apply border-teal; 81 | } 82 | .teal-text { 83 | @apply text-teal; 84 | } 85 | .tealTextField:focus { 86 | @apply border-teal; 87 | } 88 | .teal10-bg { 89 | @apply bg-teal/10; 90 | } 91 | .teal20-bg { 92 | @apply bg-teal/20; 93 | } 94 | .teal30-bg { 95 | @apply bg-teal/30; 96 | } 97 | .teal0-bg { 98 | background-color: #f6f6f6; 99 | } 100 | 101 | .orange-bg { 102 | @apply bg-orange; 103 | } 104 | .orange-border { 105 | @apply border-orange; 106 | } 107 | .orange-text { 108 | @apply text-orange; 109 | } 110 | .orangeTextField:focus { 111 | @apply border-orange; 112 | } 113 | .orange10-bg { 114 | @apply bg-orange/10; 115 | } 116 | .orange20-bg { 117 | @apply bg-orange/20; 118 | } 119 | .orange30-bg { 120 | @apply bg-orange/30; 121 | } 122 | .orange0-bg { 123 | background-color: #f6f6f6; 124 | } 125 | 126 | .redorange-bg { 127 | @apply bg-redorange; 128 | } 129 | .redorange-border { 130 | @apply border-redorange; 131 | } 132 | .redorange-text { 133 | @apply text-redorange; 134 | } 135 | .redorangeTextField:focus { 136 | @apply border-redorange; 137 | } 138 | .redorange10-bg { 139 | @apply bg-redorange/10; 140 | } 141 | .redorange20-bg { 142 | @apply bg-redorange/20; 143 | } 144 | .redorange30-bg { 145 | @apply bg-redorange/30; 146 | } 147 | .redorange0-bg { 148 | background-color: #f6f6f6; 149 | } 150 | 151 | .cyan-bg { 152 | @apply bg-cyan; 153 | } 154 | .cyan-border { 155 | @apply border-cyan; 156 | } 157 | .cyan-text { 158 | @apply text-cyan; 159 | } 160 | .cyanTextField:focus { 161 | @apply border-cyan; 162 | } 163 | .cyan10-bg { 164 | @apply bg-cyan/10; 165 | } 166 | .cyan20-bg { 167 | @apply bg-cyan/20; 168 | } 169 | .cyan30-bg { 170 | @apply bg-cyan/30; 171 | } 172 | .cyan0-bg { 173 | background-color: #f6f6f6; 174 | } 175 | 176 | .lightblue-bg { 177 | @apply bg-lightblue; 178 | } 179 | .lightblue-border { 180 | @apply border-lightblue; 181 | } 182 | .lightblue-text { 183 | @apply text-lightblue; 184 | } 185 | .lightblueTextField:focus { 186 | @apply border-lightblue; 187 | } 188 | .lightblue10-bg { 189 | @apply bg-lightblue/10; 190 | } 191 | .lightblue20-bg { 192 | @apply bg-lightblue/20; 193 | } 194 | .lightblue30-bg { 195 | @apply bg-lightblue/30; 196 | } 197 | .lightblue0-bg { 198 | background-color: #f6f6f6; 199 | } 200 | 201 | .indigo-bg { 202 | @apply bg-indigo; 203 | } 204 | .indigo-border { 205 | @apply border-indigo; 206 | } 207 | .indigo-text { 208 | @apply text-indigo; 209 | } 210 | .indigoTextField:focus { 211 | @apply border-indigo; 212 | } 213 | .indigo10-bg { 214 | @apply bg-indigo/10; 215 | } 216 | .indigo20-bg { 217 | @apply bg-indigo/20; 218 | } 219 | .indigo30-bg { 220 | @apply bg-indigo/30; 221 | } 222 | .indigo0-bg { 223 | background-color: #f6f6f6; 224 | } 225 | 226 | .red-bg { 227 | @apply bg-red; 228 | } 229 | .red-border { 230 | @apply border-red; 231 | } 232 | .red-text { 233 | @apply text-red; 234 | } 235 | .redTextField:focus { 236 | @apply border-red; 237 | } 238 | .red10-bg { 239 | @apply bg-red/10; 240 | } 241 | .red20-bg { 242 | @apply bg-red/20; 243 | } 244 | .red30-bg { 245 | @apply bg-red/30; 246 | } 247 | .red0-bg { 248 | background-color: #f6f6f6; 249 | } 250 | 251 | .blue-bg { 252 | @apply bg-blue; 253 | } 254 | .blue-border { 255 | @apply border-blue; 256 | } 257 | .blue-text { 258 | @apply text-blue; 259 | } 260 | .blueTextField:focus { 261 | @apply border-blue; 262 | } 263 | .blue10-bg { 264 | @apply bg-blue/10; 265 | } 266 | .blue20-bg { 267 | @apply bg-blue/20; 268 | } 269 | .blue30-bg { 270 | @apply bg-blue/30; 271 | } 272 | .blue0-bg { 273 | background-color: #f6f6f6; 274 | } 275 | 276 | .purple-bg { 277 | @apply bg-purple; 278 | } 279 | .purple-border { 280 | @apply border-purple; 281 | } 282 | .purple-text { 283 | @apply text-purple; 284 | } 285 | .purpleTextField:focus { 286 | @apply border-purple; 287 | } 288 | .purple10-bg { 289 | @apply bg-purple/10; 290 | } 291 | .purple20-bg { 292 | @apply bg-purple/20; 293 | } 294 | .purple30-bg { 295 | @apply bg-purple/30; 296 | } 297 | .purple0-bg { 298 | background-color: #f6f6f6; 299 | } 300 | -------------------------------------------------------------------------------- /src/services/firebase/firestore.firebase.js: -------------------------------------------------------------------------------- 1 | import { db } from "services/firebase/config.firebase"; 2 | import { 3 | collection, 4 | getDocs, 5 | doc, 6 | getDoc, 7 | addDoc, 8 | setDoc, 9 | updateDoc, 10 | query, 11 | where, 12 | arrayRemove, 13 | arrayUnion, 14 | deleteDoc, 15 | } from "firebase/firestore"; 16 | import { 17 | generateForm, 18 | generateFormPreview, 19 | } from "components/helpers/generateForm"; 20 | import { 21 | ERR_NOT_AUTHORISED, 22 | ERR_SAVING_FAILED, 23 | SUCCESS_SAVED, 24 | } from "data/statusMessages"; 25 | 26 | const formCollection = "forms"; 27 | 28 | const getFormsFromFirebase = async (uid, dispatchCallback, loadDispatch) => { 29 | try { 30 | const q = query(collection(db, formCollection), where("uid", "==", uid)); 31 | const qSnapshot = await getDocs(q); 32 | const formsData = []; 33 | qSnapshot.forEach((form) => { 34 | formsData.push( 35 | generateFormPreview( 36 | form.id, 37 | form.data().name, 38 | form.data().img, 39 | form.data().date.toDate().toDateString(), 40 | form.data().shared 41 | ) 42 | ); 43 | }); 44 | dispatchCallback(formsData); 45 | loadDispatch(); 46 | } catch (e) { 47 | console.error(e); 48 | } 49 | }; 50 | 51 | const getForm = async (uid, formId, dispatchCallback) => { 52 | try { 53 | const docSnap = await getDoc(doc(db, formCollection, formId)); 54 | const data = docSnap.data(); 55 | if (data.uid !== uid) { 56 | dispatchCallback({ error: ERR_NOT_AUTHORISED }); 57 | } else { 58 | if (docSnap.exists()) 59 | dispatchCallback({ 60 | form: generateForm( 61 | formId, 62 | data.theme, 63 | data.title, 64 | data.description, 65 | data.questions 66 | ), 67 | }); 68 | else console.error("No such form exists"); 69 | } 70 | } catch (e) { 71 | console.error(e); 72 | } 73 | }; 74 | 75 | const renameFormInDB = async (formId, name) => { 76 | try { 77 | const docRef = doc(db, formCollection, formId); 78 | await updateDoc(docRef, { name }); 79 | } catch (err) { 80 | console.error(err); 81 | } 82 | }; 83 | 84 | const addFormInDB = async (uid, form, dispatchCallback) => { 85 | form.uid = uid; 86 | try { 87 | const docRef = await addDoc(collection(db, "forms"), form); 88 | form.id = docRef.id; 89 | form.date = form.date.toDateString(); 90 | await dispatchCallback(form); 91 | return docRef.id; 92 | } catch (e) { 93 | console.error(e); 94 | } 95 | }; 96 | 97 | const setFormInDB = async (formId, form) => { 98 | try { 99 | const docRef = doc(db, formCollection, formId); 100 | await setDoc(docRef, form); 101 | } catch (e) { 102 | console.error(e); 103 | } 104 | }; 105 | 106 | const deleteFormFromDB = async (formId) => { 107 | const docRef = doc(db, formCollection, formId); 108 | await deleteDoc(docRef); 109 | }; 110 | const getTemplateFromDB = async (name) => { 111 | try { 112 | const docRef = doc(db, "templates", name); 113 | await getDoc(docRef); 114 | } catch (e) { 115 | console.error(e); 116 | } 117 | }; 118 | const addQuestionInDB = async (formId, question, savedCallBack) => { 119 | try { 120 | const docRef = doc(db, formCollection, formId); 121 | await updateDoc(docRef, { 122 | questions: arrayUnion(question), 123 | }); 124 | savedCallBack(SUCCESS_SAVED); 125 | } catch (e) { 126 | console.error(e); 127 | } 128 | }; 129 | 130 | const setQuestionsInDB = async (formId, questions, savedCallBack) => { 131 | try { 132 | const formRef = doc(db, formCollection, formId); 133 | await updateDoc(formRef, { questions }); 134 | savedCallBack(SUCCESS_SAVED); 135 | } catch (e) { 136 | console.error("Set Question Error:", e); 137 | savedCallBack(ERR_SAVING_FAILED); 138 | } 139 | }; 140 | 141 | const removeQuestionFromDB = async (formId, question, savedCallBack) => { 142 | const qRef = await getDoc(doc(db, formCollection, formId)); 143 | var myQ = {}; 144 | qRef.data().questions.forEach((q) => { 145 | if (q.id === question.id) myQ = q; 146 | }); 147 | try { 148 | const docRef = doc(db, formCollection, formId); 149 | await updateDoc(docRef, { 150 | questions: arrayRemove(myQ), 151 | }); 152 | savedCallBack(SUCCESS_SAVED); 153 | } catch (e) { 154 | console.error("Remove Question Error: ", e); 155 | savedCallBack(ERR_SAVING_FAILED); 156 | } 157 | }; 158 | 159 | const setThemeInDB = async (formId, theme, savedCallBack) => { 160 | try { 161 | const docRef = doc(db, formCollection, formId); 162 | await updateDoc(docRef, { theme }); 163 | savedCallBack(SUCCESS_SAVED); 164 | } catch (e) { 165 | console.error(e); 166 | savedCallBack(ERR_SAVING_FAILED); 167 | } 168 | }; 169 | 170 | const setSharedInDB = async (formId, shared, savedCallBack) => { 171 | try { 172 | const docRef = doc(db, formCollection, formId); 173 | await updateDoc(docRef, { shared }); 174 | savedCallBack(SUCCESS_SAVED); 175 | } catch (e) { 176 | console.error(e); 177 | savedCallBack(ERR_SAVING_FAILED); 178 | } 179 | }; 180 | const setFormTitleInDB = async (formId, title, savedCallBack) => { 181 | try { 182 | const docRef = doc(db, formCollection, formId); 183 | await updateDoc(docRef, { title }); 184 | savedCallBack(SUCCESS_SAVED); 185 | } catch (e) { 186 | console.error(e); 187 | savedCallBack(ERR_SAVING_FAILED); 188 | } 189 | }; 190 | const setFormDescriptionInDB = async (formId, description, savedCallBack) => { 191 | try { 192 | const docRef = doc(db, formCollection, formId); 193 | await updateDoc(docRef, { description }); 194 | savedCallBack(SUCCESS_SAVED); 195 | } catch (e) { 196 | console.error(e); 197 | savedCallBack(ERR_SAVING_FAILED); 198 | } 199 | }; 200 | 201 | export { 202 | getFormsFromFirebase, 203 | getForm, 204 | addFormInDB, 205 | setFormInDB, 206 | deleteFormFromDB, 207 | addQuestionInDB, 208 | setQuestionsInDB, 209 | removeQuestionFromDB, 210 | setThemeInDB, 211 | setSharedInDB, 212 | setFormTitleInDB, 213 | setFormDescriptionInDB, 214 | getTemplateFromDB, 215 | renameFormInDB, 216 | }; 217 | -------------------------------------------------------------------------------- /src/components/cards/QuestionCard.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import CustomDropdown from "components/dropdown/CustomDropdown"; 3 | import OptionCard from "components/cards/options/Option"; 4 | import { dropdownOptions } from "data/optionTypes"; 5 | import DisplayOptions from "components/cards/options/DisplayOptions"; 6 | import { faClone, faTrashAlt } from "@fortawesome/free-regular-svg-icons"; 7 | import Slider from "components/slider/slider"; 8 | import { useDispatch, useSelector } from "react-redux"; 9 | import { 10 | faEllipsisV, 11 | faGripHorizontal, 12 | } from "@fortawesome/free-solid-svg-icons"; 13 | import Icon from "components/icon/Icon"; 14 | import { 15 | setQuestion, 16 | removeQuestion, 17 | duplicateQuestion, 18 | setSaved, 19 | } from "store/data/form.slice"; 20 | import createQuestion from "components/helpers/createQuestion"; 21 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 22 | import { 23 | removeQuestionFromDB, 24 | setQuestionsInDB, 25 | } from "services/firebase/firestore.firebase"; 26 | import { PROGRESS_SAVING, SUCCESS_SAVED } from "data/statusMessages"; 27 | import { useFormik } from "formik"; 28 | import { questionSchema as validationSchema } from "components/helpers/validations"; 29 | 30 | const QuestionCard = ({ question, selected, onClick }) => { 31 | const { id, theme, questions } = useSelector((state) => state.form); 32 | const dispatch = useDispatch(); 33 | 34 | const { handleChange, values, errors } = useFormik({ 35 | initialValues: { title: question.title }, 36 | validationSchema, 37 | }); 38 | 39 | const setOptionType = (opType) => { 40 | const temp = { ...question }; 41 | temp.optionType = opType; 42 | dispatch(setQuestion({ id: question.id, question: temp })); 43 | }; 44 | 45 | const savedCallBack = (status) => { 46 | if (status.error) dispatch(setSaved("Error Saving data in Drive")); 47 | dispatch(setSaved(SUCCESS_SAVED)); 48 | }; 49 | 50 | const setOptions = (options) => { 51 | const temp = { ...question }; 52 | temp.options = options; 53 | dispatch(setQuestion({ id: question.id, question: temp })); 54 | dispatch(setSaved(PROGRESS_SAVING)); 55 | setQuestionsInDB(id, questions, savedCallBack); 56 | }; 57 | 58 | const toggleRequired = (e) => { 59 | const temp = { ...question }; 60 | temp.required = !question.required; 61 | dispatch(setQuestion({ id: question.id, question: temp })); 62 | 63 | dispatch(setSaved(PROGRESS_SAVING)); 64 | const tempQ = [...questions]; 65 | const ind = tempQ.findIndex((x) => x.id === question.id); 66 | tempQ[ind] = question; 67 | setQuestionsInDB(id, tempQ, savedCallBack); 68 | }; 69 | 70 | const deleteQuestion = (qid) => { 71 | dispatch(setSaved(PROGRESS_SAVING)); 72 | dispatch(removeQuestion({ id: qid })); 73 | removeQuestionFromDB(id, question, savedCallBack); 74 | }; 75 | 76 | const handleDuplicateQuestion = (id) => { 77 | dispatch(duplicateQuestion({ id: question.id, question })); 78 | dispatch(setSaved(PROGRESS_SAVING)); 79 | setQuestionsInDB(id, questions, savedCallBack); 80 | }; 81 | 82 | const saveTitle = () => { 83 | const ques = { ...question }; 84 | ques.title = values.title; 85 | if (!errors.title) { 86 | dispatch(setQuestion({ id: ques.id, question: ques })); 87 | dispatch(setSaved(PROGRESS_SAVING)); 88 | setQuestionsInDB(id, questions, savedCallBack); 89 | } 90 | }; 91 | 92 | return ( 93 |
    { 98 | onClick(question.id); 99 | }} 100 | > 101 |
    102 | 106 |
    107 | {!selected ? ( 108 |
    109 |
    110 |
    111 | {question.title} 112 |
    113 | {question.required ? ( 114 |
    *
    115 | ) : ( 116 |
    117 | )} 118 |
    119 |
    120 | 124 |
    125 |
    126 | ) : ( 127 |
    128 |
    129 | 138 | 139 | d.text === question.optionType 146 | ) 147 | ] 148 | } 149 | /> 150 |
    151 |

    152 | {errors.title ? errors.title : null} 153 |

    154 |
    155 | 160 |
    161 |
    162 | 167 | deleteQuestion(question.id)} 169 | icon={faTrashAlt} 170 | label="Delete Question" 171 | /> 172 |
    173 |
    174 |
    175 | Required 176 |
    177 | 183 |
    184 | 185 |
    186 |
    187 | )} 188 |
    189 | ); 190 | }; 191 | 192 | QuestionCard.defaultProps = { 193 | question: createQuestion(0), 194 | selected: false, 195 | }; 196 | QuestionCard.propTypes = { 197 | question: PropTypes.object, 198 | selected: PropTypes.bool, 199 | onClick: PropTypes.func.isRequired, 200 | }; 201 | 202 | export default QuestionCard; 203 | -------------------------------------------------------------------------------- /src/views/Home.js: -------------------------------------------------------------------------------- 1 | import HomeHeader from "components/layout/headers/HomeHeader"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import { useEffect, useState } from "react"; 4 | import { 5 | faEllipsisV, 6 | faChevronUp, 7 | faChevronDown, 8 | faThList, 9 | faGripHorizontal, 10 | } from "@fortawesome/free-solid-svg-icons"; 11 | import { faFolder } from "@fortawesome/free-regular-svg-icons"; 12 | import { useNavigate } from "react-router-dom"; 13 | import { formTemplates, ownershipFilters } from "data/templates"; 14 | import FormTile from "components/form/formTile/FormTile"; 15 | import Dropdown from "components/dropdown/Dropdown"; 16 | import sortIcon from "assets/sort.png"; 17 | import { useDispatch, useSelector } from "react-redux"; 18 | import Loading from "components/loaders/page.loader"; 19 | import { 20 | addFormInDB, 21 | getTemplateFromDB, 22 | renameFormInDB, 23 | } from "services/firebase/firestore.firebase"; 24 | import { setForm, setLoading } from "store/data/form.slice"; 25 | import { setForms } from "store/data/allForms.slice"; 26 | 27 | const Home = () => { 28 | const displayDate = "Yesterday"; 29 | const [gridView, setGridView] = useState(false); 30 | const [ownedFilter, setOwnedFilter] = useState(ownershipFilters[1]); 31 | const { forms, loading } = useSelector((state) => state.allForms); 32 | const { user } = useSelector((state) => state.authentication); 33 | const { theme, title, description, questions } = useSelector( 34 | (state) => state.form 35 | ); 36 | const dispatch = useDispatch(); 37 | const navigate = useNavigate(); 38 | const toggleGridView = () => { 39 | setGridView(!gridView); 40 | }; 41 | 42 | const dispatchCallBack = (form) => { 43 | dispatch(setForm(form)); 44 | }; 45 | 46 | const addNewForm = async (name, uid) => { 47 | const myForm = { 48 | theme, 49 | title, 50 | description, 51 | questions, 52 | date: new Date(), 53 | shared: true, 54 | name: "Untitled Form", 55 | }; 56 | await getTemplateFromDB(name); 57 | dispatch(setLoading(true)); 58 | const myId = await addFormInDB(uid, myForm, dispatchCallBack); 59 | console.log("id", myId); 60 | dispatch(setLoading(false)); 61 | navigate(`/create/${myId}/edit`); 62 | }; 63 | 64 | const openForm = (formId) => { 65 | navigate(`/create/${formId}/edit`); 66 | }; 67 | 68 | const removeForm = (formId) => { 69 | const temp = [...forms]; 70 | const i = temp.findIndex((e) => e.id === formId); 71 | temp.splice(i, 1); 72 | dispatch(setForms({ forms: temp })); 73 | }; 74 | const renameForm = (formId, name) => { 75 | const i = forms.findIndex((e) => e.id === formId); 76 | const temp = { ...forms[i] }; 77 | temp.name = name; 78 | dispatch(setForms({ forms: temp })); 79 | renameFormInDB(formId, name); 80 | }; 81 | 82 | return ( 83 |
    84 | 85 |
    86 |
    87 |
    88 |
    Start a new form
    89 |
    90 |
    91 |
    Template Gallery
    92 |
    93 | 94 | 95 |
    96 |
    97 |
    98 | 99 |
    100 |
    101 |
    102 |
    103 | {formTemplates.map((temp, i) => ( 104 |
    105 | {`template-${i}`} addNewForm(temp.name, user.uid, temp)} 110 | /> 111 |
    {temp.name}
    112 |
    113 | ))} 114 |
    115 |
    116 |
    117 |
    118 |
    {displayDate}
    119 | 124 |
    125 |
    129 | {gridView ? ( 130 | 134 | ) : ( 135 | 139 | )} 140 |
    141 |
    142 | sort 143 |
    144 |
    145 | 146 |
    147 |
    148 |
    149 | {loading ? ( 150 | 151 | ) : ( 152 |
    157 | {forms.length ? ( 158 | 1 && 159 | forms.map((form) => ( 160 | openForm(form.id)} 167 | /> 168 | )) 169 | ) : ( 170 |
    No Forms Yet
    171 | )} 172 |
    173 | )} 174 |
    175 |
    176 |
    177 | ); 178 | }; 179 | 180 | export default Home; 181 | --------------------------------------------------------------------------------