├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── config_override.js ├── package.json ├── public ├── _redirects ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── index.html ├── manifest.json ├── mstile-150x150.png ├── robots.txt ├── safari-pinned-tab.svg └── site.webmanifest ├── src ├── App.tsx ├── assets │ ├── fonts │ │ ├── CircularStd-Black.otf │ │ ├── CircularStd-Bold.otf │ │ ├── CircularStd-Book.otf │ │ ├── CircularStd-Light.otf │ │ └── CircularStd-Medium.otf │ ├── illustations │ │ ├── 200_EMPTY_RESPONSE.png │ │ ├── 403.png │ │ ├── 404.png │ │ ├── 500.png │ │ ├── Active.png │ │ ├── Add Questions.png │ │ ├── Create.png │ │ ├── Stats.png │ │ ├── ecofriendly.png │ │ └── landing.png │ ├── logos │ │ ├── Black-White-Circle.png │ │ ├── Black-White.png │ │ ├── Purple-White-Circle.png │ │ ├── Purple-White.png │ │ ├── White-Black-Circle.png │ │ ├── White-Black.png │ │ ├── White-Purple-Circle.png │ │ └── White-Purple.png │ └── svgs │ │ └── landing.svg ├── components │ ├── AddEditQuizFormFields.tsx │ ├── AddQuestionsSidebar.tsx │ ├── ConfirmSubmitModal.tsx │ ├── DeleteModal.tsx │ ├── Dropdown.tsx │ ├── EmptyResponse.tsx │ ├── ErrorMessage.tsx │ ├── FinishQuiz.tsx │ ├── Footer.tsx │ ├── GridWrapper.tsx │ ├── Layout.tsx │ ├── Modal.tsx │ ├── NavBar.tsx │ ├── Option.tsx │ ├── PaginationButton.tsx │ ├── Player.tsx │ ├── QuizCard.tsx │ ├── QuizModalContents.tsx │ ├── Sidebar.tsx │ ├── SidebarQuestion.tsx │ ├── Svgs.tsx │ └── forms │ │ ├── AddEditQuestionFormFields.tsx │ │ ├── AddQuestionForm.tsx │ │ ├── FiltersForm.tsx │ │ ├── QuizForm.tsx │ │ └── UpdateQuestionForm.tsx ├── hooks │ └── useKeyPress.tsx ├── index.css ├── index.tsx ├── pages │ ├── AddQuestions.tsx │ ├── Attempts.tsx │ ├── CreateQuiz.tsx │ ├── Dashboard.tsx │ ├── Landing.tsx │ ├── PlayerScreen.tsx │ ├── QuizResponse.tsx │ ├── Quizes.tsx │ ├── SignIn.tsx │ ├── Signup.tsx │ ├── UpdateQuestion.tsx │ ├── UpdateQuiz.tsx │ ├── UserProfile.tsx │ └── stats │ │ ├── StatisticsAllQuestions.tsx │ │ ├── StatisticsByQuiz.tsx │ │ └── StatisticsByQuizQuestionsId.tsx ├── react-app-env.d.ts └── shared │ ├── constants.ts │ ├── formatDate.ts │ ├── interfaces.ts │ ├── queries.ts │ ├── sampleQuizes.ts │ ├── theme.tsx │ ├── urls.ts │ ├── utils.ts │ └── validationSchema.ts ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.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 | .env 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit", 5 | "source.organizeImports": "explicit", 6 | "source.fixAll.stylelint": "explicit" 7 | }, 8 | "eslint.validate": [ 9 | "html", 10 | "javascript", 11 | "javascriptreact", 12 | "typescript", 13 | "typescriptreact" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Vishwajeet Raj 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quizco - Quiz Builder and Assessment Application 2 | 3 | **Live**: https://quizco-app.netlify.app/ 4 | **Frontend**: https://github.com/vishwajeetraj11/quizco-frontend 5 | **Backend**: https://github.com/vishwajeetraj11/quizco-backend/ 6 | AI Quiz Builder Enabled only for Admin 7 | 8 | ## 💻 Screens 9 | Landing Page | Landing Page (Scrolled Down) 10 | :-------------------------:|:-------------------------: 11 | Quizco Landing Page | Quizco Landing Page (Scrolled Down) 12 | Quizzes Page | Dashboard Page 13 | Quizzes Page | Dashboard Page 14 | Selected Quiz in Dashboard | Add/Update Questions 15 | | 16 | Player Screen | Finish Quiz 17 | | 18 | Attempts Screen | Statistics Screen 19 | | 20 | View All Questions Screen | Question Statistics 21 | | 22 | 23 | Running it Locally: 24 | 1. Go to Clerk Dashboard and create an application. 25 | 2. Go to Clerk Dashboard > Your Application > Paths > Change the paths as in this image. 26 | image 27 | 4. Make a MongoDB cluster and copy its connection string. 28 | 5. Copy Frontend API Key and Backend API Key. 29 | 6. Backend env. 30 | - DB_PASSWORD= 31 | - MONGODB_URI= 32 | - PORT= 33 | - CLERK_API_KEY= 34 | - CLERK_API_URL=https://api.clerk.dev 35 | 7. Frontend env. 36 | - REACT_APP_CLERK_FRONTEND_API= 37 | 8. Install Dependencies. 38 | 9. Run Backend (```yarn dev```), Run Frontend (```yarn start```) 39 | 40 | 41 | -------------------------------------------------------------------------------- /config_override.js: -------------------------------------------------------------------------------- 1 | 2 | const webpack = require("webpack") 3 | 4 | module.exports = function override(config, env) { 5 | //do stuff with the webpack config... 6 | config.resolve.fallback = { 7 | ...config.resolve.fallback, 8 | stream: require.resolve("stream-browserify"), 9 | buffer: require.resolve("buffer"), 10 | } 11 | config.resolve.extensions = [...config.resolve.extensions, ".ts", ".js"] 12 | config.plugins = [ 13 | ...config.plugins, 14 | new webpack.ProvidePlugin({ 15 | process: "process/browser", 16 | Buffer: ["buffer", "Buffer"], 17 | }), 18 | ] 19 | // console.log(config.resolve) 20 | // console.log(config.plugins) 21 | 22 | return config 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quizicon", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@clerk/clerk-react": "^2.10.1", 7 | "@material-ui/core": "^4.12.3", 8 | "@testing-library/jest-dom": "^5.16.1", 9 | "@testing-library/react": "^12.1.2", 10 | "@testing-library/user-event": "^13.5.0", 11 | "@types/jest": "^27.4.0", 12 | "@types/node": "^16.11.19", 13 | "@types/react": "^17.0.38", 14 | "@types/react-dom": "^17.0.11", 15 | "ag-grid-community": "^27.0.1", 16 | "ag-grid-react": "^27.0.1", 17 | "axios": "^0.25.0", 18 | "buffer": "^6.0.3", 19 | "formik": "^2.2.9", 20 | "material-ui-chip-input": "^2.0.0-beta.2", 21 | "notistack": "^1.0.10", 22 | "pdfmake": "^0.2.4", 23 | "process": "^0.11.10", 24 | "react": "^17.0.2", 25 | "react-dom": "^17.0.2", 26 | "react-icons": "^4.3.1", 27 | "react-query": "^3.34.12", 28 | "react-router-dom": "^6.2.1", 29 | "react-scripts": "5.0.0", 30 | "stream-browserify": "^3.0.0", 31 | "typescript": "^4.5.4", 32 | "web-vitals": "^2.1.3", 33 | "xlsx": "^0.18.2", 34 | "yup": "^0.32.11" 35 | }, 36 | "scripts": { 37 | "start": "react-scripts start", 38 | "build": "react-scripts build", 39 | "test": "react-scripts test", 40 | "eject": "react-scripts eject" 41 | }, 42 | "eslintConfig": { 43 | "extends": [ 44 | "react-app", 45 | "react-app/jest" 46 | ] 47 | }, 48 | "browserslist": { 49 | "production": [ 50 | ">0.2%", 51 | "not dead", 52 | "not op_mini all" 53 | ], 54 | "development": [ 55 | "last 1 chrome version", 56 | "last 1 firefox version", 57 | "last 1 safari version" 58 | ] 59 | }, 60 | "devDependencies": { 61 | "mini-css-extract-plugin": "2.4.5", 62 | "react-app-rewired": "^2.2.1" 63 | }, 64 | "resolutions": { 65 | "mini-css-extract-plugin": "2.4.5" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 32 | Quizco 33 | 34 | 35 | 36 | 37 |
38 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Quizco", 3 | "name": "Quiz Builder and Assessment Tool", 4 | "icons": [ 5 | { 6 | "src": "favicon-32x32.png", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "android-chrome-192x192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "android-chrome-512x512.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 | } -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ClerkLoaded, 3 | ClerkProvider, 4 | SignedIn, 5 | SignedOut, 6 | } from "@clerk/clerk-react"; 7 | import { Navigate, Route, Routes, useNavigate } from "react-router-dom"; 8 | import { Layout } from "./components/Layout"; 9 | import { AddQuestions } from "./pages/AddQuestions"; 10 | import { Attempts } from "./pages/Attempts"; 11 | import { CreateQuiz } from "./pages/CreateQuiz"; 12 | import { Dashboard } from "./pages/Dashboard"; 13 | import { Landing } from "./pages/Landing"; 14 | import { PlayerScreen } from "./pages/PlayerScreen"; 15 | import { Quizes } from "./pages/Quizes"; 16 | import { QuizResponse } from "./pages/QuizResponse"; 17 | import { SignInPage } from "./pages/SignIn"; 18 | import SignUpPage from "./pages/Signup"; 19 | import { StatisticsAllQuestions } from "./pages/stats/StatisticsAllQuestions"; 20 | import { StatisticsByQuiz } from "./pages/stats/StatisticsByQuiz"; 21 | import { StatisticsByQuizQuestionsId } from "./pages/stats/StatisticsByQuizQuestionsId"; 22 | import { UpdateQuestion } from "./pages/UpdateQuestion"; 23 | import { UpdateQuiz } from "./pages/UpdateQuiz"; 24 | import { UserProfilePage } from "./pages/UserProfile"; 25 | 26 | function App() { 27 | const frontendApi = process.env.REACT_APP_CLERK_FRONTEND_API; 28 | const navigate = useNavigate(); 29 | 30 | return ( 31 | navigate(to)}> 32 | 33 | 34 | 35 | 36 | } /> 37 | } /> 38 | } /> 39 | } 42 | /> 43 | } 46 | /> 47 | } 50 | /> 51 | } 54 | /> 55 | } /> 56 | } /> 57 | } /> 58 | } /> 59 | } 62 | /> 63 | } /> 64 | } /> 65 | 66 | 67 | 68 | 69 | } /> 70 | } /> 71 | } /> 72 | } /> 73 | {/* } /> */} 74 | 75 | 76 | 77 | 78 | 79 | ); 80 | } 81 | 82 | export default App; 83 | -------------------------------------------------------------------------------- /src/assets/fonts/CircularStd-Black.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/fonts/CircularStd-Black.otf -------------------------------------------------------------------------------- /src/assets/fonts/CircularStd-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/fonts/CircularStd-Bold.otf -------------------------------------------------------------------------------- /src/assets/fonts/CircularStd-Book.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/fonts/CircularStd-Book.otf -------------------------------------------------------------------------------- /src/assets/fonts/CircularStd-Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/fonts/CircularStd-Light.otf -------------------------------------------------------------------------------- /src/assets/fonts/CircularStd-Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/fonts/CircularStd-Medium.otf -------------------------------------------------------------------------------- /src/assets/illustations/200_EMPTY_RESPONSE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/illustations/200_EMPTY_RESPONSE.png -------------------------------------------------------------------------------- /src/assets/illustations/403.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/illustations/403.png -------------------------------------------------------------------------------- /src/assets/illustations/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/illustations/404.png -------------------------------------------------------------------------------- /src/assets/illustations/500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/illustations/500.png -------------------------------------------------------------------------------- /src/assets/illustations/Active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/illustations/Active.png -------------------------------------------------------------------------------- /src/assets/illustations/Add Questions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/illustations/Add Questions.png -------------------------------------------------------------------------------- /src/assets/illustations/Create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/illustations/Create.png -------------------------------------------------------------------------------- /src/assets/illustations/Stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/illustations/Stats.png -------------------------------------------------------------------------------- /src/assets/illustations/ecofriendly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/illustations/ecofriendly.png -------------------------------------------------------------------------------- /src/assets/illustations/landing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/illustations/landing.png -------------------------------------------------------------------------------- /src/assets/logos/Black-White-Circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/logos/Black-White-Circle.png -------------------------------------------------------------------------------- /src/assets/logos/Black-White.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/logos/Black-White.png -------------------------------------------------------------------------------- /src/assets/logos/Purple-White-Circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/logos/Purple-White-Circle.png -------------------------------------------------------------------------------- /src/assets/logos/Purple-White.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/logos/Purple-White.png -------------------------------------------------------------------------------- /src/assets/logos/White-Black-Circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/logos/White-Black-Circle.png -------------------------------------------------------------------------------- /src/assets/logos/White-Black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/logos/White-Black.png -------------------------------------------------------------------------------- /src/assets/logos/White-Purple-Circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/logos/White-Purple-Circle.png -------------------------------------------------------------------------------- /src/assets/logos/White-Purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vishwajeetraj11/quizco-frontend/5ff56bdd66043e5125ddb5f59ee13878adf43729/src/assets/logos/White-Purple.png -------------------------------------------------------------------------------- /src/assets/svgs/landing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/components/AddEditQuizFormFields.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FormControl, 3 | InputLabel, 4 | MenuItem, 5 | Select, 6 | TextField, 7 | } from "@material-ui/core"; 8 | import { useFormikContext } from "formik"; 9 | import ChipInput from "material-ui-chip-input"; 10 | import { IQuiz } from "../shared/interfaces"; 11 | 12 | export const AddEditQuizFormFields = ({ id }: { id?: string }) => { 13 | const { touched, errors, values, handleBlur, handleChange, setFieldValue } = 14 | useFormikContext(); 15 | 16 | return ( 17 | <> 18 |
19 | 30 |
31 | 32 |
33 | 45 |
46 | {id && ( 47 |
48 | 49 | Status 50 | 64 | 65 |
66 | )} 67 |
68 | { 81 | setFieldValue("tags", values.tags.concat(chip)); 82 | }} 83 | onDelete={(chip, indexChip) => { 84 | const tags = values.tags.filter((_, i) => i !== indexChip); 85 | setFieldValue("tags", tags); 86 | }} 87 | /> 88 |
89 | 90 | ); 91 | }; 92 | -------------------------------------------------------------------------------- /src/components/AddQuestionsSidebar.tsx: -------------------------------------------------------------------------------- 1 | import { useMediaQuery } from "@material-ui/core"; 2 | import { useEffect, useState } from "react"; 3 | import { BsLayoutSidebarInset } from "react-icons/bs"; 4 | import { IQuestion } from "../shared/interfaces"; 5 | import { SidebarQuestion } from "./SidebarQuestion"; 6 | 7 | interface Props { 8 | questions: IQuestion[]; 9 | } 10 | 11 | export const AddQuestionsSidebar: React.FC = ({ questions }) => { 12 | const [showQuestions, setShowQuestions] = useState(false); 13 | const [expandQuestion, setExpandQuestion] = useState(""); 14 | const isMobile = useMediaQuery("(max-width:600px)"); 15 | const [expanded, setExpanded] = useState(true); 16 | 17 | useEffect(() => { 18 | if (expanded) { 19 | const timeout = setTimeout(() => { 20 | setShowQuestions(true); 21 | }, 300); 22 | return () => { 23 | clearTimeout(timeout); 24 | setShowQuestions(false); 25 | setExpandQuestion(""); 26 | }; 27 | } 28 | }, [expanded]); 29 | 30 | useEffect(() => { 31 | if (isMobile) { 32 | setExpanded(false); 33 | } else { 34 | setExpanded(true); 35 | } 36 | }, [isMobile]); 37 | 38 | return ( 39 |
43 |
setExpanded((p) => !p)} 45 | className="w-8 h-8 p-2 flex justify-center items-center cursor-pointer hover:bg-gray-200 rounded-full" 46 | > 47 | 48 |
49 |
50 | {questions.length > 0 ? ( 51 | questions.map((question, index) => ( 52 | 62 | )) 63 | ) : ( 64 | <> 65 |

No Questions Added Yet.

66 | 67 | )} 68 |
69 |
70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /src/components/ConfirmSubmitModal.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@material-ui/core"; 2 | import { useEffect, useState } from "react"; 3 | import { IResponse } from "../shared/interfaces"; 4 | 5 | interface Props { 6 | handleConfirmSubmitModalClose: () => void; 7 | isQuizCorrectAnsLoading: boolean; 8 | setFetchCorrectAns: React.Dispatch>; 9 | responses: [] | IResponse[]; 10 | } 11 | 12 | export const ConfirmSubmitModalContent: React.FC = ({ 13 | handleConfirmSubmitModalClose, 14 | isQuizCorrectAnsLoading, 15 | setFetchCorrectAns, 16 | responses, 17 | }) => { 18 | const [marked, setMarked] = useState(0); 19 | const [unmarked, setUnmarked] = useState(0); 20 | 21 | useEffect(() => { 22 | responses.forEach((res) => { 23 | if (res.response === "") { 24 | setUnmarked((p) => p + 1); 25 | } else { 26 | setMarked((p) => p + 1); 27 | } 28 | }); 29 | }, [responses]); 30 | 31 | return ( 32 | <> 33 |

34 | Submit Quiz 35 |

36 |
37 |

38 | Are you sure you want to submit? 39 |

40 |
41 |

42 | Questions Attempted: {marked} 43 |

44 |

45 | Questions Unattempted: {unmarked} 46 |

47 |
48 | {marked === 0 && ( 49 |

50 | Sorry can't allow you to omit for all questions. 51 |

52 | )} 53 |
54 |
55 |
56 | 57 |
58 | 66 |
67 |
68 |
69 | 70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /src/components/DeleteModal.tsx: -------------------------------------------------------------------------------- 1 | import { Backdrop, Box, Button, Fade, Modal } from "@material-ui/core"; 2 | import { modalStyle } from "../shared/constants"; 3 | 4 | interface IDeleteModal { 5 | deleteModalActive: boolean; 6 | handleDeleteModalClose: () => void; 7 | deleteLoading: boolean; 8 | onDelete: () => void; 9 | resource: string; 10 | confirmMessage?: string; 11 | informMessage?: string; 12 | modalTitle?: string; 13 | } 14 | 15 | export const DeleteModal = ({ 16 | deleteModalActive, 17 | handleDeleteModalClose, 18 | deleteLoading, 19 | onDelete, 20 | resource, 21 | confirmMessage = "Are you sure?", 22 | informMessage = `You want to delete this ${resource || "resource"}.`, 23 | modalTitle = `Delete ${resource || "Resource"}`, 24 | }: IDeleteModal) => ( 25 | 36 | 37 | 38 |

39 | {modalTitle} 40 |

41 |
42 |

{confirmMessage}

43 |

{informMessage}

44 |
45 |
46 |
47 | 50 |
51 | 59 |
60 |
61 |
62 |
63 |
64 |
65 | ); 66 | -------------------------------------------------------------------------------- /src/components/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | alpha, 3 | Button, 4 | Menu, 5 | MenuItem, 6 | MenuProps, 7 | styled, 8 | } from "@material-ui/core"; 9 | import { Column, ColumnApi, GridApi } from "ag-grid-community"; 10 | import { Buffer } from "buffer"; 11 | import pdfMake from "pdfmake/build/pdfmake"; 12 | import pdfFonts from "pdfmake/build/vfs_fonts"; 13 | import * as React from "react"; 14 | import { GoChevronDown } from "react-icons/go"; 15 | import { GrDocumentCsv } from "react-icons/gr"; 16 | import { SiMicrosoftexcel } from "react-icons/si"; 17 | import { VscFilePdf } from "react-icons/vsc"; 18 | import * as xlsx from "xlsx"; 19 | pdfMake.vfs = pdfFonts.pdfMake.vfs; 20 | 21 | interface Props { 22 | selected: string[]; 23 | gridApi: GridApi | undefined; 24 | gridColumnApi?: ColumnApi; 25 | excludedColumns?: string[]; 26 | quizId?: string; 27 | } 28 | 29 | export const DownloadButton: React.FC = ({ 30 | selected, 31 | gridApi, 32 | gridColumnApi, 33 | excludedColumns, 34 | quizId, 35 | }) => { 36 | const [anchorEl, setAnchorEl] = React.useState(null); 37 | const open = Boolean(anchorEl); 38 | const handleClick = (event: React.MouseEvent) => { 39 | setAnchorEl(event.currentTarget); 40 | }; 41 | const handleClose = () => { 42 | setAnchorEl(null); 43 | }; 44 | 45 | const downloadCSV = () => { 46 | const columnDefinitions = gridApi?.getColumnDefs(); 47 | 48 | const columnKeys: string[] = []; 49 | 50 | columnDefinitions?.forEach((el: any) => { 51 | if (excludedColumns?.includes(el.field)) return; 52 | columnKeys.push(el.field); 53 | }); 54 | 55 | gridApi?.exportDataAsCsv({ 56 | onlySelected: !!selected.length, 57 | columnKeys, 58 | fileName: `quiz_csv_${quizId}`, 59 | }); 60 | }; 61 | 62 | const getDocDefinition = ( 63 | printParams: any, 64 | agGridApi: GridApi | undefined, 65 | agGridColumnApi: ColumnApi | undefined, 66 | excludedColumns?: string[] 67 | ) => { 68 | const { 69 | PDF_HEADER_COLOR, 70 | PDF_INNER_BORDER_COLOR, 71 | PDF_OUTER_BORDER_COLOR, 72 | PDF_ODD_BKG_COLOR, 73 | PDF_EVEN_BKG_COLOR, 74 | PDF_HEADER_HEIGHT, 75 | PDF_ROW_HEIGHT, 76 | PDF_PAGE_ORITENTATION, 77 | PDF_WITH_CELL_FORMATTING, 78 | PDF_SELECTED_ROWS_ONLY, 79 | } = printParams; 80 | 81 | return (function () { 82 | const columnGroupsToExport = getColumnGroupsToExport(); 83 | 84 | const columnsToExport = getColumnsToExport(); 85 | 86 | const widths = getExportedColumnsWidths(columnsToExport); 87 | 88 | const rowsToExport = getRowsToExport(columnsToExport); 89 | 90 | const body = columnGroupsToExport 91 | ? [columnGroupsToExport, columnsToExport, ...rowsToExport] 92 | : [columnsToExport, ...rowsToExport]; 93 | 94 | const headerRows = columnGroupsToExport ? 2 : 1; 95 | 96 | // const header = PDF_WITH_HEADER_IMAGE 97 | // ? { 98 | // image: 'ag-grid-logo', 99 | // width: 150, 100 | // alignment: 'center', 101 | // margin: [0, 10, 0, 10], 102 | // } 103 | // : undefined; 104 | 105 | /* const footer = PDF_WITH_FOOTER_PAGE_COUNT 106 | ? function (currentPage: any, pageCount: any) { 107 | return { 108 | text: currentPage.toString() + ' of ' + pageCount, 109 | margin: [20], 110 | }; 111 | } 112 | : null;*/ 113 | 114 | // const pageMargins = [ 115 | // 10, 20, 116 | // // PDF_WITH_HEADER_IMAGE ? 70 : 20, 117 | // 10, 20 118 | // // PDF_WITH_FOOTER_PAGE_COUNT ? 40 : 10, 119 | // ]; 120 | 121 | const pageMargins: [number, number, number, number] = [10, 20, 10, 20]; 122 | 123 | const heights = (rowIndex: number) => 124 | rowIndex < headerRows ? PDF_HEADER_HEIGHT : PDF_ROW_HEIGHT; 125 | 126 | const fillColor = (rowIndex: number, node: any, columnIndex: number) => { 127 | if (node?.table?.headerRows && rowIndex < node?.table?.headerRows) { 128 | return PDF_HEADER_COLOR; 129 | } 130 | return rowIndex % 2 === 0 ? PDF_ODD_BKG_COLOR : PDF_EVEN_BKG_COLOR; 131 | }; 132 | 133 | const hLineWidth = (i: number, node: any) => 1; 134 | 135 | const vLineWidth = (i: number, node: any) => 136 | i === 0 || i === node?.table?.widths?.length ? 1 : 0; 137 | 138 | const hLineColor = (i: number, node: any) => 139 | i === 0 || i === node.table.body.length 140 | ? PDF_OUTER_BORDER_COLOR 141 | : PDF_INNER_BORDER_COLOR; 142 | 143 | const vLineColor = (i: number, node: any) => 144 | i === 0 || i === node?.table?.widths?.length 145 | ? PDF_OUTER_BORDER_COLOR 146 | : PDF_INNER_BORDER_COLOR; 147 | 148 | const docDefintiion: any = { 149 | pageOrientation: PDF_PAGE_ORITENTATION, 150 | content: [ 151 | { 152 | style: "myTable", 153 | table: { 154 | headerRows, 155 | widths, 156 | body, 157 | heights, 158 | }, 159 | layout: { 160 | fillColor, 161 | hLineWidth, 162 | vLineWidth, 163 | hLineColor, 164 | vLineColor, 165 | }, 166 | }, 167 | ], 168 | styles: { 169 | myTable: { 170 | margin: [0, 0, 0, 0], 171 | }, 172 | tableHeader: { 173 | bold: true, 174 | margin: [0, PDF_HEADER_HEIGHT / 3, 0, 0], 175 | }, 176 | tableCell: { 177 | // margin: [0, 15, 0, 0] 178 | }, 179 | }, 180 | pageMargins, 181 | }; 182 | 183 | return docDefintiion; 184 | })(); 185 | 186 | function getColumnGroupsToExport() { 187 | const displayedColumnGroups: any[] | null | undefined = 188 | agGridColumnApi?.getAllDisplayedColumnGroups(); 189 | 190 | const isColumnGrouping = displayedColumnGroups?.some((col: any) => 191 | col.hasOwnProperty("children") 192 | ); 193 | 194 | if (!isColumnGrouping) { 195 | return null; 196 | } 197 | 198 | const columnGroupsToExport: any = []; 199 | 200 | displayedColumnGroups?.forEach((colGroup: any) => { 201 | const isColSpanning = colGroup.children.length > 1; 202 | let numberOfEmptyHeaderCellsToAdd = 0; 203 | 204 | if (isColSpanning) { 205 | const headerCell = createHeaderCell(colGroup); 206 | columnGroupsToExport.push(headerCell); 207 | // subtract 1 as the column group counts as a header 208 | numberOfEmptyHeaderCellsToAdd--; 209 | } 210 | 211 | // add an empty header cell now for every column being spanned 212 | colGroup.displayedChildren.forEach((childCol: any) => { 213 | const pdfExportOptions = getPdfExportOptions(childCol.getColId()); 214 | if (!pdfExportOptions || !pdfExportOptions.skipColumn) { 215 | numberOfEmptyHeaderCellsToAdd++; 216 | } 217 | }); 218 | 219 | for (let i = 0; i < numberOfEmptyHeaderCellsToAdd; i++) { 220 | columnGroupsToExport.push({}); 221 | } 222 | }); 223 | 224 | return columnGroupsToExport; 225 | } 226 | 227 | function getColumnsToExport() { 228 | const columnsToExport: any = []; 229 | 230 | agGridColumnApi?.getAllDisplayedColumns().forEach((col: Column) => { 231 | const pdfExportOptions = getPdfExportOptions(col.getColId()); 232 | const colId = col.getColId(); 233 | if (excludedColumns?.includes(colId)) { 234 | return; 235 | } 236 | if (pdfExportOptions && pdfExportOptions.skipColumn) { 237 | return; 238 | } 239 | const headerCell = createHeaderCell(col); 240 | columnsToExport.push(headerCell); 241 | }); 242 | 243 | return columnsToExport; 244 | } 245 | 246 | function getRowsToExport(columnsToExport: any) { 247 | const rowsToExport: any = []; 248 | 249 | agGridApi?.forEachNodeAfterFilterAndSort((node) => { 250 | if (PDF_SELECTED_ROWS_ONLY && !node.isSelected()) { 251 | return; 252 | } 253 | const rowToExport = columnsToExport.map(({ colId }: any) => { 254 | const cellValue = agGridApi.getValue(colId, node); 255 | const tableCell = createTableCell(cellValue, colId); 256 | return tableCell; 257 | }); 258 | rowsToExport.push(rowToExport); 259 | }); 260 | 261 | return rowsToExport; 262 | } 263 | 264 | function getExportedColumnsWidths(columnsToExport: any) { 265 | return columnsToExport.map(() => 100 / columnsToExport.length + "%"); 266 | } 267 | 268 | function createHeaderCell(col: any) { 269 | const headerCell: any = {}; 270 | 271 | const isColGroup = col.hasOwnProperty("children"); 272 | 273 | if (isColGroup) { 274 | headerCell.text = col.originalColumnGroup.colGroupDef.headerName; 275 | headerCell.colSpan = col.children.length; 276 | headerCell.colId = col.groupId; 277 | } else { 278 | let headerName = col.colDef.headerName; 279 | 280 | if (col.sort) { 281 | headerName += ` (${col.sort})`; 282 | } 283 | if (col.filterActive) { 284 | headerName += ` [FILTERING]`; 285 | } 286 | 287 | headerCell.text = headerName; 288 | headerCell.colId = col.getColId(); 289 | } 290 | 291 | headerCell["style"] = "tableHeader"; 292 | 293 | return headerCell; 294 | } 295 | 296 | function createTableCell(cellValue: any, colId: any) { 297 | const tableCell: any = { 298 | text: cellValue !== undefined ? cellValue : "", 299 | style: "tableCell", 300 | }; 301 | 302 | const pdfExportOptions = getPdfExportOptions(colId); 303 | 304 | if (pdfExportOptions) { 305 | const { styles } = pdfExportOptions; 306 | 307 | if (PDF_WITH_CELL_FORMATTING && styles) { 308 | Object.entries(styles).forEach( 309 | ([key, value]: any) => (tableCell[key] = value) 310 | ); 311 | } 312 | } 313 | 314 | return tableCell; 315 | } 316 | 317 | function getPdfExportOptions(colId: any) { 318 | const col: any = agGridColumnApi?.getColumn(colId); 319 | return col.colDef.pdfExportOptions; 320 | } 321 | }; 322 | 323 | const printDoc = ( 324 | printParams: any, 325 | gridApi: GridApi | undefined, 326 | columnApi: ColumnApi | undefined, 327 | excludedColumns?: string[] 328 | ) => { 329 | const docDefinition = getDocDefinition( 330 | printParams, 331 | gridApi, 332 | columnApi, 333 | excludedColumns 334 | ); 335 | pdfMake.createPdf(docDefinition).download(`quiz_pdf_${quizId}`); 336 | }; 337 | 338 | const downloadPDF = () => { 339 | const isAnyRowSelected = gridApi?.getSelectedRows(); 340 | 341 | const printParams = { 342 | PDF_HEADER_COLOR: "#f8f8f8", 343 | PDF_INNER_BORDER_COLOR: "#dde2eb", 344 | PDF_OUTER_BORDER_COLOR: "#babfc7", 345 | PDF_PAGE_ORITENTATION: "landscape", 346 | PDF_HEADER_HEIGHT: 25, 347 | PDF_ROW_HEIGHT: 15, 348 | PDF_ODD_BKG_COLOR: "#fcfcfc", 349 | PDF_EVEN_BKG_COLOR: "#fff", 350 | PDF_WITH_CELL_FORMATTING: true, 351 | PDF_SELECTED_ROWS_ONLY: !!isAnyRowSelected?.length, 352 | }; 353 | printDoc(printParams, gridApi, gridColumnApi, excludedColumns); 354 | }; 355 | 356 | const downloadExcel = () => { 357 | const columnDefinitions = gridApi?.getColumnDefs(); 358 | 359 | // Remove the column having checkboxes 360 | columnDefinitions?.shift(); 361 | 362 | const columnKeys: string[] = []; 363 | 364 | columnDefinitions?.forEach((el: any) => { 365 | if (excludedColumns?.includes(el.field)) return; 366 | columnKeys.push(el.field); 367 | }); 368 | 369 | const blob: string | undefined = gridApi?.getDataAsCsv({ 370 | onlySelected: !!selected.length, 371 | columnKeys, 372 | }); 373 | 374 | if (blob) { 375 | const arrayOfArrayCsv = blob 376 | .toString() 377 | .split("\n") 378 | .map((row: string) => { 379 | row = row.replaceAll('"', ""); 380 | row = row.replaceAll("\r", ""); 381 | return row.split(","); 382 | }); 383 | 384 | const wb = xlsx.utils.book_new(); 385 | const newWs = xlsx.utils.aoa_to_sheet(arrayOfArrayCsv); 386 | xlsx.utils.book_append_sheet(wb, newWs); 387 | const rawExcel = xlsx.write(wb, { type: "base64" }); 388 | const file = Buffer.from(rawExcel, "base64"); 389 | 390 | const excelFile = new Blob([file], { 391 | type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 392 | }); 393 | 394 | const a = document.createElement("a"); 395 | a.download = `quiz_excel_${quizId}`; 396 | a.href = window.URL.createObjectURL(excelFile); 397 | a.click(); 398 | } 399 | }; 400 | 401 | return ( 402 |
403 | 416 | 425 | 426 | 427 |

Excel

428 |
429 | 430 | 431 |

CSV

432 |
433 | 434 | 435 |

PDF

436 |
437 |
438 |
439 | ); 440 | }; 441 | 442 | const StyledMenu = styled((props: MenuProps) => ( 443 | 455 | ))(({ theme }) => ({ 456 | "& .MuiPaper-root": { 457 | borderRadius: 6, 458 | marginTop: theme.spacing(6), 459 | minWidth: 180, 460 | boxShadow: "0 5px 20px rgba(0,0,0,0.07)", 461 | "& .MuiMenu-list": { 462 | padding: "4px 0", 463 | }, 464 | "& .MuiMenuItem-root": { 465 | "& .MuiSvgIcon-root": { 466 | fontSize: 18, 467 | color: theme.palette.text.secondary, 468 | marginRight: theme.spacing(1.5), 469 | }, 470 | "&:active": { 471 | backgroundColor: alpha( 472 | theme.palette.primary.main, 473 | theme.palette.action.selectedOpacity 474 | ), 475 | }, 476 | }, 477 | }, 478 | })); 479 | -------------------------------------------------------------------------------- /src/components/EmptyResponse.tsx: -------------------------------------------------------------------------------- 1 | import EmptyMail from "../assets/illustations/200_EMPTY_RESPONSE.png"; 2 | import { emptyResponseMessages } from "../shared/constants"; 3 | 4 | interface Props { 5 | resource: 6 | | "Quiz" 7 | | "Attempt" 8 | | "Dashboard Quizes" 9 | | "Quiz Questions" 10 | | "All Active Quizes" 11 | | "All Active Filtered Quizes" 12 | | "Responses"; 13 | } 14 | 15 | export const EmptyResponse: React.FC = ({ resource }) => { 16 | const attempt = resource === "Attempt"; 17 | const dashboard = resource === "Dashboard Quizes"; 18 | const quizQuestions = resource === "Quiz Questions"; 19 | const mainQuizes = resource === "All Active Quizes"; 20 | const allActiveFilteredQuizes = resource === "All Active Filtered Quizes"; 21 | const responses = resource === "Responses"; 22 | 23 | // const img = attempt ? EmptyMail : ""; 24 | const img = EmptyMail; 25 | const text = attempt 26 | ? emptyResponseMessages.attempt 27 | : dashboard 28 | ? emptyResponseMessages.dashboardQuizes 29 | : quizQuestions 30 | ? emptyResponseMessages.quizQuestions 31 | : mainQuizes 32 | ? emptyResponseMessages.mainQuizes 33 | : allActiveFilteredQuizes 34 | ? emptyResponseMessages.filteredQuizes 35 | : responses 36 | ? emptyResponseMessages.responses 37 | : [""]; 38 | 39 | return ( 40 |
41 | {img && ( 42 |
43 | Empty Attempt Illustration 48 |
49 | )} 50 | {text && 51 | text.map((text, i) => ( 52 |

56 | {text} 57 |

58 | ))} 59 |
60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /src/components/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import SadFace from "../assets/illustations/500.png"; 2 | import { errorMessages } from "../shared/constants"; 3 | import HandStop from "./../assets/illustations/403.png"; 4 | import Response404 from "./../assets/illustations/404.png"; 5 | interface IProps { 6 | statusCode: number; 7 | message?: string | undefined; 8 | resource?: "Quiz" | "Question" | "Attempt" | "Questions"; 9 | } 10 | 11 | export const ErrorMessage: React.FC = ({ 12 | statusCode, 13 | message, 14 | resource, 15 | }) => { 16 | const notFound = statusCode === 404; 17 | const auth403 = statusCode === 403; 18 | 19 | // const serviceUnavailable = statusCode === 503; // Heroku 20 | 21 | return ( 22 |
23 | {notFound ? ( 24 | <> 25 |
26 |

27 |
28 | Hand Stop Illustration 33 |
34 |

35 | {message || errorMessages.notFound(resource)} 36 |

37 | 38 | ) : auth403 ? ( 39 | <> 40 |
41 | Hand Stop Illustration 46 |
47 |

48 | {message || errorMessages.auth403} 49 |

50 | 51 | ) : ( 52 | <> 53 |
54 | Something Went Wrong Illustration 59 |
60 |

61 | {errorMessages.default} 62 |

63 | 64 | )} 65 |
66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /src/components/FinishQuiz.tsx: -------------------------------------------------------------------------------- 1 | import { IOption } from "../shared/interfaces"; 2 | import { EmptyResponse } from "./EmptyResponse"; 3 | import { Option } from "./Option"; 4 | interface Props { 5 | responses: any; 6 | score: number; 7 | as: "AFTER_QUIZ_RESPONSE" | "AUTHOR_CHECK_RESPONSE" | "USER_CHECK_RESPONSE"; 8 | quizDeleted?: boolean; 9 | } 10 | 11 | export const ShowResponses: React.FC = ({ 12 | responses, 13 | score, 14 | as, 15 | quizDeleted, 16 | }) => { 17 | const AFTER_QUIZ_RESPONSE = as === "AFTER_QUIZ_RESPONSE"; 18 | const AUTHOR_CHECK_RESPONSE = as === "AUTHOR_CHECK_RESPONSE"; 19 | // const USER_CHECK_RESPONSE = as === "USER_CHECK_RESPONSE"; 20 | 21 | return ( 22 | <> 23 |
24 | {AFTER_QUIZ_RESPONSE && ( 25 |

26 | Thank you for playing this Quiz 27 |

28 | )} 29 | {quizDeleted && ( 30 |

31 | This Quiz has been Deleted. 32 |

33 | )} 34 |

35 | Here is {AUTHOR_CHECK_RESPONSE ? "his" : "your"} score: {score} 36 |

37 | 38 |
39 |
40 | {responses.length > 0 && ( 41 | <> 42 |
43 |

Correct Answer

44 |
51 |
52 |

{AUTHOR_CHECK_RESPONSE ? "His" : "Your"} Response

53 |
60 |
61 |

62 | {AUTHOR_CHECK_RESPONSE ? "He" : "You"} chose correct option 63 |

64 | 65 |
72 | 73 | )} 74 |
75 | {responses.length > 0 ? ( 76 |
77 |

Answers

78 | {responses.map((resp: any, i: number) => ( 79 |
80 |

{resp.title}

81 |
82 | {resp.options.map((option: IOption, i: number) => ( 83 |
92 |
93 | ))} 94 |
95 | ) : ( 96 | <> 97 | 98 | 99 | )} 100 |
101 |
102 | 103 | ); 104 | }; 105 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Logo from "../assets/logos/White-Black-Circle.png"; 3 | 4 | export const Footer = () => ( 5 |
6 | landing 7 |

© Quizco 2022

8 |
9 | ); 10 | -------------------------------------------------------------------------------- /src/components/GridWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ColDef, 3 | ColumnApi, 4 | GridApi, 5 | GridReadyEvent, 6 | RowSelectedEvent, 7 | } from "ag-grid-community"; 8 | import { AgGridReact } from "ag-grid-react"; 9 | import { useEffect, useState } from "react"; 10 | 11 | interface Props { 12 | list: any[]; 13 | colDefs: ColDef[]; 14 | loading: boolean; 15 | setSelected?: React.Dispatch>; 16 | setGridApiParent?: React.Dispatch>; 17 | setGridColApiParent?: React.Dispatch< 18 | React.SetStateAction 19 | >; 20 | } 21 | 22 | export const GridWrapper: React.FC = ({ 23 | colDefs, 24 | list, 25 | loading, 26 | setGridApiParent, 27 | setGridColApiParent, 28 | setSelected, 29 | }) => { 30 | const [gridApi, setGridApi] = useState(); 31 | const onRowSelection = (event: RowSelectedEvent) => { 32 | const node = event.node; 33 | const rowData = event.data; 34 | const status = node.isSelected(); 35 | if (status) { 36 | setSelected && setSelected((p) => p.concat(rowData.id)); 37 | } else { 38 | setSelected && 39 | setSelected((p) => 40 | p.filter((id: string) => { 41 | return id !== rowData.id; 42 | }) 43 | ); 44 | } 45 | }; 46 | 47 | const onGridReady = (params: GridReadyEvent) => { 48 | setGridApi(params.api); 49 | // setGridColumnApi(params.columnApi); 50 | setGridApiParent && setGridApiParent(params.api); 51 | setGridColApiParent && setGridColApiParent(params.columnApi); 52 | 53 | const allColumnIds: any = []; 54 | params.columnApi.getAllColumns()?.forEach((column: any) => { 55 | allColumnIds.push(column.colId); 56 | }); 57 | params.columnApi.autoSizeColumns(allColumnIds, false); 58 | }; 59 | 60 | useEffect(() => { 61 | if (loading) { 62 | gridApi?.showLoadingOverlay(); 63 | } else if (!list.length) { 64 | gridApi?.showNoRowsOverlay(); 65 | } else { 66 | gridApi?.hideOverlay(); 67 | } 68 | }, [loading, gridApi, list.length]); 69 | return ( 70 |
74 | 86 |
87 | ); 88 | }; 89 | -------------------------------------------------------------------------------- /src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { useMatch } from "react-router-dom"; 2 | import { NavBar } from "./NavBar"; 3 | 4 | interface Props {} 5 | 6 | export const Layout: React.FC = ({ children }) => { 7 | const isPlayerPage = useMatch("/quizes/:id"); 8 | const isQuestionsPage = useMatch("/quizes/:id/questions"); 9 | const isLandingPage = useMatch("/"); 10 | return ( 11 |
12 | 13 |
20 | {children} 21 |
22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { Backdrop, Box, Fade, Modal } from "@material-ui/core"; 2 | import { modalStyle } from "../shared/constants"; 3 | 4 | interface Props { 5 | open: boolean; 6 | onClose: () => void; 7 | } 8 | 9 | export const ModalSkeleton: React.FC = ({ open, onClose, children }) => { 10 | return ( 11 | 22 | 23 | {children} 24 | 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import { SignedIn, SignedOut, UserButton } from "@clerk/clerk-react"; 2 | import { Link } from "react-router-dom"; 3 | import Logo from "../assets/logos/White-Purple-Circle.png"; 4 | 5 | interface Props {} 6 | 7 | export const NavBar: React.FC = () => { 8 | return ( 9 |
10 | 14 | Quizco Logo 15 |

Quizco

16 | 17 |
18 | 19 | 20 |

21 | Sign Up 22 |

23 | 24 | 25 |

26 | Sign In 27 |

28 | 29 |
30 | 31 | 32 | 33 |

34 | Quizes 35 |

36 | 37 | 38 |

39 | Dashboard 40 |

41 | 42 | 43 |
44 |
45 |
46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /src/components/Option.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | option: { value: string }; 3 | selectedOption?: string; 4 | onClick?: () => void; 5 | disabled?: boolean; 6 | correctAns?: string; 7 | } 8 | 9 | export const Option: React.FC = ({ 10 | option, 11 | selectedOption, 12 | onClick, 13 | disabled, 14 | correctAns, 15 | }) => { 16 | const userSelectedCorrectOption = 17 | selectedOption === option.value && option.value === correctAns; 18 | 19 | const getClasses = () => { 20 | return `grid grid-player-options items-center px-4 py-2 border w-full text-left mt-4 rounded-md disabled:opacity-80 transition-all duration-300${ 21 | !userSelectedCorrectOption && selectedOption === option.value 22 | ? " border-indigo-600" 23 | : " border-gray-300" 24 | }${correctAns === option.value ? " bg-indigo-600" : ""}${ 25 | userSelectedCorrectOption ? " bg-blue-600" : "" 26 | }`; 27 | }; 28 | 29 | const classes = getClasses(); 30 | 31 | return ( 32 | 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /src/components/PaginationButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@material-ui/core"; 2 | import { MdNavigateBefore, MdNavigateNext } from "react-icons/md"; 3 | import { globalColors } from "../shared/constants"; 4 | 5 | interface IPaginationButton { 6 | onClick: () => void; 7 | disabled: boolean; 8 | title: "Previous Question" | "Next Question"; 9 | } 10 | 11 | export const PaginationButton: React.FC = ({ 12 | onClick, 13 | disabled, 14 | title, 15 | }) => ( 16 | 40 | ); 41 | -------------------------------------------------------------------------------- /src/components/Player.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { IOption, IQuestion, IResponse } from "../shared/interfaces"; 3 | import { Option } from "./Option"; 4 | import { PaginationButton } from "./PaginationButton"; 5 | 6 | interface Props { 7 | questions: IQuestion[]; 8 | activeIndex: number; 9 | setActiveIndex: React.Dispatch>; 10 | response: IResponse[]; 11 | setResponse: React.Dispatch>; 12 | } 13 | 14 | export const Player: React.FC = ({ 15 | questions, 16 | activeIndex, 17 | setActiveIndex, 18 | response, 19 | setResponse, 20 | }) => { 21 | const [selectedOption, setSelectedOption] = useState(""); 22 | 23 | const onOptionClick = (option: string) => { 24 | setSelectedOption(option); 25 | setResponse((res) => { 26 | const newRes: IResponse[] = []; 27 | res.forEach((resp) => { 28 | newRes.push({ 29 | ...resp, 30 | response: 31 | resp._id === questions[activeIndex]._id ? option : resp.response, 32 | }); 33 | }); 34 | return newRes; 35 | }); 36 | }; 37 | 38 | useEffect(() => { 39 | const currentQuestionResponse = response && response[activeIndex]?.response; 40 | if (currentQuestionResponse) { 41 | setSelectedOption(currentQuestionResponse); 42 | } 43 | return () => { 44 | setSelectedOption(""); 45 | }; 46 | }, [activeIndex, response]); 47 | 48 | return ( 49 |
50 |

{questions && questions[activeIndex].title}

51 |
52 | {questions && 53 | questions[activeIndex].options.map((option: IOption, i: number) => ( 54 |
62 |
63 | { 65 | setActiveIndex((p: number) => p - 1); 66 | }} 67 | disabled={activeIndex === 0} 68 | title="Previous Question" 69 | /> 70 | 71 | { 73 | setActiveIndex((p) => p + 1); 74 | }} 75 | title="Next Question" 76 | disabled={activeIndex === questions?.length - 1} 77 | /> 78 |
79 |
80 | ); 81 | }; 82 | -------------------------------------------------------------------------------- /src/components/QuizCard.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useMatch, useNavigate } from "react-router-dom"; 3 | import { IQuiz } from "../shared/interfaces"; 4 | import { ModalSkeleton } from "./Modal"; 5 | import { QuizModalContents } from "./QuizModalContents"; 6 | 7 | interface Props extends IQuiz { 8 | onSelect?: () => void; 9 | score?: number; 10 | deleted?: boolean; 11 | redirect?: string; 12 | selected?: boolean; 13 | } 14 | 15 | export const QuizCard: React.FC = (props) => { 16 | const { 17 | title, 18 | description, 19 | tags, 20 | onSelect, 21 | status, 22 | score, 23 | redirect, 24 | selected, 25 | } = props; 26 | const isDashboardPage = useMatch("/dashboard"); 27 | const navigate = useNavigate(); 28 | 29 | const [quizModalActive, setQuizModalActive] = useState(false); 30 | const handleQuizModalActive = () => setQuizModalActive(true); 31 | const handleQuizModalClose = () => { 32 | setQuizModalActive((p) => !p); 33 | }; 34 | 35 | return ( 36 | <> 37 |
40 | onSelect 41 | ? onSelect() 42 | : redirect 43 | ? navigate(redirect) 44 | : handleQuizModalActive() 45 | // : navigate(`/quizes/${_id}`) 46 | } 47 | className={`relative shadow-md px-10 py-8 rounded-md bg-white cursor-pointer ${ 48 | selected ? " border-2 border-teal-500" : "" 49 | }`} 50 | style={{ boxShadow: "15px 15px 54px -10px #0000001f" }} 51 | > 52 | {isDashboardPage && ( 53 |

64 | {status} 65 |

66 | )} 67 |

68 | {title} 69 |

70 |

71 | {description.length > 200 72 | ? description.slice(0, 200) + "..." 73 | : description} 74 |

75 | {(score === 0 || score) && ( 76 |

77 | Score : {score} 78 |

79 | )} 80 |
81 | {tags.map((tag, i) => ( 82 |

92 | {tag} 93 |

94 | ))} 95 |
96 |
97 | 98 | 99 | 100 | 101 | ); 102 | }; 103 | -------------------------------------------------------------------------------- /src/components/QuizModalContents.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@material-ui/core"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { IQuiz } from "../shared/interfaces"; 4 | 5 | interface Props extends IQuiz { 6 | onSelect?: () => void; 7 | score?: number; 8 | deleted?: boolean; 9 | redirect?: string; 10 | selected?: boolean; 11 | onClose: () => void; 12 | } 13 | 14 | export const QuizModalContents: React.FC = ({ 15 | onClose, 16 | title, 17 | description, 18 | tags, 19 | attemptsCount, 20 | questionsCount, 21 | _id, 22 | }) => { 23 | const navigate = useNavigate(); 24 | return ( 25 |
26 |
27 |
28 |

{title}

29 |

{description}

30 |
31 |

32 | Number of times People played this Quiz:{" "} 33 |

34 | 35 | {attemptsCount} 36 | 37 |
38 |
39 |

Number of Questions:

40 | 41 | {questionsCount} 42 | 43 |
44 | 45 |
46 | {tags.map((tag, i) => ( 47 |

57 | {tag} 58 |

59 | ))} 60 |
61 |
62 |
63 |
64 | 67 |
68 | 75 |
76 |
77 |
78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /src/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { useMediaQuery } from "@material-ui/core"; 2 | import { Dispatch, SetStateAction, useEffect, useState } from "react"; 3 | import { BsLayoutSidebarInset } from "react-icons/bs"; 4 | import { IQuestion, IResponse } from "../shared/interfaces"; 5 | 6 | interface Props { 7 | questions: IQuestion[]; 8 | activeIndex?: number; 9 | setActiveIndex: Dispatch>; 10 | responses: [] | IResponse[]; 11 | } 12 | 13 | export const Sidebar: React.FC = ({ 14 | activeIndex, 15 | setActiveIndex, 16 | questions, 17 | responses, 18 | }) => { 19 | const [expanded, setExpanded] = useState(false); 20 | const [showQuestions, setShowQuestions] = useState(false); 21 | 22 | const isMobile = useMediaQuery("(max-width:600px)"); 23 | 24 | useEffect(() => { 25 | if (expanded) { 26 | const timeout = setTimeout(() => { 27 | setShowQuestions(true); 28 | }, 300); 29 | return () => { 30 | clearTimeout(timeout); 31 | setShowQuestions(false); 32 | }; 33 | } 34 | }, [expanded]); 35 | 36 | return ( 37 |
41 |
setExpanded((p) => !p)} 43 | className="w-8 h-8 p-2 flex justify-center items-center cursor-pointer hover:bg-gray-200 rounded-full" 44 | > 45 | 46 |
47 |
48 | {questions?.map((quiz, index) => ( 49 |
{ 52 | setActiveIndex(index); 53 | if (isMobile) { 54 | setExpanded(false); 55 | } 56 | }} 57 | className={`items-center transition-all duration-300 cursor-pointer flex mb-4 ${ 58 | activeIndex === index ? "" : "" 59 | }`} 60 | > 61 |

68 | {index + 1} 69 |

70 | 71 | {expanded && showQuestions && ( 72 |

73 | {quiz.title.length > 60 74 | ? quiz.title.slice(0, 60) + "..." 75 | : quiz.title} 76 |

77 | )} 78 |
79 | ))} 80 |
81 |
82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /src/components/SidebarQuestion.tsx: -------------------------------------------------------------------------------- 1 | import { useSnackbar } from "notistack"; 2 | import { useState } from "react"; 3 | import { AiFillDelete, AiFillEdit } from "react-icons/ai"; 4 | import { useQueryClient } from "react-query"; 5 | import { useNavigate, useParams } from "react-router-dom"; 6 | import { 7 | errorMessages, 8 | loadingMessages, 9 | successMessages, 10 | } from "../shared/constants"; 11 | import { IQuestion } from "../shared/interfaces"; 12 | import { useDeleteQuestion } from "../shared/queries"; 13 | import { DeleteModal } from "./DeleteModal"; 14 | 15 | interface SidebarProps { 16 | index: number; 17 | expanded: boolean; 18 | showQuestions: boolean; 19 | question: IQuestion; 20 | expandQuestion: string; 21 | setExpandQuestion: React.Dispatch>; 22 | setExpanded: React.Dispatch>; 23 | } 24 | 25 | export const SidebarQuestion: React.FC = ({ 26 | index, 27 | expanded, 28 | showQuestions, 29 | question, 30 | setExpandQuestion, 31 | expandQuestion, 32 | setExpanded, 33 | }) => { 34 | const { id } = useParams() as { id: string }; 35 | const { mutate, reset, isLoading } = useDeleteQuestion(id, question._id); 36 | const queryClient = useQueryClient(); 37 | const navigate = useNavigate(); 38 | const { enqueueSnackbar } = useSnackbar(); 39 | 40 | const [deleteModalActive, setDeleteModalActive] = useState(false); 41 | const handleDeleteModalOpen = () => setDeleteModalActive(true); 42 | const handleDeleteModalClose = () => setDeleteModalActive(false); 43 | 44 | const onDeleteQuestion = async () => { 45 | enqueueSnackbar(loadingMessages.actionLoading("Deleting", "Question"), { 46 | variant: "info", 47 | }); 48 | mutate( 49 | {}, 50 | { 51 | onError: () => { 52 | enqueueSnackbar(errorMessages.default, { variant: "error" }); 53 | }, 54 | onSettled: () => { 55 | reset(); 56 | handleDeleteModalClose(); 57 | }, 58 | onSuccess: () => { 59 | queryClient.invalidateQueries(["Quiz Questions", id]); 60 | enqueueSnackbar( 61 | successMessages.actionSuccess("Deleted", "Question"), 62 | { variant: "success" } 63 | ); 64 | }, 65 | } 66 | ); 67 | }; 68 | 69 | return ( 70 |
75 |
{ 77 | setExpanded(true); 78 | setExpandQuestion(question._id); 79 | }} 80 | className={`transition-all duration-300 cursor-pointer flex`} 81 | > 82 |

85 | {index + 1} 86 |

87 | 88 | {expanded && showQuestions && ( 89 |

90 | {question.title.length > 60 91 | ? question.title.slice(0, 60) + "..." 92 | : question.title} 93 |

94 | )} 95 |
96 | {expanded && ( 97 |
101 |
102 |
106 | 107 |
108 | 116 |
118 | navigate(`/quizes/${id}/questions/${question._id}`) 119 | } 120 | className="p-2 bg-indigo-600 rounded-full mr-4 cursor-pointer" 121 | > 122 | 123 |
124 |
125 |
126 | )} 127 |
128 | ); 129 | }; 130 | -------------------------------------------------------------------------------- /src/components/Svgs.tsx: -------------------------------------------------------------------------------- 1 | export const Loader: React.FC<{ 2 | height?: number; 3 | width?: number; 4 | color?: string; 5 | fullScreen?: boolean; 6 | halfScreen?: boolean; 7 | }> = ({ height, width, color, fullScreen, halfScreen }) => { 8 | return ( 9 |
19 | 28 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 58 | 59 | 60 |
61 | ); 62 | }; 63 | 64 | interface DefaultSVgProps { 65 | height: number; 66 | width: number; 67 | color: string; 68 | } 69 | 70 | export const LandingSvg: React.FC = ({ 71 | height, 72 | width, 73 | color, 74 | }) => { 75 | return ( 76 | 83 | 87 | 91 | 95 | 99 | 103 | 107 | 111 | 112 | ); 113 | }; 114 | -------------------------------------------------------------------------------- /src/components/forms/AddEditQuestionFormFields.tsx: -------------------------------------------------------------------------------- 1 | import { Button, TextField } from "@material-ui/core"; 2 | import { FieldArray, useFormikContext } from "formik"; 3 | import { useNavigate } from "react-router-dom"; 4 | import { uiMessages } from "../../shared/constants"; 5 | import { IQuestionForm } from "../../shared/interfaces"; 6 | import { FormikError } from "../../shared/utils"; 7 | 8 | interface Props { 9 | isLoading: boolean; 10 | } 11 | 12 | export const AddEditQuestionFormFields: React.FC = ({ isLoading }) => { 13 | const { 14 | touched, 15 | errors, 16 | values, 17 | handleBlur, 18 | handleChange, 19 | handleSubmit, 20 | setFieldValue, 21 | } = useFormikContext(); 22 | const navigate = useNavigate(); 23 | 24 | return ( 25 |
26 |
27 | 38 |
39 |
40 | 41 | {({ remove, push }) => { 42 | return ( 43 | <> 44 |
51 |

Options

52 |

Correct

53 |
54 | {values.options.length > 0 && 55 | values.options.map((option, index) => ( 56 |
64 | 85 | 86 |
87 |
89 | setFieldValue( 90 | "correct", 91 | values.options[index].value 92 | ) 93 | } 94 | className={`cursor-pointer flex items-center justify-center border-2 w-6 h-6 rounded-full ${ 95 | !!values.options.find((val) => val.value === "") 96 | ? "" 97 | : "border-indigo-600" 98 | }`} 99 | > 100 | {!values.options.find((val) => val.value === "") && 101 | values.correct === option.value && ( 102 |
103 |   104 |
105 | )} 106 |
107 |
108 |
109 | ))} 110 | 111 | ); 112 | }} 113 |
114 |

115 | {touched.correct && errors.correct} 116 |

117 | 118 | {uiMessages.allowedMarkingACorrectOption.map((message) => ( 119 |

{message}

120 | ))} 121 |
122 | {uiMessages.warnQuestionCreate.map((message) => ( 123 |

{message}

124 | ))} 125 |
126 |
127 |
128 |
129 |
130 | 131 |
132 | 133 | 141 |
142 |
143 |
144 | ); 145 | }; 146 | -------------------------------------------------------------------------------- /src/components/forms/AddQuestionForm.tsx: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { Formik } from "formik"; 3 | import { useSnackbar } from "notistack"; 4 | import { useQueryClient } from "react-query"; 5 | import { useParams } from "react-router-dom"; 6 | import { 7 | errorMessages, 8 | loadingMessages, 9 | successMessages, 10 | } from "../../shared/constants"; 11 | import { IQuestionForm } from "../../shared/interfaces"; 12 | import { useCreateQuestion } from "../../shared/queries"; 13 | import { AddEditQuestionValidation } from "../../shared/validationSchema"; 14 | import { AddEditQuestionFormFields } from "./AddEditQuestionFormFields"; 15 | 16 | interface Props {} 17 | 18 | export const AddQuestionForm: React.FC = () => { 19 | const { id: quizId } = useParams() as { id: string }; 20 | const { 21 | mutate: createQuestionMutate, 22 | reset: createQuestionReset, 23 | isLoading, 24 | } = useCreateQuestion(quizId); 25 | 26 | const queryClient = useQueryClient(); 27 | 28 | const { enqueueSnackbar } = useSnackbar(); 29 | 30 | return ( 31 | 32 | initialValues={{ 33 | title: "", 34 | correct: "", 35 | options: [{ value: "" }, { value: "" }, { value: "" }, { value: "" }], 36 | }} 37 | validationSchema={AddEditQuestionValidation} 38 | onSubmit={async (values, { setSubmitting, setFieldError, resetForm }) => { 39 | try { 40 | setSubmitting(true); 41 | if (!!!values.title.trim()) { 42 | setFieldError("title", "Only Spaces not allowed."); 43 | throw Error("Form Error"); 44 | } 45 | 46 | values.options.forEach((option, index) => { 47 | if (!!!option.value.trim()) { 48 | setFieldError( 49 | `options.${index}.value`, 50 | "Only Spaces not allowed." 51 | ); 52 | throw Error("Form Error"); 53 | } 54 | }); 55 | 56 | const maping: { [key: string]: number[] } = {}; 57 | 58 | values.options.forEach((option1, i1) => { 59 | let flag = 0; 60 | const option1Indices: number[] = []; 61 | values.options.forEach((option2, i2) => { 62 | if (option1.value === option2.value) { 63 | flag++; 64 | option1Indices.push(i2); 65 | } 66 | }); 67 | if (flag > 1) { 68 | maping[option1.value] = option1Indices; 69 | } 70 | }); 71 | 72 | const errors = Object.entries(maping); 73 | 74 | errors.forEach((element) => { 75 | element[1].forEach((index) => 76 | setFieldError( 77 | `options.${index}.value`, 78 | "Duplicate option are not allowed." 79 | ) 80 | ); 81 | }); 82 | 83 | if (errors.length) throw Error("DUPLICATE_OPTION"); 84 | enqueueSnackbar( 85 | loadingMessages.actionLoading("Creating", "Question"), 86 | { 87 | variant: "info", 88 | } 89 | ); 90 | 91 | createQuestionMutate( 92 | { body: values }, 93 | { 94 | onSuccess: () => { 95 | queryClient.invalidateQueries(["Quiz Questions", quizId]); 96 | enqueueSnackbar( 97 | successMessages.actionSuccess("Created", "Question"), 98 | { variant: "success" } 99 | ); 100 | resetForm(); 101 | }, 102 | onError: (err) => { 103 | if (axios.isAxiosError(err)) { 104 | enqueueSnackbar(err.response?.data.message, { 105 | variant: "error", 106 | }); 107 | } else { 108 | enqueueSnackbar(errorMessages.default, { variant: "error" }); 109 | } 110 | }, 111 | onSettled: () => { 112 | createQuestionReset(); 113 | }, 114 | } 115 | ); 116 | } catch (e) { 117 | } finally { 118 | setSubmitting(false); 119 | } 120 | }} 121 | > 122 | 123 | 124 | ); 125 | }; 126 | -------------------------------------------------------------------------------- /src/components/forms/FiltersForm.tsx: -------------------------------------------------------------------------------- 1 | import { Button, TextField } from "@material-ui/core"; 2 | import { Formik } from "formik"; 3 | import { FiltersValidation } from "../../shared/validationSchema"; 4 | 5 | interface Props { 6 | searchTerm: string; 7 | tag: string; 8 | setSearchTerm: React.Dispatch>; 9 | setTag: React.Dispatch>; 10 | modalClose: () => void; 11 | } 12 | 13 | export const FiltersForm: React.FC = ({ 14 | searchTerm, 15 | tag, 16 | modalClose, 17 | setSearchTerm, 18 | setTag, 19 | }) => { 20 | return ( 21 | <> 22 |
23 |

24 | Available Filters 25 |

26 |
27 | 31 | validateOnChange={true} 32 | initialValues={{ 33 | search: searchTerm, 34 | tag: tag, 35 | }} 36 | validationSchema={FiltersValidation} 37 | onSubmit={async (values, { setSubmitting, setFieldError }) => { 38 | setSubmitting(true); 39 | try { 40 | if (!!!values.search.trim() && values.search.length !== 0) { 41 | setFieldError("search", "Only Spaces not allowed."); 42 | throw Error("Form Error"); 43 | } 44 | 45 | setSearchTerm(values.search); 46 | setTag(values.tag); 47 | modalClose(); 48 | } catch (e) {} 49 | 50 | setSubmitting(false); 51 | }} 52 | > 53 | {({ 54 | handleSubmit, 55 | isSubmitting, 56 | handleBlur, 57 | handleChange, 58 | values, 59 | touched, 60 | errors, 61 | }) => ( 62 |
63 |
64 |
65 | 77 |
78 | 79 | 91 | 92 |
93 |
94 | 95 |
96 | 97 | 105 |
106 |
107 |
108 | )} 109 | 110 | 111 | ); 112 | }; 113 | -------------------------------------------------------------------------------- /src/components/forms/QuizForm.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@material-ui/core"; 2 | import { AxiosError } from "axios"; 3 | import { Formik } from "formik"; 4 | import { useSnackbar } from "notistack"; 5 | import { UseMutateAsyncFunction, useQueryClient } from "react-query"; 6 | import { useNavigate } from "react-router-dom"; 7 | import { errorMessages, successMessages } from "../../shared/constants"; 8 | import { IQuizForm } from "../../shared/interfaces"; 9 | import { AddEditQuizValidation } from "../../shared/validationSchema"; 10 | import { AddEditQuizFormFields } from "../AddEditQuizFormFields"; 11 | 12 | interface Props { 13 | mutateAsync: UseMutateAsyncFunction, any, unknown>; 14 | reset: () => void; 15 | title?: string; 16 | description?: string; 17 | tags?: string[]; 18 | redirect: string; 19 | id?: string; 20 | status?: string; 21 | } 22 | 23 | export const QuizForm: React.FC = ({ 24 | mutateAsync, 25 | reset, 26 | description, 27 | tags, 28 | title, 29 | redirect, 30 | id, 31 | status, 32 | }) => { 33 | const { enqueueSnackbar } = useSnackbar(); 34 | const navigate = useNavigate(); 35 | 36 | const queryClient = useQueryClient(); 37 | 38 | return ( 39 | 40 | initialValues={{ 41 | title: title || "", 42 | description: description || "", 43 | tags: tags || [], 44 | status: status || "", 45 | }} 46 | validationSchema={AddEditQuizValidation} 47 | onSubmit={async (values, { setSubmitting, setFieldError }) => { 48 | setSubmitting(true); 49 | const body = { ...values }; 50 | if (!id) delete body.status; 51 | try { 52 | if (!!!values.title.trim()) { 53 | setFieldError("title", "Only Spaces not allowed."); 54 | throw Error("Form Error"); 55 | } 56 | if (!!!values.description.trim()) { 57 | setFieldError("description", "Only Spaces not allowed."); 58 | throw Error("Form Error"); 59 | } 60 | await mutateAsync( 61 | { body }, 62 | { 63 | onSuccess: () => { 64 | queryClient.invalidateQueries("Quizes"); 65 | enqueueSnackbar( 66 | successMessages.actionSuccess( 67 | id ? "Updated" : "Created", 68 | "Quiz" 69 | ) 70 | ); 71 | id && queryClient.invalidateQueries(["Quiz", id]); 72 | navigate(redirect); 73 | }, 74 | onError: () => { 75 | enqueueSnackbar(errorMessages.default); 76 | }, 77 | onSettled: () => { 78 | reset(); 79 | setSubmitting(false); 80 | }, 81 | } 82 | ); 83 | } catch (e) {} 84 | }} 85 | > 86 | {({ handleSubmit, isSubmitting }) => ( 87 |
88 |
89 | 90 |
91 |
92 | 93 |
94 | 95 | 103 |
104 |
105 |
106 | )} 107 | 108 | ); 109 | }; 110 | -------------------------------------------------------------------------------- /src/components/forms/UpdateQuestionForm.tsx: -------------------------------------------------------------------------------- 1 | import { Formik } from "formik"; 2 | import { useSnackbar } from "notistack"; 3 | import { useQueryClient } from "react-query"; 4 | import { useNavigate, useParams } from "react-router-dom"; 5 | import { 6 | errorMessages, 7 | loadingMessages, 8 | successMessages, 9 | } from "../../shared/constants"; 10 | import { IOption, IQuestionForm } from "../../shared/interfaces"; 11 | import { useUpdateQuestion } from "../../shared/queries"; 12 | import { AddEditQuestionValidation } from "../../shared/validationSchema"; 13 | import { AddEditQuestionFormFields } from "./AddEditQuestionFormFields"; 14 | 15 | interface Props { 16 | id: string; 17 | title: string; 18 | correct: string; 19 | options: IOption[]; 20 | } 21 | 22 | export const UpdateQuestionForm: React.FC = ({ 23 | id, 24 | title, 25 | correct, 26 | options, 27 | }) => { 28 | const { quizId } = useParams() as { 29 | quizId: string; 30 | questionId: string; 31 | }; 32 | const { 33 | mutate: updateQuestionMutate, 34 | reset: updateQuestionReset, 35 | isLoading, 36 | } = useUpdateQuestion(quizId, id); 37 | 38 | const queryClient = useQueryClient(); 39 | const navigate = useNavigate(); 40 | 41 | const { enqueueSnackbar } = useSnackbar(); 42 | 43 | return ( 44 | 45 | initialValues={{ 46 | title: title || "", 47 | correct: correct || "", 48 | options: options || [ 49 | { value: "" }, 50 | { value: "" }, 51 | { value: "" }, 52 | { value: "" }, 53 | ], 54 | }} 55 | validationSchema={AddEditQuestionValidation} 56 | onSubmit={async (values, { setSubmitting }) => { 57 | setSubmitting(true); 58 | enqueueSnackbar(loadingMessages.actionLoading("Updating", "Question"), { 59 | variant: "info", 60 | }); 61 | 62 | updateQuestionMutate( 63 | { body: values }, 64 | { 65 | onSuccess: () => { 66 | enqueueSnackbar( 67 | successMessages.actionSuccess("Updated", "Question"), 68 | { variant: "success" } 69 | ); 70 | navigate(`/quizes/${quizId}/questions`); 71 | queryClient.invalidateQueries(["Quiz Questions", quizId]); 72 | queryClient.invalidateQueries(["Quiz Question", quizId, id]); 73 | }, 74 | onError: () => { 75 | enqueueSnackbar(errorMessages.default, { variant: "error" }); 76 | }, 77 | onSettled: () => { 78 | updateQuestionReset(); 79 | setSubmitting(false); 80 | }, 81 | } 82 | ); 83 | }} 84 | > 85 | 86 | 87 | ); 88 | }; 89 | -------------------------------------------------------------------------------- /src/hooks/useKeyPress.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export const useKeyPress = (targetKey: string) => { 4 | const [keyPressed, setKeyPressed] = useState(false); 5 | 6 | const downHandler = ({ key }: { key: string }) => { 7 | if (key === targetKey) setKeyPressed(true); 8 | }; 9 | 10 | const upHandler = ({ key }: { key: string }) => { 11 | if (key === targetKey) setKeyPressed(false); 12 | }; 13 | 14 | useEffect(() => { 15 | window.addEventListener("keydown", downHandler); 16 | window.addEventListener("keyup", upHandler); 17 | 18 | return () => { 19 | window.removeEventListener("keydown", downHandler); 20 | window.removeEventListener("keyup", upHandler); 21 | }; 22 | }, []); 23 | 24 | return keyPressed; 25 | }; 26 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @font-face { 6 | font-family: CircularStd-Black; 7 | src: url("./assets/fonts/CircularStd-Black.otf"); 8 | } 9 | @font-face { 10 | font-family: CircularStd-Bold; 11 | src: url("./assets/fonts/CircularStd-Bold.otf"); 12 | } 13 | @font-face { 14 | font-family: CircularStd-Book; 15 | src: url("./assets/fonts/CircularStd-Book.otf"); 16 | } 17 | @font-face { 18 | font-family: CircularStd-Light; 19 | src: url("./assets/fonts/CircularStd-Light.otf"); 20 | } 21 | @font-face { 22 | font-family: CircularStd-Medium; 23 | src: url("./assets/fonts/CircularStd-Medium.otf"); 24 | } 25 | 26 | .ag-theme-alpine > * { 27 | font-family: "CircularStd-Book", sans-serif; 28 | } 29 | 30 | .ag-header-cell-text { 31 | font-weight: 600 !important; 32 | font-size: 14px !important; 33 | } 34 | 35 | .ag-theme-alpine .ag-root-wrapper { 36 | border: solid 1px !important; 37 | border-color: #eaeaea !important; 38 | border-radius: 6px !important; 39 | } 40 | 41 | .ag-theme-alpine .ag-header { 42 | border-bottom: none !important; 43 | } 44 | 45 | body { 46 | margin: 0; 47 | font-family: CircularStd-Book, -apple-system, BlinkMacSystemFont, Segoe UI, 48 | Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, 49 | sans-serif; 50 | -webkit-font-smoothing: antialiased; 51 | -moz-osx-font-smoothing: grayscale; 52 | /* background-color: #f8f9fa; */ 53 | } 54 | 55 | code { 56 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 57 | monospace; 58 | } 59 | 60 | .grid-player-options { 61 | grid-template-columns: 30px 1fr; 62 | } 63 | 64 | .grid-use-app { 65 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 66 | } 67 | 68 | .grid-quizes { 69 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 70 | } 71 | 72 | .grid-quiz-modal-descriptions { 73 | grid-template-columns: max-content 40px; 74 | } 75 | @media screen and (max-width: 999px) { 76 | .grid-quiz-modal-descriptions { 77 | grid-template-columns: 1fr 40px; 78 | } 79 | } 80 | 81 | .grid-responses-options-show { 82 | display: grid; 83 | grid-template-columns: 1fr 1fr 1fr; 84 | gap: 10px; 85 | } 86 | @media screen and (max-width: 600px) { 87 | .grid-responses-options-show { 88 | display: grid; 89 | grid-template-columns: 1fr; 90 | grid-template-rows: 100px 100px 100px; 91 | gap: 10px; 92 | } 93 | } 94 | 95 | @media screen and (max-width: 600px) { 96 | .grid-quizes { 97 | grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); 98 | } 99 | } 100 | @media screen and (max-width: 400px) { 101 | .grid-quizes { 102 | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 103 | } 104 | } 105 | 106 | /* .landing { 107 | background-image: url("./assets/svgs/landing.svg"); 108 | background-repeat: no-repeat; 109 | background-position: bottom; 110 | background-size: 80%; 111 | } */ 112 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { MuiThemeProvider } from "@material-ui/core"; 2 | import "ag-grid-community/dist/styles/ag-grid.css"; 3 | import "ag-grid-community/dist/styles/ag-theme-alpine.css"; 4 | import { SnackbarProvider } from "notistack"; 5 | import React from "react"; 6 | import ReactDOM from "react-dom"; 7 | import { QueryClient, QueryClientProvider } from "react-query"; 8 | import { ReactQueryDevtools } from "react-query/devtools"; 9 | import { BrowserRouter } from "react-router-dom"; 10 | import App from "./App"; 11 | import "./index.css"; 12 | import { theme } from "./shared/theme"; 13 | 14 | const queryClient = new QueryClient(); 15 | 16 | ReactDOM.render( 17 | 18 | 19 | 20 | 21 | {/* */} 22 | 23 | 24 | 25 | {/* */} 26 | 27 | 28 | 29 | 30 | , 31 | document.getElementById("root") 32 | ); 33 | -------------------------------------------------------------------------------- /src/pages/AddQuestions.tsx: -------------------------------------------------------------------------------- 1 | import { useUser } from "@clerk/clerk-react"; 2 | import { Button } from "@material-ui/core"; 3 | import { useQueryClient } from "react-query"; 4 | import { useParams } from "react-router-dom"; 5 | import { AddQuestionsSidebar } from "../components/AddQuestionsSidebar"; 6 | import { ErrorMessage } from "../components/ErrorMessage"; 7 | import { Loader } from "../components/Svgs"; 8 | import { AddQuestionForm } from "../components/forms/AddQuestionForm"; 9 | import { useCreateAIQuestion, useQuizQuestions } from "../shared/queries"; 10 | 11 | interface Props {} 12 | export const AddQuestions: React.FC = () => { 13 | const { id } = useParams() as { id: string }; 14 | const { isLoading, data } = useQuizQuestions(id); 15 | const { id: userId, primaryEmailAddress } = useUser(); 16 | const { mutate, isLoading: isAILoading } = useCreateAIQuestion(id); 17 | const queryClient = useQueryClient(); 18 | 19 | if (!isLoading && data?.author !== userId) { 20 | return ; 21 | } 22 | 23 | const onGenerateUsingAI = () => { 24 | mutate( 25 | { body: { questionCount: 1 } }, 26 | { 27 | onSuccess: (data) => { 28 | queryClient.setQueryData( 29 | ["Quiz Questions", id], 30 | (oldData: any) => { 31 | return { 32 | ...oldData, 33 | questions: oldData.questions.concat(data.questions), 34 | }; 35 | } 36 | ); 37 | }, 38 | } 39 | ); 40 | }; 41 | 42 | const isAllowed = 43 | primaryEmailAddress?.emailAddress === "vishwajeetraj11@gmail.com"; 44 | 45 | return isLoading ? ( 46 | 47 | ) : ( 48 |
52 |
53 | 54 |
55 |
56 |

Add Question

57 | 58 |
59 |

{data?.questions.length} / 10 Added

60 |
61 |
67 |
68 |
69 | {data?.questions.length < 10 && isAllowed ? ( 70 | 78 | ) : ( 79 | 80 | )} 81 |
82 |
83 | 84 |
85 |
86 |
87 |
88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /src/pages/Attempts.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@material-ui/core"; 2 | import { useEffect, useState } from "react"; 3 | import { EmptyResponse } from "../components/EmptyResponse"; 4 | import { ErrorMessage } from "../components/ErrorMessage"; 5 | import { QuizCard } from "../components/QuizCard"; 6 | import { Loader } from "../components/Svgs"; 7 | import { IAttempt } from "../shared/interfaces"; 8 | import { useMyAttempts } from "../shared/queries"; 9 | import { endpoints } from "../shared/urls"; 10 | 11 | interface Props {} 12 | 13 | export const Attempts: React.FC = () => { 14 | const [currentPage, setCurrentPage] = useState(1); 15 | const [totalPages, setTotalPages] = useState(1); 16 | 17 | const { data, isLoading, isFetching, isSuccess, error } = useMyAttempts( 18 | `${endpoints.attempts}?page=${currentPage}`, 19 | ["My Attempts", currentPage] 20 | ); 21 | 22 | useEffect(() => { 23 | if (isSuccess) { 24 | setTotalPages(data.count ? Math.ceil(data.count / 6) : 1); 25 | } 26 | }, [data?.count, isSuccess]); 27 | 28 | if (error?.response?.status) { 29 | return ( 30 | 34 | ); 35 | } 36 | return ( 37 |
38 |

39 | My Attempts 40 |

41 | 42 | {isLoading || isFetching ? ( 43 | 44 | ) : data?.attempts.length > 0 ? ( 45 | <> 46 |
47 | {data?.attempts.map((attempt: IAttempt) => ( 48 | 54 | ))} 55 |
56 |
57 | {totalPages > 1 && 58 | Array.from(Array(totalPages).keys()).map((loader, index) => ( 59 | 67 | ))} 68 |
69 | 70 | ) : ( 71 | <> 72 | 73 | 74 | )} 75 |
76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /src/pages/CreateQuiz.tsx: -------------------------------------------------------------------------------- 1 | import { QuizForm } from "../components/forms/QuizForm"; 2 | import { useCreateQuiz } from "../shared/queries"; 3 | 4 | interface Props {} 5 | 6 | export const CreateQuiz: React.FC = () => { 7 | const { mutateAsync, reset } = useCreateQuiz(); 8 | return ( 9 |
10 |

Create a Quiz

11 |
12 | 17 |
18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/pages/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@material-ui/core"; 2 | import { useSnackbar } from "notistack"; 3 | import { useEffect, useState } from "react"; 4 | import { useQueryClient } from "react-query"; 5 | import { useNavigate } from "react-router-dom"; 6 | import { DeleteModal } from "../components/DeleteModal"; 7 | import { EmptyResponse } from "../components/EmptyResponse"; 8 | import { ErrorMessage } from "../components/ErrorMessage"; 9 | import { QuizCard } from "../components/QuizCard"; 10 | import { Loader } from "../components/Svgs"; 11 | import { errorMessages, successMessages } from "../shared/constants"; 12 | import { IQuiz } from "../shared/interfaces"; 13 | import { useDeleteQuiz, useQuizes } from "../shared/queries"; 14 | import { endpoints } from "../shared/urls"; 15 | 16 | interface Props {} 17 | 18 | export const Dashboard: React.FC = () => { 19 | const [currentPage, setCurrentPage] = useState(1); 20 | const [totalPages, setTotalPages] = useState(1); 21 | 22 | const { data, isLoading, isSuccess, isFetching, error } = useQuizes( 23 | `${endpoints.quizes}?loggedIn=true&page=${currentPage}`, 24 | ["Quizes", "Current User", currentPage] 25 | ); 26 | 27 | useEffect(() => { 28 | if (isSuccess) { 29 | setTotalPages(data.count ? Math.ceil(data.count / 6) : 1); 30 | } 31 | }, [data?.count, isSuccess]); 32 | 33 | const [deleteModalActive, setDeleteModalActive] = useState(false); 34 | const handleDeleteModalOpen = () => setDeleteModalActive(true); 35 | const handleDeleteModalClose = () => setDeleteModalActive(false); 36 | 37 | const navigate = useNavigate(); 38 | 39 | const [selectedQuiz, setSelectedQuiz] = useState(); 40 | 41 | const onUpdate = () => { 42 | navigate(`/quizes/${selectedQuiz?._id}/update`); 43 | }; 44 | const { enqueueSnackbar } = useSnackbar(); 45 | const queryClient = useQueryClient(); 46 | 47 | const { 48 | isLoading: IsDeleteCampaignLoading, 49 | reset, 50 | mutateAsync, 51 | } = useDeleteQuiz(selectedQuiz?._id || "id"); 52 | 53 | if (error?.response?.status) { 54 | return ( 55 | 59 | ); 60 | } 61 | 62 | const onDelete = () => { 63 | mutateAsync( 64 | {}, 65 | { 66 | onSuccess: () => { 67 | enqueueSnackbar(successMessages.actionSuccess("Deleted", "Quiz"), { 68 | variant: "success", 69 | }); 70 | queryClient.invalidateQueries(["Quizes", "Current User"]); 71 | setSelectedQuiz(null); 72 | }, 73 | onError: () => { 74 | enqueueSnackbar(errorMessages.default, { variant: "error" }); 75 | }, 76 | onSettled: () => { 77 | reset(); 78 | handleDeleteModalClose(); 79 | }, 80 | } 81 | ); 82 | }; 83 | 84 | return ( 85 |
86 |

Dashboard

87 |
88 |

89 | My Quizes 90 |

91 |
92 | 99 | 100 |
101 | 108 |
109 |
110 |
111 | {data?.quizes.length > 0 && ( 112 |
113 |

117 | {`${ 118 | selectedQuiz 119 | ? `Selected Quiz : ${selectedQuiz.title}` 120 | : "Select a Quiz" 121 | }`} 122 |

123 |
124 | {selectedQuiz && ( 125 |
126 |
127 | 135 |
136 |
137 | 140 |
141 |
142 | 145 |
146 | 147 | 156 |
157 | )} 158 |
159 |
160 | )} 161 | {isLoading || isFetching ? ( 162 | 163 | ) : data?.quizes.length > 0 ? ( 164 |
165 | {data?.quizes.map((quiz: IQuiz) => ( 166 | setSelectedQuiz(quiz)} 168 | selected={selectedQuiz?._id === quiz._id} 169 | key={quiz._id} 170 | {...quiz} 171 | /> 172 | ))} 173 |
174 | ) : ( 175 | 176 | )} 177 |
178 | {totalPages > 1 && 179 | Array.from(Array(totalPages).keys()).map((loader, index) => ( 180 | 188 | ))} 189 |
190 | {deleteModalActive && ( 191 | 198 | )} 199 |
200 | ); 201 | }; 202 | -------------------------------------------------------------------------------- /src/pages/Landing.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from "react-router-dom"; 2 | import Active from "../assets/illustations/Active.png"; 3 | import AddQuestions from "../assets/illustations/Add Questions.png"; 4 | import Create from "../assets/illustations/Create.png"; 5 | import EcoFriendly from "../assets/illustations/ecofriendly.png"; 6 | import LandingIllustration from "../assets/illustations/landing.png"; 7 | import Stats from "../assets/illustations/Stats.png"; 8 | import Logo from "../assets/logos/White-Purple-Circle.png"; 9 | import { Footer } from "../components/Footer"; 10 | import { useStats } from "../shared/queries"; 11 | 12 | export const Landing = () => { 13 | const navigate = useNavigate(); 14 | const { data, isError } = useStats(); 15 | 16 | return ( 17 |
18 |
19 |
23 |
24 |
25 | landing 30 |
31 |
32 |
33 |
34 | landing 39 |

Quizco

40 |
41 |

42 | Quiz Builder and Assessment Tool 43 |

44 |

45 | Using Quizco, it’s super fast and easy to create a quiz - perfect 46 | for revision guides, driving theory practice and trivia. 47 |

48 |
49 | 55 |
56 |
57 |
58 |
59 | {!isError && ( 60 |
66 |
67 |

68 | {data?.users}+ 69 |

70 |

Users

71 |
72 |
73 |

74 | {data?.quizes}+ 75 |

76 | 77 |

78 | Quizes Created 79 |

80 |
81 |
82 |

83 | {data?.timesQuizesPlayed}+ 84 |

85 | 86 |

87 | Times Quizes Played 88 |

89 |
90 |
91 | )} 92 | 93 |
94 |

95 | Everything You Need to Build and Manage Your Quiz 96 |

97 |
98 | {AppUseData.map((useData) => ( 99 |
103 |
104 | Create a Quiz Illustration 109 |
110 |

111 | {useData.label} 112 |

113 |
114 | ))} 115 |
116 |
117 | 118 |
119 |
120 | 125 |
126 |

127 | Save resources and money by avoiding print-out quiz sheets and test 128 | papers. Users can complete your paperless quiz via Quizco. 129 |

130 |
131 | 132 |
133 |
134 | ); 135 | }; 136 | 137 | /* 138 | Make your own quiz and test yourself 139 | */ 140 | 141 | const AppUseData = [ 142 | { 143 | id: "1", 144 | imgsrc: Create, 145 | label: "Create a Quiz", 146 | }, 147 | { 148 | id: "2", 149 | imgsrc: AddQuestions, 150 | label: "Add Questions to your Quiz", 151 | }, 152 | { 153 | id: "3", 154 | imgsrc: Active, 155 | label: "Set status of Quiz to Active", 156 | }, 157 | { 158 | id: "4", 159 | imgsrc: Stats, 160 | label: "Checkout Statistics", 161 | }, 162 | ]; 163 | -------------------------------------------------------------------------------- /src/pages/PlayerScreen.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@material-ui/core"; 2 | import { useSnackbar } from "notistack"; 3 | import { useEffect, useState } from "react"; 4 | import { useParams } from "react-router-dom"; 5 | import { ConfirmSubmitModalContent } from "../components/ConfirmSubmitModal"; 6 | import { EmptyResponse } from "../components/EmptyResponse"; 7 | import { ErrorMessage } from "../components/ErrorMessage"; 8 | import { ShowResponses } from "../components/FinishQuiz"; 9 | import { ModalSkeleton } from "../components/Modal"; 10 | import { Player } from "../components/Player"; 11 | import { Sidebar } from "../components/Sidebar"; 12 | import { Loader } from "../components/Svgs"; 13 | import { errorMessages } from "../shared/constants"; 14 | import { IQuestion, IResponse } from "../shared/interfaces"; 15 | import { 16 | useQuizQuestionCorrectAns, 17 | useQuizQuestions, 18 | useSaveScore, 19 | } from "../shared/queries"; 20 | 21 | interface Props {} 22 | 23 | export const PlayerScreen: React.FC = () => { 24 | const params = useParams() as { id: string }; 25 | const { isLoading, isFetching, data, error } = useQuizQuestions(params.id, { 26 | staleTime: Infinity, 27 | }); 28 | 29 | const [fetchCorrectAns, setFetchCorrectAns] = useState(false); 30 | 31 | const { isLoading: isQuizCorrectAnsLoading } = useQuizQuestionCorrectAns( 32 | params.id, 33 | { 34 | enabled: fetchCorrectAns, 35 | onSuccess: (quizCorrectAnsData) => { 36 | findScore(quizCorrectAnsData); 37 | }, 38 | } 39 | ); 40 | 41 | const [activeIndex, setActiveIndex] = useState(0); 42 | const [response, setResponse] = useState([]); 43 | const [respWithCorrectAns, setRespWithCorrectAns] = useState([]); 44 | const [quizEnd, setQuizEnd] = useState(false); 45 | const [score, setScore] = useState(0); 46 | const { enqueueSnackbar } = useSnackbar(); 47 | const { mutateAsync, isLoading: isSaveScoreLoading } = useSaveScore( 48 | params.id 49 | ); 50 | 51 | const [confirmSubmitModalActive, setConfirmSubmitModalActive] = 52 | useState(false); 53 | const handleConfirmSubmitModalOpen = () => setConfirmSubmitModalActive(true); 54 | const handleConfirmSubmitModalClose = () => 55 | setConfirmSubmitModalActive(false); 56 | 57 | const onSubmit = () => { 58 | // setFetchCorrectAns(true); 59 | handleConfirmSubmitModalOpen(); 60 | }; 61 | 62 | const findScore = (quizCorrectAnsData: any) => { 63 | setQuizEnd(true); 64 | 65 | let score = 0; 66 | 67 | const responseWithAns: any = response.map((resp, index) => 68 | quizCorrectAnsData?.questions[index].title === resp.title 69 | ? { ...resp, correct: quizCorrectAnsData?.questions[index].correct } 70 | : { ...resp } 71 | ); 72 | 73 | setRespWithCorrectAns(responseWithAns); 74 | 75 | responseWithAns.forEach((res: any) => { 76 | if (res.correct === res.response) { 77 | score = score + 1; 78 | } 79 | }); 80 | 81 | setScore(score); 82 | 83 | mutateAsync( 84 | { body: { score, responses: responseWithAns } }, 85 | { 86 | onError: () => { 87 | enqueueSnackbar(errorMessages.default, { variant: "error" }); 88 | }, 89 | onSuccess: () => { 90 | enqueueSnackbar("Score Saved.", { variant: "success" }); 91 | }, 92 | onSettled: () => { 93 | // reset(); 94 | }, 95 | } 96 | ); 97 | }; 98 | 99 | useEffect(() => { 100 | const response = data?.questions.map((question: IQuestion) => ({ 101 | _id: question._id, 102 | title: question.title, 103 | quiz: question.quiz, 104 | options: question.options, 105 | response: "", 106 | })); 107 | 108 | setResponse(response); 109 | }, [data?.questions]); 110 | 111 | if (error?.response?.status) { 112 | return ( 113 | 117 | ); 118 | } 119 | if (data?.questions.length === 0) { 120 | return ( 121 |
122 | 123 |
124 | ); 125 | } 126 | 127 | return isLoading || isFetching ? ( 128 | 129 | ) : ( 130 |
134 |
135 | {!quizEnd ? ( 136 | <> 137 | 143 |
144 |
145 |

146 | Question {activeIndex + 1} 147 |

148 | {!quizEnd && ( 149 |
150 |

151 | {response?.filter((resp) => resp.response !== "").length}/{" "} 152 | {data?.questions.length} Completed 153 |

154 |
155 |
resp.response !== "") 160 | .length / 161 | data?.questions.length) * 162 | 100 163 | }%`, 164 | }} 165 | >
166 |
167 |
168 | )} 169 | 170 | {!quizEnd && ( 171 | 178 | )} 179 |
180 | 187 | 191 | 197 | 198 |
199 | 200 | ) : ( 201 | <> 202 | {isQuizCorrectAnsLoading || isSaveScoreLoading ? ( 203 | 204 | ) : ( 205 | 210 | )} 211 | 212 | )} 213 |
214 |
215 | ); 216 | }; 217 | -------------------------------------------------------------------------------- /src/pages/QuizResponse.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useLocation, useParams } from "react-router-dom"; 3 | import { ErrorMessage } from "../components/ErrorMessage"; 4 | import { ShowResponses } from "../components/FinishQuiz"; 5 | import { Loader } from "../components/Svgs"; 6 | import { useMyAttemptById } from "../shared/queries"; 7 | 8 | interface Props {} 9 | 10 | export const QuizResponse: React.FC = () => { 11 | const { attemptId } = useParams() as { attemptId: string }; 12 | const { isLoading, isFetching, data, isSuccess, error } = 13 | useMyAttemptById(attemptId); 14 | const [score, setScore] = useState(0); 15 | const [respWithCorrectAns, setRespWithCorrectAns] = useState([]); 16 | const location = useLocation() as { state: { from: string } }; 17 | 18 | useEffect(() => { 19 | if (isSuccess) { 20 | setScore(data?.attempt.score); 21 | setRespWithCorrectAns( 22 | data?.responses.map((resp: any) => { 23 | return { 24 | quiz: resp.quiz, 25 | correct: resp.question.correct, 26 | title: resp.question.title, 27 | options: resp.question.options, 28 | response: resp.question.response, 29 | _id: resp.question._id, 30 | }; 31 | }) 32 | ); 33 | } 34 | }, [data?.attempt.score, data?.responses, isSuccess]); 35 | 36 | if (error?.response?.status) { 37 | return ( 38 | 39 | ); 40 | } 41 | 42 | return ( 43 |
44 | {isLoading || isFetching ? ( 45 | 46 | ) : ( 47 | <> 48 | {data?.responses && ( 49 | 59 | )} 60 | 61 | )} 62 |
63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /src/pages/Quizes.tsx: -------------------------------------------------------------------------------- 1 | import { Button, IconButton } from "@material-ui/core"; 2 | import * as React from "react"; 3 | import { useState } from "react"; 4 | import { FcClearFilters } from "react-icons/fc"; 5 | import { RiFilterFill } from "react-icons/ri"; 6 | import { EmptyResponse } from "../components/EmptyResponse"; 7 | import { ErrorMessage } from "../components/ErrorMessage"; 8 | import { FiltersForm } from "../components/forms/FiltersForm"; 9 | import { ModalSkeleton } from "../components/Modal"; 10 | import { QuizCard } from "../components/QuizCard"; 11 | import { Loader } from "../components/Svgs"; 12 | import { globalColors } from "../shared/constants"; 13 | import { IQuiz } from "../shared/interfaces"; 14 | import { useQuizes } from "../shared/queries"; 15 | import { endpoints } from "../shared/urls"; 16 | 17 | export const Quizes = () => { 18 | const [searchTerm, setSearchTerm] = useState(""); 19 | const [tag, setTag] = useState(""); 20 | const [currentPage, setCurrentPage] = useState(1); 21 | const [totalPages, setTotalPages] = useState(1); 22 | 23 | const { data, isLoading, isFetching, isSuccess, error } = useQuizes( 24 | `${endpoints.quizes}?search=${encodeURIComponent( 25 | searchTerm 26 | )}&tag=${encodeURIComponent(tag)}&page=${currentPage}`, 27 | ["Quizes", searchTerm, tag, currentPage] 28 | ); 29 | 30 | const [filtersOpen, setFiltersOpen] = useState(false); 31 | const handleFiltersOpen = () => setFiltersOpen(true); 32 | const handleFiltersClose = () => setFiltersOpen(false); 33 | 34 | const clearFilters = () => { 35 | setSearchTerm(""); 36 | setTag(""); 37 | }; 38 | 39 | React.useEffect(() => { 40 | if (isSuccess) { 41 | setTotalPages(data.count ? Math.ceil(data.count / 6) : 1); 42 | } 43 | }, [data?.count, isSuccess]); 44 | 45 | if (error?.response?.status) { 46 | return ( 47 | 51 | ); 52 | } 53 | 54 | return ( 55 |
56 |
57 |

All Quizes

58 |
59 | 60 | 61 | 62 | {(searchTerm || tag) && ( 63 | 64 | 65 | 66 | )} 67 |
68 |
69 | 70 | {isLoading || isFetching ? ( 71 | 72 | ) : data?.quizes.length > 0 ? ( 73 |
74 | {data?.quizes.map((quiz: IQuiz) => ( 75 | 76 | ))} 77 |
78 | ) : ( 79 |
80 | 87 |
88 | )} 89 |
90 | {totalPages > 1 && 91 | Array.from(Array(totalPages).keys()).map((loader, index) => ( 92 | 100 | ))} 101 |
102 | 103 | 110 | 111 |
112 | ); 113 | }; 114 | -------------------------------------------------------------------------------- /src/pages/SignIn.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/clerk-react"; 2 | 3 | export const SignInPage = () => ( 4 | <> 5 | 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /src/pages/Signup.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/clerk-react"; 2 | 3 | const SignUpPage = () => ( 4 | <> 5 | 6 | 7 | ); 8 | 9 | export default SignUpPage; 10 | -------------------------------------------------------------------------------- /src/pages/UpdateQuestion.tsx: -------------------------------------------------------------------------------- 1 | import { useParams } from "react-router-dom"; 2 | import { UpdateQuestionForm } from "../components/forms/UpdateQuestionForm"; 3 | import { Loader } from "../components/Svgs"; 4 | import { useQuizQuestion } from "../shared/queries"; 5 | 6 | interface Props {} 7 | 8 | export const UpdateQuestion: React.FC = () => { 9 | const { questionId, quizId } = useParams() as { 10 | quizId: string; 11 | questionId: string; 12 | }; 13 | 14 | const { isLoading, data } = useQuizQuestion(quizId, questionId); 15 | 16 | return ( 17 |
18 |

19 | Update Question 20 |

21 |
22 | {isLoading ? ( 23 | <> 24 | 25 | 26 | ) : ( 27 | 28 | )} 29 |
30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/pages/UpdateQuiz.tsx: -------------------------------------------------------------------------------- 1 | import { useUser } from "@clerk/clerk-react"; 2 | import { useParams } from "react-router-dom"; 3 | import { ErrorMessage } from "../components/ErrorMessage"; 4 | import { QuizForm } from "../components/forms/QuizForm"; 5 | import { Loader } from "../components/Svgs"; 6 | import { useQuiz, useUpdateQuiz } from "../shared/queries"; 7 | 8 | interface Props {} 9 | 10 | export const UpdateQuiz: React.FC = () => { 11 | const { id } = useParams() as { id: string }; 12 | const { mutateAsync, reset } = useUpdateQuiz(id); 13 | const { data, isLoading, isFetching, isSuccess, error } = useQuiz(id); 14 | const { id: userId } = useUser(); 15 | 16 | if (error?.response?.status) { 17 | return ; 18 | } 19 | 20 | if (isSuccess && data?.quiz.author !== userId) { 21 | return ; 22 | } 23 | 24 | return ( 25 |
26 |

Update a Quiz

27 | {isLoading || isFetching ? ( 28 | 29 | ) : ( 30 |
31 | 37 |
38 | )} 39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/pages/UserProfile.tsx: -------------------------------------------------------------------------------- 1 | import { UserProfile } from "@clerk/clerk-react"; 2 | 3 | export const UserProfilePage = () => { 4 | return ; 5 | }; 6 | -------------------------------------------------------------------------------- /src/pages/stats/StatisticsAllQuestions.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ColDef, 3 | ColumnApi, 4 | GridApi, 5 | ICellRendererParams, 6 | } from "ag-grid-community"; 7 | import { useEffect, useState } from "react"; 8 | import { Link, useParams } from "react-router-dom"; 9 | import { DownloadButton } from "../../components/Dropdown"; 10 | import { ErrorMessage } from "../../components/ErrorMessage"; 11 | import { GridWrapper } from "../../components/GridWrapper"; 12 | import { formatDate } from "../../shared/formatDate"; 13 | import { useQuizQuestionCorrectAns } from "../../shared/queries"; 14 | 15 | interface Props {} 16 | 17 | export const StatisticsAllQuestions: React.FC = () => { 18 | const { quizId } = useParams() as { quizId: string }; 19 | 20 | const { isLoading, data, isSuccess, error } = 21 | useQuizQuestionCorrectAns(quizId); 22 | 23 | const [selected, setSelected] = useState([]); 24 | const [gridApi, setGridApi] = useState(); 25 | const [gridColumnApi, setGridColumnApi] = useState(); 26 | 27 | const [list, setList] = useState([]); 28 | 29 | useEffect(() => { 30 | if (isSuccess) { 31 | setList( 32 | data?.questions 33 | ? data?.questions.map((question: any) => { 34 | return { 35 | quiz: question.quiz, 36 | correct: question.correct, 37 | title: question.title, 38 | option1: question.options[0].value, 39 | option2: question.options[1].value, 40 | option3: question.options[2].value, 41 | option4: question.options[3].value, 42 | _id: question._id, 43 | updatedAt: formatDate(question.updatedAt), 44 | }; 45 | }) 46 | : [] 47 | ); 48 | } 49 | }, [data?.questions, isSuccess]); 50 | 51 | if (error?.response?.status) { 52 | return ( 53 | 54 | ); 55 | } 56 | 57 | const colDefs: ColDef[] = [ 58 | { 59 | headerName: "", 60 | field: "select", 61 | checkboxSelection: true, 62 | headerCheckboxSelection: true, 63 | cellStyle: { display: "flex", textAlign: "center" }, 64 | maxWidth: 100, 65 | }, 66 | { 67 | headerName: "Question", 68 | field: "title", 69 | minWidth: 250, 70 | cellRendererFramework: (params: ICellRendererParams) => ( 71 | 75 | {params.data.title} 76 | 77 | ), 78 | }, 79 | 80 | { 81 | headerName: "Option 1", 82 | field: "option1", 83 | }, 84 | { 85 | headerName: "Option 2", 86 | field: "option2", 87 | }, 88 | { 89 | headerName: "Option 3", 90 | field: "option3", 91 | }, 92 | { 93 | headerName: "Option 4", 94 | field: "option4", 95 | }, 96 | { 97 | headerName: "Correct", 98 | field: "correct", 99 | }, 100 | { 101 | headerName: "Updated At", 102 | field: "updatedAt", 103 | }, 104 | ]; 105 | 106 | return ( 107 |
108 |
109 |

110 | Please click on Question 111 | (highlited) to view Question 112 | Statistics. 113 |

114 | 121 |
122 |
123 | 131 |
132 |
133 | ); 134 | }; 135 | -------------------------------------------------------------------------------- /src/pages/stats/StatisticsByQuiz.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@material-ui/core"; 2 | import { 3 | ColDef, 4 | ColumnApi, 5 | GridApi, 6 | ICellRendererParams, 7 | } from "ag-grid-community"; 8 | import { useEffect, useState } from "react"; 9 | import { useNavigate, useParams } from "react-router-dom"; 10 | import { DownloadButton } from "../../components/Dropdown"; 11 | import { GridWrapper } from "../../components/GridWrapper"; 12 | import { IStatsByQuiz } from "../../shared/interfaces"; 13 | import { useStatsByQuizId } from "../../shared/queries"; 14 | 15 | interface Props {} 16 | 17 | export const StatisticsByQuiz: React.FC = () => { 18 | const { quizId } = useParams() as { quizId: string }; 19 | const { isLoading, data, isSuccess } = useStatsByQuizId(quizId); 20 | const [list, setList] = useState([]); 21 | const [selected, setSelected] = useState([]); 22 | const [gridApi, setGridApi] = useState(); 23 | const [gridColumnApi, setGridColumnApi] = useState(); 24 | const navigate = useNavigate(); 25 | 26 | useEffect(() => { 27 | if (isSuccess) { 28 | const user = data.users.map((user: IStatsByQuiz) => { 29 | return { 30 | score: user.attempt.score, 31 | attemptId: user.attempt._id, 32 | quizId: user.attempt.quiz, 33 | ...user.user, 34 | maxAttempts: user.maxAttempts.val, 35 | }; 36 | }); 37 | setList(user); 38 | } 39 | }, [data?.users, isSuccess]); 40 | 41 | const colDefs: ColDef[] = [ 42 | { 43 | headerName: "", 44 | field: "select", 45 | checkboxSelection: true, 46 | headerCheckboxSelection: true, 47 | maxWidth: 100, 48 | cellStyle: { display: "flex", textAlign: "center" }, 49 | }, 50 | { 51 | headerName: "Photo", 52 | field: "photo", 53 | autoHeight: true, 54 | minWidth: 80, 55 | cellRendererFramework: (params: ICellRendererParams) => ( 56 |
57 | {"User"} 62 |
63 | ), 64 | }, 65 | { 66 | headerName: "First Name", 67 | field: "firstName", 68 | }, 69 | { 70 | headerName: "Last Name", 71 | field: "lastName", 72 | }, 73 | { 74 | headerName: "Email", 75 | field: "email", 76 | minWidth: 250, 77 | }, 78 | { 79 | headerName: "1st Attempt Score", 80 | field: "score", 81 | }, 82 | { 83 | headerName: "Max Attempts", 84 | field: "maxAttempts", 85 | }, 86 | { 87 | headerName: "View Attempt", 88 | field: "view_attempt", 89 | cellRendererFramework: (params: ICellRendererParams) => ( 90 |
91 |

93 | navigate(`/dashboard/attempts/${params.data.attemptId}`, { 94 | state: { from: "STATISTICS" }, 95 | }) 96 | } 97 | className="text-indigo-600 hover:underline cursor-pointer" 98 | > 99 | View Attempt 100 |

101 |
102 | ), 103 | }, 104 | ]; 105 | 106 | return ( 107 |
108 |
109 |
110 | 117 |
118 | 125 |
126 |
127 | 134 |
135 |
136 | ); 137 | }; 138 | -------------------------------------------------------------------------------- /src/pages/stats/StatisticsByQuizQuestionsId.tsx: -------------------------------------------------------------------------------- 1 | import { useParams } from "react-router-dom"; 2 | import { ErrorMessage } from "../../components/ErrorMessage"; 3 | import { Loader } from "../../components/Svgs"; 4 | import { IOptionWithFrequency } from "../../shared/interfaces"; 5 | import { useStatsByQuizIdByQuestionId } from "../../shared/queries"; 6 | 7 | interface Props {} 8 | 9 | export const StatisticsByQuizQuestionsId: React.FC = () => { 10 | const { quizId, questionId } = useParams() as { 11 | quizId: string; 12 | questionId: string; 13 | }; 14 | 15 | const { isLoading, error, data } = useStatsByQuizIdByQuestionId( 16 | quizId, 17 | questionId 18 | ); 19 | if (error?.response?.status) { 20 | return ( 21 | 25 | ); 26 | } 27 | return ( 28 | <> 29 |
30 |

31 | Question Statistics 32 |

33 | 34 | {isLoading ? ( 35 | 36 | ) : ( 37 | <> 38 |
44 |
45 |

Total Responses

46 |

{data?.totalResponses}

47 |
48 |
49 |

Total Correct Responses

50 |

{data?.totalCorrectResponses}

51 |
52 |
53 |

Total Empty Responses (Omitted)

54 |

{data?.totalEmptyResponses}

55 |
56 |
57 |

Total Incorrect

58 |

{data?.totalIncorrectResponses}

59 |
60 |
61 |

{data?.question.title}

62 | 63 |
64 | {data?.question?.options.map((option: IOptionWithFrequency) => { 65 | const percentage = 66 | (option.frequency / data?.totalResponses) * 100; 67 | return ( 68 |
73 |
77 | 89 |   90 | 91 |

92 | {option.value} 93 |

94 |
95 |

96 | {Math.round(percentage)}% 97 |

98 |
99 | ); 100 | })} 101 |
102 |
106 | 107 | Percentage of users omitted this question : 108 | 109 |

110 | {Math.round( 111 | (data?.totalEmptyResponses / data?.totalResponses) * 100 112 | )} 113 | % 114 |

115 |
116 |
120 | Total : 121 |

100%

122 |
123 | 124 | )} 125 |
126 | 127 | ); 128 | }; 129 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare module 'pdfmake'; 3 | declare module 'pdfmake/build/pdfmake'; 4 | declare module 'pdfmake/build/vfs_fonts'; 5 | // declare module 'pdfmake/interfaces'; -------------------------------------------------------------------------------- /src/shared/constants.ts: -------------------------------------------------------------------------------- 1 | export const errorMessages = { 2 | default: 'Something went wrong, please try again later.', 3 | notFound: (resource?: string) => `${resource || 'Resource'} not found.`, 4 | auth403: `You do not have the permission to do this action.`, 5 | } 6 | 7 | export const globalColors = { 8 | brand: '#4f46e5', 9 | red: '#e11d48' 10 | } 11 | 12 | type TactionSuccess = 13 | | 'Updated' 14 | | 'Deleted' 15 | | 'Created'; 16 | 17 | type TactionLoading = 18 | | 'Updating' 19 | | 'Deleting' 20 | | 'Creating'; 21 | 22 | type TResource = 'Question' | 'Quiz' 23 | 24 | export const successMessages = { 25 | actionSuccess: (action: TactionSuccess, resource?: TResource) => 26 | `Successfully ${action} ${resource || 'resource'}`, 27 | } 28 | 29 | export const loadingMessages = { 30 | actionLoading: (action: TactionLoading, resource?: TResource) => 31 | `${action} ${resource || 'resource'}`, 32 | } 33 | 34 | export const emptyResponseMessages = { 35 | attempt: ['You have not attempted any quizes yet.'], 36 | responses: ["You can only see responses to first attempt at any quiz."], 37 | dashboardQuizes: ['You have not created any quizes yet.'], 38 | quizQuestions: ['This quiz have no questions.'], 39 | mainQuizes: ['There are no active Quizes at the moment.', 'Go ahead make a Quiz.'], 40 | filteredQuizes: ['No active Quizes found with the given filters.'], 41 | } 42 | 43 | export const uiMessages = { 44 | allowedMarkingACorrectOption: ['* Marking a correct option is only allowed after you have already written all options.'], 45 | warnQuestionCreate: ['Note: Please be careful before creating a question because if you have to later edit it you will lose all responses to the question.', ' This is done so that we can provide you better and correct statistics.'] 46 | } 47 | 48 | export const modalStyle = { 49 | position: 'absolute' as 'absolute', 50 | top: '50%', 51 | left: '50%', 52 | transform: 'translate(-50%, -50%)', 53 | width: '40vw', 54 | bgcolor: '#ffffff', 55 | overflow: 'auto', 56 | boxShadow: 24, 57 | padding: '1rem 2rem', 58 | border: 0, 59 | borderRadius: '6px', 60 | }; 61 | -------------------------------------------------------------------------------- /src/shared/formatDate.ts: -------------------------------------------------------------------------------- 1 | const SECOND_IN_MILLISECOND = 1000; 2 | const MINUTE_IN_SECS = 60; 3 | const HOUR_IN_SECS = 60 * MINUTE_IN_SECS; 4 | const DAY_IN_SECS = 24 * HOUR_IN_SECS; 5 | const MONTHS = [ 6 | 'January', 7 | 'February', 8 | 'March', 9 | 'April', 10 | 'May', 11 | 'June', 12 | 'July', 13 | 'August', 14 | 'September', 15 | 'October', 16 | 'November', 17 | 'December', 18 | ]; 19 | 20 | function jsCoreDateCreator(dateString: string) { 21 | // ref: https://github.com/facebook/react-native/issues/15819#issuecomment-369976505 22 | // dateString *HAS* to be in this format "YYYY-MM-DD HH:MM:SS" 23 | const _timestamp = dateString.split('.')[0]; 24 | const dateParam: any[] = _timestamp.split(/[\s-:T]/); 25 | dateParam[1] = (parseInt(dateParam[1], 10) - 1).toString(); 26 | // @ts-ignore 27 | return new Date(...dateParam); 28 | } 29 | 30 | const getLocalDateFromUTC = (timestamp: string | Date): Date => { 31 | // convert server (UTC) date to local date 32 | 33 | const _date = 34 | typeof timestamp === 'string' 35 | ? jsCoreDateCreator(`${timestamp}`) 36 | : new Date(`${timestamp}`); 37 | 38 | const tzOffsetInMs = 39 | _date.getTimezoneOffset() * MINUTE_IN_SECS * SECOND_IN_MILLISECOND; 40 | return new Date(_date.getTime() - tzOffsetInMs); 41 | }; 42 | 43 | export const formatDate = (timestamp: string | Date, options?: any): string => { 44 | const _options = { 45 | fullMonth: false, 46 | withDate: true, 47 | withMonth: true, 48 | relativeDepth: ['secs', 'mins', 'hrs', 'days'], 49 | ...(options && Object.keys(options).length ? options : {}), 50 | }; 51 | 52 | const nownow = new Date(); 53 | const datetime = getLocalDateFromUTC(timestamp); 54 | const timeDiff = (nownow.getTime() - datetime.getTime()) / 1000; 55 | 56 | if (!_options.absolute) { 57 | // absolute date is not required 58 | if ( 59 | timeDiff < MINUTE_IN_SECS && 60 | _options.relativeDepth.includes('secs') 61 | ) { 62 | return 'just now'; 63 | } else if ( 64 | timeDiff < MINUTE_IN_SECS * 2 && 65 | _options.relativeDepth.includes('mins') 66 | ) { 67 | return 'a min ago'; 68 | } else if ( 69 | timeDiff < HOUR_IN_SECS && 70 | _options.relativeDepth.includes('mins') 71 | ) { 72 | return Math.round(timeDiff / MINUTE_IN_SECS) + ' min ago'; 73 | } else if ( 74 | timeDiff < DAY_IN_SECS && 75 | _options.relativeDepth.includes('hrs') 76 | ) { 77 | return Math.round(timeDiff / HOUR_IN_SECS) + ' hr ago'; 78 | } else if ( 79 | timeDiff < DAY_IN_SECS && 80 | nownow.getDate() === datetime.getDate() && 81 | _options.relativeDepth.includes('days') 82 | ) { 83 | return 'Today'; // + ' | ' + nownow.getDate() + ' - ' + datetime.getDate(); 84 | } else if ( 85 | timeDiff < DAY_IN_SECS * 2 && 86 | nownow.getDate() - datetime.getDate() === 1 && 87 | _options.relativeDepth.includes('days') 88 | ) { 89 | return 'Yesterday'; 90 | } 91 | } 92 | 93 | let timeString = ''; 94 | 95 | if (_options.withMonth) { 96 | if (_options.fullMonth) { 97 | timeString += MONTHS[datetime.getMonth()]; 98 | } else { 99 | timeString += MONTHS[datetime.getMonth()].substr(0, 3); 100 | } 101 | } 102 | if (_options.withDate) { 103 | timeString += ` ${datetime.getDate()}`; 104 | } 105 | if (_options.withYear) { 106 | timeString += (() => { 107 | const _year = datetime.getFullYear(); 108 | return nownow.getFullYear() === _year ? '' : `, ${_year}`; 109 | })(); 110 | } 111 | if (_options.withTime) { 112 | timeString += (() => { 113 | let _hr: any = datetime.getHours(); 114 | if (_options.timeFormat12hr) { 115 | if (_hr === 0) _hr = 12; 116 | else if (_hr > 12) _hr = _hr - 12; 117 | } 118 | 119 | if (_hr < 10) { 120 | _hr = '0' + _hr; 121 | } 122 | 123 | return ` ${_hr}`; 124 | })(); 125 | timeString += (() => { 126 | let _mins: any = datetime.getMinutes(); 127 | if (_mins < 10) { 128 | _mins = '0' + _mins; 129 | } 130 | return `:${_mins}`; 131 | })(); 132 | 133 | if (_options.timeFormat12hr) { 134 | timeString += (() => { 135 | const _hr = datetime.getHours(); 136 | if (_hr < 12) return ' AM'; 137 | else return ' PM'; 138 | })(); 139 | } 140 | } 141 | 142 | // return `${datetime.getDate()} | ${timeString}`; 143 | return timeString; 144 | }; 145 | -------------------------------------------------------------------------------- /src/shared/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IQuestionForm { 2 | title: string; 3 | correct: string; 4 | options: IOption[]; 5 | } 6 | export interface IQuestion { 7 | _id: string; 8 | quiz: string; // unpopulated 9 | title: string; 10 | options: IOption[]; 11 | } 12 | 13 | export interface IOption { 14 | value: string; 15 | _id?: string; 16 | } 17 | 18 | export interface IOptionWithFrequency extends IOption { 19 | frequency: number 20 | } 21 | 22 | export interface IResponse extends IQuestion { 23 | response: string; 24 | } 25 | 26 | export interface IResponseWithCorrect extends IResponse { 27 | correct: string; 28 | } 29 | 30 | export interface IQuizForm { 31 | title: string; 32 | description: string; 33 | tags: string[]; 34 | status?: string; 35 | } 36 | 37 | export interface IQuiz extends IQuizForm { 38 | status: string; 39 | _id: string; 40 | author: string; 41 | createdAt: Date; 42 | updatedAt: Date; 43 | __v: number; 44 | id: string; 45 | attemptsCount: number; 46 | questionsCount: number; 47 | } 48 | 49 | export interface IAttempt { 50 | _id: string; 51 | userId: string; 52 | score: number; 53 | quiz: IQuiz 54 | } 55 | 56 | export interface IStatsByQuiz { 57 | attempt: { 58 | createdAt: Date, 59 | id: string, 60 | _id: string, 61 | score: number; 62 | quiz: { 63 | description: string; 64 | createdAt: string; 65 | author: string; 66 | id: string; 67 | status: string; 68 | tags: string[]; 69 | title: string; 70 | updatedAt: Date; 71 | } 72 | }; 73 | maxAttempts: { 74 | userId: string; 75 | val: number; 76 | }; 77 | user: { 78 | email: string; 79 | firstName: string; 80 | lastName: string; 81 | photo: string; 82 | userId: string 83 | } 84 | } -------------------------------------------------------------------------------- /src/shared/queries.ts: -------------------------------------------------------------------------------- 1 | import { useSession } from "@clerk/clerk-react"; 2 | import axios, { AxiosError, AxiosResponse } from "axios"; 3 | import { MutationOptions, QueryKey, UseQueryOptions, useMutation, useQuery } from "react-query"; 4 | import { endpoints } from "./urls"; 5 | 6 | export const PublicQueryFactory = (queryKey: QueryKey, url: string, options?: UseQueryOptions) => { 7 | 8 | return useQuery( 9 | queryKey, 10 | async () => { 11 | return axios({ 12 | url, 13 | method: 'GET', 14 | }).then( 15 | (result: AxiosResponse) => result.data 16 | ) 17 | }, { 18 | refetchOnWindowFocus: false, 19 | retry: false, 20 | ...options 21 | } 22 | ) 23 | } 24 | 25 | export const QueryFactory = (queryKey: QueryKey, url: string, options?: UseQueryOptions) => { 26 | const { getToken } = useSession(); 27 | return useQuery( 28 | queryKey, 29 | async () => { 30 | const token = await getToken(); 31 | return axios({ 32 | url, 33 | method: 'GET', 34 | headers: { 35 | "Authorization": `Bearer ${token}` 36 | }, 37 | }).then( 38 | (result: AxiosResponse) => result.data 39 | ) 40 | }, { 41 | refetchOnWindowFocus: false, 42 | retry: false, 43 | ...options 44 | } 45 | ) 46 | } 47 | 48 | const MutationFactory = (mutationKey: QueryKey, url: string, method: 'POST' | 'PUT' | 'PATCH', options?: MutationOptions) => { 49 | const { getToken } = useSession(); 50 | return useMutation({ 51 | mutationKey, 52 | mutationFn: async (variables: { body: any }) => { 53 | const token = await getToken() 54 | return axios({ 55 | url, 56 | method, 57 | headers: { 58 | "Authorization": `Bearer ${token}` 59 | }, 60 | data: variables.body 61 | }).then( 62 | (response: AxiosResponse) => response.data 63 | ) 64 | }, 65 | ...options 66 | }) 67 | 68 | } 69 | 70 | const DeleteMutationFactory = (mutationKey: QueryKey, url: string, options?: MutationOptions) => { 71 | const { getToken } = useSession(); 72 | return useMutation({ 73 | mutationKey, 74 | mutationFn: async () => { 75 | const token = await getToken() 76 | return axios({ 77 | url, 78 | method: 'DELETE', 79 | headers: { 80 | "Authorization": `Bearer ${token}` 81 | }, 82 | }).then( 83 | (response: AxiosResponse) => response.data 84 | ) 85 | }, 86 | ...options 87 | }) 88 | 89 | } 90 | 91 | 92 | export const useQuizes = (url: string, queryKey: QueryKey, options?: UseQueryOptions) => QueryFactory(queryKey, url, options); 93 | export const useQuiz = (id: string, options?: UseQueryOptions) => QueryFactory(['Quiz', id], endpoints.quizById(id), options); 94 | export const useQuizQuestions = (id: string, options?: UseQueryOptions) => QueryFactory(['Quiz Questions', id], endpoints.quizQuestions(id), options); 95 | export const useQuizQuestion = (quizId: string, questionId: string, options?: UseQueryOptions) => QueryFactory(['Quiz Question', quizId, questionId], endpoints.quizQuestionById(quizId, questionId), options); 96 | export const useQuizQuestionCorrectAns = (quizId: string, options?: UseQueryOptions) => QueryFactory(['Quiz Question Correct', quizId], endpoints.quizQuestionsCorrectAns(quizId), options); 97 | export const useMyAttempts = (url: string, queryKey: QueryKey, options?: UseQueryOptions) => QueryFactory(queryKey, url, options); 98 | export const useMyAttemptById = (id: string, options?: UseQueryOptions) => QueryFactory(['Attempts', id], endpoints.attemptsById(id), options); 99 | export const useStatsByQuizId = (id: string, options?: UseQueryOptions) => QueryFactory(['Statistics', id], endpoints.statsByQuizId(id), options); 100 | export const useStatsByQuizIdByQuestionId = (quizId: string, questionId: string, options?: UseQueryOptions) => QueryFactory(['Statistics', quizId, questionId], endpoints.statsByQuizIdbyQuestionId(quizId, questionId), options); 101 | export const useStats = (options?: UseQueryOptions) => PublicQueryFactory(['Statistics'], endpoints.stats, options); 102 | 103 | export const useCreateQuiz = (options?: MutationOptions) => MutationFactory('Create Quiz', endpoints.quizes, 'POST', options) 104 | export const useCreateAIQuestion = (quizId: string, options?: MutationOptions) => MutationFactory('Create AI Question', endpoints.quizAIQuestions(quizId), 'POST', options) 105 | export const useCreateQuestion = (id: string, options?: MutationOptions) => MutationFactory('Create Question', endpoints.quizQuestions(id), 'POST', options) 106 | export const useSaveScore = (id: string, options?: MutationOptions) => MutationFactory(['Save Score', id], endpoints.saveScore(id), 'POST', options) 107 | 108 | export const useUpdateQuiz = (id: string, options?: MutationOptions) => MutationFactory('Update Quiz', endpoints.quizes + id, 'PATCH', options) 109 | export const useUpdateQuestion = (quizId: string, questionId: string, options?: MutationOptions) => MutationFactory('Update Question', endpoints.quizQuestionById(quizId, questionId), 'PATCH', options) 110 | 111 | export const useDeleteQuestion = (quizId: string, questionId: string, options?: MutationOptions) => DeleteMutationFactory('Delete Question', endpoints.quizQuestionById(quizId, questionId), options) 112 | export const useDeleteQuiz = (quizId: string, options?: MutationOptions) => DeleteMutationFactory('Delete Question', endpoints.quizById(quizId), options) 113 | -------------------------------------------------------------------------------- /src/shared/sampleQuizes.ts: -------------------------------------------------------------------------------- 1 | // No Longer used since API Integration 2 | export const JSQuiz = [ 3 | { 4 | id: 1, 5 | question: "An infinity iterator is one that never expresses true through what property?", 6 | correct: 'done', 7 | options: [ 8 | { value: "complete" }, 9 | { value: "done" }, 10 | { value: "end" }, 11 | { value: "finished" }, 12 | ], 13 | }, 14 | { 15 | id: 2, 16 | question: "You managed a web page that allows external customers to register for a logon to your company's intranet. The webpage has a vulnerability where users can access variables that have sensitive data. What could you implement to prevent this from happening?", 17 | options: [ 18 | { value: "Client-side Node.js" }, 19 | { value: "Input/Output (I/O) standardization" }, 20 | { value: "Getters and setters" }, 21 | { value: "Multi-threading" }, 22 | ], 23 | correct: 'Getters and setters', 24 | }, 25 | 26 | { 27 | id: 3, 28 | question: "What is a consequence of setting exported modules to strict mode by default?", 29 | options: [ 30 | { value: "Export statements cannot be imported without being renamed." }, 31 | { value: "Export statements cannot be used in most non-JavaScript web frameworks." }, 32 | { value: "Export statement cannot use variables such as const or let" }, 33 | { value: "Export statements cannot be used in embedded scripts" }, 34 | ], 35 | correct: "Export statements cannot be used in embedded scripts", 36 | }, 37 | { 38 | id: 4, 39 | question: "What object is returned when you call map.prototype.entries()?", 40 | options: [ 41 | { value: "An iterator" }, 42 | { value: "An array" }, 43 | { value: "A generators" }, 44 | { value: "A WeakMap" }, 45 | ], 46 | correct: "An iterator", 47 | }, 48 | { 49 | id: 5, 50 | question: "After the following code is executed, what is the printed to the console? console.log(eval('2 + 2'));", 51 | options: [ 52 | { value: "4" }, 53 | { value: "NaN" }, 54 | { value: "22" }, 55 | { value: "true" }, 56 | ], 57 | correct: "4" 58 | }, 59 | { 60 | id: 6, 61 | question: "What method determined if two values are the same?", 62 | options: [ 63 | { value: "data.cal()" }, 64 | { value: "object.is()" }, 65 | { value: "var.compare()" }, 66 | { value: "data.isEqual()" }, 67 | ], 68 | correct: 'object.is()' 69 | }, 70 | { 71 | id: 7, 72 | question: "What type of loop iterates over the properties of an object?", 73 | options: [ 74 | { value: "for" }, 75 | { value: "for...of" }, 76 | { value: "forEach" }, 77 | { value: "for...in" }, 78 | ], 79 | correct: 'for...in' 80 | }, 81 | { 82 | id: 8, 83 | question: "What is one way to create an object without a prototype to prevent Prototype Pollution?", 84 | options: [ 85 | { value: "Object.create()" }, 86 | { value: "Object.constructor()" }, 87 | { value: "Object.proto()" }, 88 | { value: "Object.type()" }, 89 | ], 90 | correct: 'Object.create()' 91 | }, 92 | { 93 | id: 9, 94 | question: "Why was JavaScript's browser security model designed?", 95 | options: [ 96 | { value: "To protect the server from receiving denial-of-service (DoS) attacks" }, 97 | { value: "To protect website owners from receiving amlicious attacks" }, 98 | { value: "To protect the internet browser from being corrupted" }, 99 | { value: "To protect users from malicious websites" }, 100 | ], 101 | correct: 'To protect users from malicious websites' 102 | }, 103 | { 104 | id: 10, 105 | question: "How would you make a new key flavor equal to sweet in a Map called food?", 106 | options: [ 107 | { value: "food.create('flavor', 'sweet');" }, 108 | { value: "food.add('flavor', 'sweet');" }, 109 | { value: "food.set('flavor','sweet');" }, 110 | { value: "food['flavor'] = 'sweet';" }, 111 | ], 112 | correct: "food.set('flavor','sweet');" 113 | }, 114 | ]; 115 | -------------------------------------------------------------------------------- /src/shared/theme.tsx: -------------------------------------------------------------------------------- 1 | import { createTheme } from "@material-ui/core"; 2 | 3 | export const theme = createTheme({ 4 | typography: { 5 | fontFamily: `"CircularStd-Book", "Helvetica", "Arial", sans-serif`, 6 | fontSize: 14, 7 | fontWeightLight: 300, 8 | fontWeightRegular: 400, 9 | fontWeightMedium: 500, 10 | }, 11 | palette: { 12 | primary: { 13 | main: "#4f46e5", 14 | }, 15 | secondary: { 16 | main: "#e11d48", 17 | }, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/shared/urls.ts: -------------------------------------------------------------------------------- 1 | // export const baseURL = 'https://quizco.herokuapp.com/api/v1'; 2 | export const baseURL = process.env.NODE_ENV === 'development' ? 'http://localhost:3001/api/v1' : 'https://quizco-backend.onrender.com/api/v1'; 3 | 4 | 5 | export const endpoints = { 6 | quizes: `${baseURL}/quizes/`, 7 | quizById: (id: string) => `${baseURL}/quizes/${id}/`, 8 | quizQuestions: (id: string) => `${baseURL}/quizes/${id}/questions`, 9 | quizAIQuestions: (id: string) => `${baseURL}/quizes/${id}/questions/ai`, 10 | quizQuestionById: (quizId: string, questionId: string) => `${baseURL}/quizes/${quizId}/questions/${questionId}`, 11 | quizQuestionsCorrectAns: (quizId: string) => `${baseURL}/quizes/${quizId}/questions/correct`, 12 | saveScore: (quizId: string) => `${baseURL}/quizes/${quizId}/attempts/save-score`, 13 | attempts: `${baseURL}/quizes/attempts`, 14 | attemptsById: (attemptId: string) => `${baseURL}/quizes/attempts/${attemptId}`, 15 | statsByQuizId: (quizId: string) => `${baseURL}/quizes/statistics/${quizId}`, 16 | statsByQuizIdbyQuestionId: (quizId: string, questionId: string) => `${baseURL}/quizes/statistics/${quizId}/questions/${questionId}`, 17 | stats: `${baseURL}/stats/` 18 | } 19 | -------------------------------------------------------------------------------- /src/shared/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | import { getIn } from "formik"; 3 | 4 | export const FormikError = (errors: any, touched: any, fieldName: any) => { 5 | const error = getIn(errors, fieldName); 6 | const touch = getIn(touched, fieldName); 7 | return touch && error ? error : null; 8 | }; 9 | 10 | -------------------------------------------------------------------------------- /src/shared/validationSchema.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup'; 2 | 3 | export const AddEditQuizValidation = Yup.object().shape({ 4 | title: Yup.string().required('Title is required.'), 5 | description: Yup.string().required('Description is required.'), 6 | tags: Yup.array().of(Yup.string()).min(1, 'At least one tag is required'), 7 | }); 8 | 9 | export const AddEditQuestionValidation = Yup.object().shape({ 10 | title: Yup.string().required('Title is required.'), 11 | correct: Yup.string().required('Correct Option is Required.'), 12 | options: Yup.array().of( 13 | Yup.object().shape({ 14 | value: Yup.string().required('Required.'), 15 | }) 16 | ), 17 | }) 18 | 19 | export const FiltersValidation = Yup.object().shape({ 20 | search: Yup.string().nullable(), 21 | tags: Yup.string().nullable(), 22 | }) -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | let colors = require('tailwindcss/colors') 4 | delete colors['lightBlue'] // <----- 5 | colors = { ...colors, ...{ transparent: 'transparent' } } 6 | 7 | module.exports = { 8 | purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'], 9 | darkMode: false, // or 'media' or 'class' 10 | theme: { 11 | extend: { 12 | minHeight: { 13 | auto: 'auto', 14 | '0': '0', 15 | '1/10': '20%', 16 | '1/4': '25%', 17 | '1/2': '50%', 18 | '3/4': '75%', 19 | 'full': '100%', 20 | 'screen': '100vh' 21 | }, 22 | fontFamily: { 23 | 'light': ['CircularStd-Light'], 24 | 'book': ['CircularStd-Book'], 25 | 'medium': ['CircularStd-Medium'], 26 | 'bold': ['CircularStd-Bold'], 27 | 'black': ['CircularStd-Black'], 28 | } 29 | }, 30 | colors: { 31 | ...colors, 32 | 'a': '#111111', 33 | 'input-hover': '#ebecf0', 34 | 'input-border': '#dfe1e6', 35 | 'input-bg': '#f4f5f7', 36 | 'input-color': '#172b4d' 37 | }, 38 | boxShadow: { 39 | modal: 'rgb(0 0 0 / 9%) 0px 3px 12px', 40 | small: 'rgb(0 0 0 / 7%) 0px 5px 10px', 41 | large: 'rgb(0 0 0 / 7%) 0px 5px 20px', 42 | 'large-modal': 'rgb(0 0 0 / 50%) 0px 16px 70px' 43 | }, 44 | }, 45 | variants: { 46 | variants: { 47 | backgroundColor: ({ after }) => after(['disabled']), 48 | opacity: ({ after }) => after(['disabled']), 49 | textColor: ({ after }) => after(['disabled']), 50 | cursor: ({ after }) => after(['disabled']), 51 | extend: {}, 52 | }, 53 | extend: { 54 | 55 | }, 56 | }, 57 | 58 | } 59 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------