├── .env.local.example ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ └── feature_request.md └── pull-request-template.md ├── .gitignore ├── .prettierrc.js ├── AUTH-DEV-NOTES.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── babel.config.js ├── components ├── aboutUs │ ├── Contributors.tsx │ ├── HeroSection.tsx │ ├── InfoSection.tsx │ ├── Resources.tsx │ └── styles.ts ├── contributepage │ ├── InfoSection.tsx │ └── ResourcesSection.tsx ├── frontpage │ ├── ChinguSection.tsx │ ├── HeroSection.tsx │ └── InfoSection.tsx ├── layout │ ├── Footer.tsx │ ├── Layout.tsx │ ├── MobileMenu.tsx │ └── styles.ts ├── quizSelection │ ├── QuizTile.tsx │ ├── TopicSelection.tsx │ ├── TopicSelectionChoice.tsx │ └── styles.ts ├── quizSingle │ ├── AnswerTileContainer.tsx │ ├── AnswerTileMark.tsx │ ├── AnswerTileText.tsx │ ├── NextQuestionBtn.tsx │ ├── QuestionHeader.tsx │ ├── ResultView.tsx │ ├── ScoreGraph.tsx │ ├── SubmitQuizBtn.tsx │ └── styles.ts └── shared │ ├── DisplayMessage.tsx │ ├── PageHeader.tsx │ ├── icons.tsx │ └── styles.ts ├── contexts └── quiz-context.tsx ├── db-setup.ts ├── db ├── config.ts ├── index.ts ├── roles.ts └── users.ts ├── docker-compose.yml ├── frontend-config.ts ├── jest.config.js ├── models ├── ChinguQuiz │ ├── Answer.ts │ ├── Question.ts │ ├── Quiz.ts │ └── QuizRecord.ts ├── UI │ └── Quizzes.ts ├── User │ ├── role.ts │ └── user.ts └── index.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── about.tsx ├── api │ ├── admin │ │ └── roles │ │ │ └── get.ts │ ├── auth │ │ └── [...nextauth].ts │ └── quiz-result.ts ├── contribute.tsx ├── data-privacy.tsx ├── index.tsx ├── profile.tsx ├── quiz │ └── [slug].tsx └── quizzes.tsx ├── public ├── Home.png ├── favicon.ico ├── home-chingu-image.png ├── home-chingu.svg ├── logo.png └── vercel.svg ├── quiz_db.sql ├── styles ├── Home.module.css ├── globals.css └── reset.css ├── test ├── roles.test.ts └── users.test.ts ├── tsconfig.json └── types └── node.d.ts /.env.local.example: -------------------------------------------------------------------------------- 1 | PGUSER=docker 2 | PGHOST=localhost 3 | PGDATABASE=docker 4 | PGPASSWORD=docker 5 | PGPORT=5432 6 | NEXT_PUBLIC_AUTH0_CLIENT_ID=... 7 | AUTH0_CLIENT_SECRET=... 8 | NEXT_PUBLIC_AUTH0_DOMAIN=... -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["eslint:recommended", "airbnb", "prettier"], 3 | rules: { 4 | "react/jsx-props-no-spreading": [1, { custom: "ignore" }], 5 | "react/jsx-filename-extension": [ 6 | 1, 7 | { extensions: [".js", ".jsx", ".ts", ".tsx", ".json"] }, 8 | ], 9 | "react/prop-types": 0, 10 | "jsx-a11y/anchor-is-valid": 0, 11 | "import/extensions": [ 12 | "error", 13 | "ignorePackages", 14 | { 15 | js: "never", 16 | mjs: "never", 17 | jsx: "never", 18 | ts: "never", 19 | tsx: "never", 20 | json: "never", 21 | }, 22 | ], 23 | }, 24 | settings: { 25 | "import/resolver": { 26 | node: { 27 | extensions: [".js", ".jsx", ".ts", ".tsx", ".json"], 28 | }, 29 | }, 30 | }, 31 | env: { 32 | browser: true, 33 | node: true, 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull-request-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pull Request Template 3 | about: Title 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | What is this PR about? 11 | 12 | Resolves #Number-of-Issue 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # DS_Store 2 | .DS_Store 3 | */.DS_Store 4 | **/.DS_Store 5 | 6 | # Node Modules 7 | node_modules/ 8 | 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | lerna-debug.log* 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | *.lcov 32 | 33 | # nyc test coverage 34 | .nyc_output 35 | 36 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | bower_components 41 | 42 | # node-waf configuration 43 | .lock-wscript 44 | 45 | # Compiled binary addons (https://nodejs.org/api/addons.html) 46 | build/Release 47 | 48 | # Dependency directories 49 | jspm_packages/ 50 | 51 | # TypeScript v1 declaration files 52 | typings/ 53 | 54 | # TypeScript cache 55 | *.tsbuildinfo 56 | 57 | # Optional npm cache directory 58 | .npm 59 | 60 | # Optional eslint cache 61 | .eslintcache 62 | 63 | # Microbundle cache 64 | .rpt2_cache/ 65 | .rts2_cache_cjs/ 66 | .rts2_cache_es/ 67 | .rts2_cache_umd/ 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variables file 79 | .env 80 | .env.test 81 | .env.local 82 | .env.test.local 83 | .env.production 84 | .env.db-setup 85 | 86 | # parcel-bundler cache (https://parceljs.org/) 87 | .cache 88 | 89 | # Next.js build output 90 | .next 91 | 92 | # Nuxt.js build / generate output 93 | .nuxt 94 | dist 95 | 96 | # Gatsby files 97 | .cache/ 98 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 99 | # https://nextjs.org/blog/next-9-1#public-directory-support 100 | # public 101 | 102 | # vuepress build output 103 | .vuepress/dist 104 | 105 | # Serverless directories 106 | .serverless/ 107 | 108 | # FuseBox cache 109 | .fusebox/ 110 | 111 | # DynamoDB Local files 112 | .dynamodb/ 113 | 114 | # TernJS port file 115 | .tern-port 116 | 117 | .idea/ 118 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: "avoid", 3 | bracketSpacing: true, 4 | htmlWhitespaceSensitivity: "css", 5 | insertPragma: false, 6 | jsxBracketSameLine: false, 7 | jsxSingleQuote: false, 8 | printWidth: 80, 9 | proseWrap: "preserve", 10 | quoteProps: "as-needed", 11 | requirePragma: false, 12 | semi: true, 13 | singleQuote: false, 14 | tabWidth: 2, 15 | trailingComma: "es5", 16 | useTabs: false, 17 | vueIndentScriptAndStyle: false, 18 | }; 19 | -------------------------------------------------------------------------------- /AUTH-DEV-NOTES.md: -------------------------------------------------------------------------------- 1 | # Notes 2 | 3 | ## Local Setup 4 | 5 | This is a Next.js project. It also requires a Postgresql database. A `docker-compose.yml` file is included 6 | for convenience so a test environment can be bootstrapped quickly. You do not have to use the 7 | `docker-compose.yml` file and probably won't use it on production but provision the Postgresql separately. 8 | The following instructions are geared towards a local setup on a Development machine. 9 | 10 | ### 0. Auth0 Setup 11 | 12 | This setup is required for Auth-related stuff to work. 13 | 14 | 1. Sign up with Auth0: https://auth0.com 15 | 2. A default application is created. We'll use that. Click on *Applications* and then *Applications* 16 | then the sole app *Default App* 17 | 3. Update the *Name* to whatever is appropriate 18 | 4. *Application Type* should be `Regular Web Application` 19 | 5. *Token Endpoint Authentication Method* should be `Post` 20 | 6. *Allowed Callback URLs* should be `http://localhost:3000/api/auth/callback/auth0`. (Note that this can be found 21 | by running the app and goinge here http://localhost:3000/api/auth/providers) 22 | 7. *Allowed Logout URLs* should be set to `http://localhost:3000`. This is so `returnTo` query param will work. 23 | 24 | ### 1. Creating a `.env.local` file 25 | 26 | You'll need to create a `.env.local` file with the following: 27 | 28 | ``` 29 | PGUSER=docker 30 | PGHOST=localhost 31 | PGDATABASE=docker 32 | PGPASSWORD=docker 33 | PGPORT=5432 34 | NEXT_PUBLIC_AUTH0_CLIENT_ID=... 35 | AUTH0_CLIENT_SECRET=... 36 | NEXT_PUBLIC_AUTH0_DOMAIN=... 37 | ``` 38 | 39 | Obtain the three Auth0-related values from: https://manage.auth0.com/dashboard and place in `.env.local` 40 | 41 | ### 2. Create a `.env.test.local` file for Test Database 42 | 43 | You'll need an `.env.test.local` which will be similar to `.env.local`. The following values should work: 44 | 45 | ``` 46 | PGUSER=docker 47 | PGHOST=localhost 48 | PGDATABASE=docker 49 | PGPASSWORD=docker 50 | PGPORT=15432 51 | ``` 52 | 53 | Run tests with `npm run test`. This will run Jest. 54 | 55 | ### 3. Running the app 56 | 57 | ```shell 58 | # 0. Ensure all dependencies are installed 59 | npm install 60 | 61 | # 1. Start everything 62 | npm run local 63 | 64 | # The above command takes over the terminal. Start a new terminal and continue. 65 | 66 | # 2. Create symlink .env.db-setup pointing to .env.local 67 | ln -s .env.local .env.db-setup 68 | 69 | # 3. Create Users table 70 | npm run db-setup 71 | 72 | # 4. Start your Next.js app 73 | npm run dev 74 | ``` 75 | 76 | Visit http://localhost:3000 77 | 78 | ## Production notes 79 | 80 | - Don't forget to do a one-time import of `quiz_db.sql` into your Postgresql instance 81 | 82 | ```shell 83 | # For example 84 | psql -U postgres -h host.somewhere -p 5432 moonshot < quiz_db.sql 85 | ``` 86 | 87 | ## Resource Links 88 | 89 | ### next-auth 90 | 91 | - Example Code: https://next-auth.js.org/getting-started/example 92 | - Auth0 Provider: https://next-auth.js.org/providers/auth0 93 | - REST API: https://next-auth.js.org/getting-started/rest-api 94 | - User Data Storage: https://auth0.com/docs/security/data-security/user-data-storage 95 | 96 | ### auth0 97 | 98 | - Next.js Guide: https://auth0.com/docs/quickstart/webapp/nextjs -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at jdmedlock@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from 71 | 72 | - [Axios](https://github.com/axios/axios) 73 | - [Contributor Covenant][homepage], version 1.4, available at 74 | [http://contributor-covenant.org/version/1/4][version] 75 | 76 | [homepage]: http://contributor-covenant.org 77 | [version]: http://contributor-covenant.org/version/1/4/ 78 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We welcome all contributions! 4 | 5 | To get started, please feel free to start working on any issues we have created and that are _not_ assigned to anyone yet. If you would like to pick up an issue, please leave a comment for the other contributors to see that this issue is already be worked on. 6 | 7 | For starters, we recommend to pick up issues labelled _good first issue_. 8 | 9 | In addition to everything written in here, there is a [Collaborator Guide](https://github.com/chingu-voyages/ChinguResourceList/blob/development/docs/COLLABORATOR_GUIDE.md) you can read into for additional information about collaborations. 10 | 11 | ## Table of Contents 12 | 13 | - [Process](#process) 14 | - [Git Branches](#git-branches) 15 | - [Commit Messages](#commit-messages) 16 | 17 | ## Process 18 | 19 | ### Initial Setup 20 | 21 | 1. Fork the repository 22 | 2. Clone your forked repository onto your local machine 23 | 3. Inside your CLI (Command Line Interface), move into your working directory 24 | 4. RUN `npm run docker-dev` if using docker or `npm run dev` otherwise. 25 | 26 | ### Installing packages 27 | Note: Initial setup will _not_ require npm i or npm ci, as it is done in the `npm run dev-setup` script. 28 | 29 | Note: PR's containing commits that _only_ updates package-lock.json files will not be accepted. 30 | 31 | * For installing or updating packages, use "npm i" or "npm install". 32 | * For reinstalling node_modules, run "npm ci". This ensures that the package-lock.json remains unchanged. 33 | 34 | ### How to Contribute 35 | 36 | 1. Create a branch for your feature (see [below](#feature-branch-example) for an example) 37 | 2. Add your code 38 | 3. Create a Pull Request 39 | 40 | ## Git Branches 41 | 42 | Below you can find an overview of the branches we are using. 43 | 44 | ``` 45 | - master (protected) 46 | - dev (protected) 47 | - documentation 48 | - feature 49 | -- feature/FEATURE-NAME 50 | --- feature/#_ 51 | --- feature/#_ 52 | --- feature/#_ 53 | - bugfix (fixing issues that are not urgent) 54 | - hotfix (fixing issues that need to be merged ASAP) 55 | ``` 56 | 57 | ### Feature Branch Example 58 | 59 | An example for a ToDo list feature could look as following: 60 | 61 | ``` 62 | -- feature/#1_todo-list 63 | ``` 64 | 65 | ## Commit Messages & Pull Requests 66 | 67 | For consistency and easier readability, we would like to ask everyone to use the templates for Pull Requests and Issues that we provide. 68 | They follow this pattern: 69 | 70 | ``` 71 | (A title to summarise what this is about) 72 | 73 | (A detailed description of your PR, feature idea or issue) 74 | 75 | Resolves #ISSUE_NUMBER 76 | ``` 77 | 78 | Note: It is important to add the #number for the issue the PR is resolving in order to close the issue accordingly once the PR gets merged. You can read more about this [here](https://docs.github.com/en/free-pro-team@latest/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue) 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [contributors-shield]: https://img.shields.io/github/contributors/chingu-voyages/moonshot-chingu-quiz.svg?style=for-the-badge 2 | [contributors-url]: https://github.com/chingu-voyages/moonshot-chingu-quiz/graphs/contributors 3 | [forks-shield]: https://img.shields.io/github/forks/chingu-voyages/moonshot-chingu-quiz.svg?style=for-the-badge 4 | [forks-url]: https://github.com/chingu-voyages/moonshot-chingu-quiz/network/members 5 | [stars-shield]: https://img.shields.io/github/stars/chingu-voyages/moonshot-chingu-quiz.svg?style=for-the-badge 6 | [stars-url]: https://github.com/chingu-voyages/moonshot-chingu-quiz/stargazers 7 | [issues-shield]: https://img.shields.io/github/issues/chingu-voyages/moonshot-chingu-quiz.svg?style=for-the-badge 8 | [issues-url]: https://github.com/chingu-voyages/moonshot-chingu-quiz/issues 9 | 10 | [![Contributors][contributors-shield]][contributors-url] 11 | [![Forks][forks-shield]][forks-url] 12 | [![Stargazers][stars-shield]][stars-url] 13 | [![Issues][issues-shield]][issues-url] 14 | 15 | # moonshot-chingu-quiz 16 | 17 | Chingu Moonshot - Quiz App 18 | 19 | This repository contains a web application for practising programming and interview questions. 20 | 21 | ## Table of Contents 22 | 23 | - [Instructions](#instructions) 24 | - [Designs](#designs) 25 | - [Code Dependencies](#code-dependencies) 26 | - [Contributing](#contributing) 27 | - [Code of Conduct](#code-of-conduct) 28 | - [License](#license) 29 | 30 | ## Instructions 31 | 32 | There are two ways to get up and running locally for development. 33 | 34 | #### Docker Workflow 35 | 36 | If you would like to work within the docker workflow, make sure to have Docker Desktop installed on Windows and Mac, or Docker and Docker-Compose on a linux distro. For more info on installing docker please visit the Docker [website](https://www.docker.com/products/docker-desktop). 37 | 38 | Once docker has been installed and set up: 39 | 40 | 1. Fork this repository 41 | 2. Clone your fork locally 42 | 3. Run `npm run local` from the root directory 43 | 4. Once you see `client_1 | ready - started server on http://localhost:3000` the app is ready to be opened in the browser (you will have to manually launch your browser and navigate to `http://localhost:3000`) 44 | 45 | > Note: It may take a little while to set up the docker containers the first time `docker-dev` is run. 46 | 47 | #### Without Docker 48 | 49 | If you wish to work without Docker you will need Node (we recommend >= 15.0.0) and Postgres (>= 13.0.0). 50 | 51 | 1. After forking and cloning this repository (steps 1 and 2 above), make sure Postgres is running 52 | 2. Create a database for local use and adjust the `.env.local` file to match your credentials 53 | 3. Run `npm install` from the root directory 54 | 4. Once install command is done run `npm run dev` 55 | 56 | ## Component Folder Structure 57 | 58 | ```text 59 | -pages (each file in here is a 'page' for our app) 60 | -index.ts (homepage for our app, links to '/' go here) 61 | -//other pages 62 | 63 | -components 64 | -quiz 65 | -Question.ts 66 | -Answer.ts 67 | -Timer.ts 68 | -//etc.. 69 | -shared 70 | -LinkButton.ts 71 | -//etc.. 72 | -//other folder 73 | -//other related components 74 | ``` 75 | 76 | ## Designs 77 | 78 | You can find the designs to this project here: [Figma](https://www.figma.com/file/2mKq8rdawiJO6EEVwugWYp/Chingu_Mockups?node-id=84%3A198) 79 | 80 | If you're a designer and would like to contribute, feel free to reach out to us! 81 | 82 | ## Code Dependencies 83 | 84 | The app is built with the following code dependencies: 85 | 86 | 1. [NextJS](https://github.com/vercel/next.js) 87 | 2. [Styled Components](https://github.com/styled-components/styled-components) 88 | 3. [PostgreSQL](https://github.com/postgres/postgres) 89 | 90 | ## Contributing 91 | 92 | This repository is open for contribution. For details on how to get started, check out our [Contributing Guide](/CONTRIBUTING.md). 93 | 94 | ## Code of Conduct 95 | 96 | Please check our [code of conduct](/CODE_OF_CONDUCT.md) before you start contributing. 97 | 98 | ## License 99 | 100 | This repository is licensed under the GNU General Public License v3.0. 101 | Please, read [this](/LICENSE.md) for additional information. 102 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://nextjs.org/docs/advanced-features/customizing-babel-config 3 | */ 4 | module.exports = { 5 | presets: [ 6 | "next/babel", 7 | ["@babel/preset-env", { targets: { node: "current" } }], 8 | "@babel/preset-typescript", 9 | ], 10 | plugins: [ 11 | [ 12 | "babel-plugin-styled-components", 13 | { 14 | minify: false, 15 | transpileTemplateLiterals: false, 16 | }, 17 | ], 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /components/aboutUs/Contributors.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import Image from 'next/image' 3 | import styled from 'styled-components'; 4 | 5 | import { 6 | SubInfo, 7 | Headline, 8 | MidGreenBar, 9 | HeadingGroup, 10 | } from "./styles"; 11 | 12 | const Wrapper = styled(SubInfo)` 13 | display: block; 14 | max-width: 990px; 15 | margin: 0 auto; 16 | `; 17 | 18 | const ContributionsWrapper = styled.div` 19 | display: flex; 20 | flex-wrap: wrap; 21 | `; 22 | 23 | const ContributorBubble = styled.a` 24 | display: flex; 25 | width: 60px; 26 | height: 60px; 27 | margin: 8px 9px; 28 | border-radius: 50%; 29 | object-fit: cover; 30 | overflow: hidden; 31 | `; 32 | 33 | const Contributors = () => { 34 | const [contributors, setContributors] = useState([]); 35 | 36 | useEffect(() => { 37 | fetch("https://api.github.com/repos/chingu-voyages/moonshot-chingu-quiz/contributors") 38 | .then(response => response.json()) 39 | .then(json => setContributors(json)); 40 | }, []); 41 | 42 | return contributors && contributors.length > 0 ? ( 43 | 44 | 45 | 46 | Contributors 47 | 48 | 49 | 50 | { 51 | contributors.map(contributor => ( 52 | 53 | {contributor.login} 54 | 55 | )) 56 | } 57 | 58 | 59 | ) : null; 60 | }; 61 | 62 | export default Contributors; 63 | -------------------------------------------------------------------------------- /components/aboutUs/HeroSection.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { breakpoint } from "../../frontend-config"; 4 | 5 | import { Heading1, TextBody } from "../shared/styles"; 6 | 7 | const Section = styled.div` 8 | text-align: center; 9 | background: ${props => props.theme.colors.backgroundPrimary}; 10 | padding: 35px 75px 38px; 11 | 12 | @media (min-width: ${breakpoint("lg")}) { 13 | padding: 40px 75px 62px; 14 | } 15 | `; 16 | 17 | const Title = styled(Heading1)` 18 | font-size: 31px; 19 | color: ${props => props.theme.colors.greenPrimary}; 20 | margin-bottom: 2px; 21 | 22 | @media (min-width: ${breakpoint("lg")}) { 23 | margin-bottom: 12px; 24 | font-size: 53px; 25 | } 26 | `; 27 | 28 | const Subtext = styled(TextBody)` 29 | color: ${props => props.theme.colors.textPrimary}; 30 | `; 31 | 32 | const HeroSection = () => { 33 | return ( 34 |
35 | About 36 | We believe in Freedom and Independence 37 |
38 | ); 39 | }; 40 | 41 | export default HeroSection; 42 | -------------------------------------------------------------------------------- /components/aboutUs/InfoSection.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { 4 | SubInfo, 5 | Logo, 6 | InfoBox, 7 | Headline, 8 | Text, 9 | MidGreenBar, 10 | HeadingGroup, 11 | } from "./styles"; 12 | 13 | import { ABOUT_DATA, HEADER_DATA } from "./Resources"; 14 | 15 | const InfoSection = () => { 16 | return ( 17 | <> 18 | 19 | 20 | 21 | 22 | 23 | {HEADER_DATA.title} 24 | 25 | {HEADER_DATA.text} 26 | 27 | 28 | 29 | {/* looping data from Resources.js */} 30 | {ABOUT_DATA.map(data => { 31 | const darkSection = data.id % 2 !== 0; 32 | return ( 33 | 34 | {data.svg} 35 | 36 | 37 | 38 | {data.title} 39 | 40 | {data.text} 41 | 42 | 43 | ); 44 | })} 45 | 46 | ); 47 | }; 48 | 49 | export default InfoSection; 50 | -------------------------------------------------------------------------------- /components/aboutUs/Resources.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import styled from "styled-components"; 4 | import { breakpoint } from "../../frontend-config"; 5 | 6 | // Styles 7 | const SVGGroup = styled.div` 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: center; 11 | align-items: center; 12 | 13 | @media (max-width: ${breakpoint("lg")}) { 14 | svg { 15 | width: 70%; 16 | height: 70%; 17 | } 18 | } 19 | 20 | @media (min-width: ${breakpoint("lg")}) { 21 | svg { 22 | width: 80%; 23 | height: 80%; 24 | } 25 | } 26 | `; 27 | 28 | const SVGSub = styled.div` 29 | display: flex; 30 | justify-content: center; 31 | align-items: center; 32 | gap: 10px; 33 | margin-top: 66.1px; 34 | 35 | span svg { 36 | width: 42px; 37 | } 38 | 39 | @media (max-width: ${breakpoint("xl")}) { 40 | display: none; 41 | } 42 | `; 43 | 44 | const IconWrapper = styled.span` 45 | fill: ${props => props.theme.colors.greenPrimary}; 46 | -webkit-order: 2; 47 | -ms-flex-order: 1; 48 | order: 2; 49 | `; 50 | 51 | const IconWrapperTwo = styled.span` 52 | fill: ${props => props.theme.colors.greenPrimary}; 53 | `; 54 | 55 | // Links 56 | const repoLink = ( 57 | 62 | repo 63 | 64 | ); 65 | 66 | export const repoGitLink = ( 67 | 72 | README 73 | 74 | ); 75 | 76 | const contributeLink = ( 77 | 78 | page on contributing 79 | 80 | ); 81 | 82 | const chinguLink = ( 83 | 84 | Chingu 85 | 86 | ); 87 | 88 | const chinguIOLink = ( 89 | 90 | Chingu.io 91 | 92 | ); 93 | 94 | const issueLink = ( 95 | 100 | here 101 | 102 | ); 103 | 104 | // SVGs 105 | export const MessageSVG = () => { 106 | return ( 107 | 108 | 114 | 119 | 120 | 121 | ); 122 | }; 123 | 124 | export const LockSVG = () => { 125 | return ( 126 | 127 | 133 | 134 | 135 | 136 | ); 137 | }; 138 | 139 | export const LinkSVG = () => { 140 | return ( 141 | 142 | 148 | 153 | 154 | 155 | ); 156 | }; 157 | 158 | export const ArrowSVG = () => { 159 | return ( 160 | 161 | 168 | 169 | 173 | 174 | 175 | ); 176 | }; 177 | 178 | export const ShareSVG = () => { 179 | return ( 180 | 181 | 182 | 188 | 189 | 190 | 191 |

Visit our GitHub

192 | 197 | 198 | 199 |
200 |
201 |
202 | ); 203 | }; 204 | 205 | export const CodeSVG = () => { 206 | return ( 207 | 208 | 214 | 219 | 220 | 221 | ); 222 | }; 223 | 224 | // Constant Data 225 | export const HEADER_DATA = { 226 | id: 0, 227 | title: "How did this project start?", 228 | text: ( 229 | <> 230 | The idea started with a group of ({chinguLink}) developers who saw a need 231 | in the community for a tool to help people prepare for technical 232 | interviews.
233 |
234 | This app aims to help fellow developers get a better sense of weak points 235 | in their knowledge base, provide resources to improve in those areas, and 236 | offer some ways to better prepare for technical interviews 237 | 238 | ), 239 | }; 240 | 241 | export const ABOUT_DATA = [ 242 | { 243 | id: 1, 244 | svg: , 245 | title: "What in the world is Chingu?", 246 | text: ( 247 | <> 248 | Chingu is a developer community that strives to help people level-up by 249 | building real projects in a team setting with others focused on 250 | improving their skill set. The benefits extend past tech and into 251 | getting a better grip on the soft-skills needed when working in a team 252 | environment; things like working through project ideas, resolving merge 253 | conflicts, and planning sprints. Learn more at ({chinguIOLink}) 254 | 255 | ), 256 | }, 257 | { 258 | id: 2, 259 | svg: , 260 | title: "Why Open Source?", 261 | text: ( 262 | <> 263 | In keeping with the theme of helping fellow developers, the project 264 | itself serves as a way to improve confidence and ability when 265 | contributing to open source. This project is open to contributors at all 266 | skill levels, and the maintainers / community are here to help when 267 | questions arise. 268 | 269 | ), 270 | }, 271 | { 272 | id: 3, 273 | svg: , 274 | title: "How can I contribute?", 275 | text: ( 276 | <> 277 | Ready to jump in and contribute? Check out our ({contributeLink}) or 278 | take a look at the ({repoLink})! 279 | 280 | ), 281 | }, 282 | { 283 | id: 4, 284 | svg: , 285 | title: `How should I report a bug or incorrect information?`, 286 | text: ( 287 | <> 288 | First of all, thanks for taking the time to report any issues you find 289 | with our app! We are using GitHub issues to track any bugs or incorrect 290 | information so they can be resolved by the community. 291 |
292 | Please be sure to check through the issues to make sure the one you are 293 | seeing hasn’t already been reported. If it has please feel free to add 294 | comments to the conversation inside the existing issue. 295 |
296 | If creating a new issue, please follow the issue template that is in 297 | place. 298 |
299 | Report issues / bugs / incorrect information ({issueLink}) 300 | 301 | ), 302 | }, 303 | { 304 | id: 5, 305 | svg: , 306 | title: `What are you using to build this app?`, 307 | text: ( 308 | <> 309 | It's a Next.js app powered by 310 | a PostgreSQL database. 311 | 312 | ), 313 | }, 314 | ]; 315 | -------------------------------------------------------------------------------- /components/aboutUs/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | import { breakpoint } from "../../frontend-config"; 3 | 4 | import { Heading2, TextBody } from "../shared/styles"; 5 | 6 | type styleProps = { 7 | grey?: boolean; 8 | light?: boolean; 9 | }; 10 | 11 | export const Wrapper = styled.div` 12 | background: ${props => props.theme.colors.textPrimary}; 13 | padding: 30px 30px 50px; 14 | width: 100%; 15 | 16 | @media (min-width: ${breakpoint("lg")}) { 17 | padding: 55px 30px; 18 | } 19 | `; 20 | 21 | export const SubInfo = styled.div` 22 | display: flex; 23 | flex-wrap: wrap; 24 | flex-direction: column; 25 | justify-content: center; 26 | align-items: center; 27 | overflow: hidden; 28 | padding: 55px 20px; 29 | 30 | a { 31 | color: ${props => props.theme.colors.link}; 32 | } 33 | 34 | svg { 35 | margin-bottom: 45px; 36 | } 37 | 38 | @media (min-width: ${breakpoint("xl")}) { 39 | flex-direction: row; 40 | gap: 60px; 41 | 42 | svg { 43 | margin-bottom: 0px; 44 | } 45 | } 46 | 47 | ${props => 48 | props.grey && 49 | css` 50 | background: ${props.theme.colors.backgroundPrimary}; 51 | flex-direction: column-reverse; 52 | 53 | svg { 54 | order: 2; 55 | margin-bottom: 45px; 56 | } 57 | 58 | @media (min-width: ${breakpoint("xl")}) { 59 | svg { 60 | margin-bottom: 0px; 61 | } 62 | } 63 | `} 64 | `; 65 | 66 | export const Logo = styled.img` 67 | width: 230px; 68 | height: 230px; 69 | align-self: center; 70 | margin-bottom: 54px; 71 | 72 | @media (min-width: ${breakpoint("xl")}) { 73 | width: 285px; 74 | height: 285px; 75 | margin-right: 40px; 76 | margin-bottom: 0px; 77 | } 78 | `; 79 | 80 | export const InfoBox = styled.div` 81 | text-align: center; 82 | margin-bottom: 20px; 83 | `; 84 | 85 | export const Headline = styled(Heading2)` 86 | margin-bottom: 60px; 87 | text-align: left; 88 | width: 623px; 89 | position: relative; 90 | display: inline-block; 91 | 92 | ${props => 93 | props.light && 94 | css` 95 | color: ${props.theme.colors.textPrimary}; 96 | `}; 97 | 98 | @media (max-width: ${breakpoint("md")}) { 99 | font-size: 24px; 100 | line-height: 28px; 101 | max-width: 325px; 102 | margin-bottom: 44px; 103 | } 104 | `; 105 | 106 | export const Text = styled(TextBody)` 107 | max-width: 620px; 108 | padding: 0 8px; 109 | text-align: left; 110 | 111 | ${props => 112 | props.light && 113 | css` 114 | color: ${props.theme.colors.textPrimary}; 115 | `} 116 | `; 117 | 118 | export const Icon = styled.div` 119 | width: 40px; 120 | height: 40px; 121 | margin: 0 auto 25px; 122 | `; 123 | 124 | export const HeadingGroup = styled.div` 125 | display: flex; 126 | `; 127 | 128 | export const MidGreenBar = styled.div` 129 | background-color: ${props => props.theme.colors.greenPrimary}; 130 | width: 14px; 131 | height: 41px; 132 | position: relative; 133 | display: inline-block; 134 | margin-right: 15px; 135 | margin-top: 2px; 136 | `; 137 | -------------------------------------------------------------------------------- /components/contributepage/InfoSection.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { breakpoint } from "../../frontend-config"; 4 | 5 | const Article = styled.article` 6 | display: grid; 7 | place-items: center center; 8 | line-height: 24px; 9 | 10 | a { 11 | color: ${props => props.theme.colors.link}; 12 | } 13 | `; 14 | 15 | const BigList = styled.ol` 16 | max-width: 1000px; 17 | background: ${props => props.theme.colors.light}; 18 | color: ${props => props.theme.colors.grey}; 19 | padding: 0 10px; 20 | margin: 0 0 64px 0; 21 | 22 | @media (min-width: ${breakpoint("md")}) { 23 | padding: 0; 24 | } 25 | `; 26 | 27 | const BigItem = styled.li` 28 | padding: 16px 0; 29 | list-style-type: none; 30 | list-style-position: inside; 31 | `; 32 | 33 | const ItemTitle = styled.h3` 34 | display: inline-block; 35 | font-size: 31px; 36 | font-weight: bold; 37 | margin: 16px 0; 38 | `; 39 | 40 | const ItemContent = styled.div` 41 | font-size: 18px; 42 | margin: 18px 32px; 43 | font-weight: normal; 44 | 45 | ol li { 46 | list-style-type: decimal; 47 | list-style-position: inside; 48 | } 49 | `; 50 | 51 | const CodeBlock = styled.code` 52 | white-space: pre-wrap; 53 | display: inline-block; 54 | background: ${props => props.theme.colors.lightGrey}; 55 | background: #ebeff3; /* props.theme.colors.lightGrey is currently #ccc - outside of design spec */ 56 | padding: 8px 24px; 57 | margin: 16px 0; 58 | `; 59 | 60 | const issuesLink = ( 61 | 66 | issue 67 | 68 | ); 69 | 70 | const repoLink = ( 71 | 76 | repository 77 | 78 | ); 79 | 80 | const linkingAPRToIssueLink = ( 81 | 86 | here 87 | 88 | ); 89 | 90 | const branchOverview = `- master (protected) 91 | - dev (protected) 92 | - documentation 93 | - feature 94 | -- feature/FEATURE-NAME 95 | --- feature/#_ 96 | --- feature/#_ 97 | --- feature/#_ 98 | - bugfix (fixing issues that are not urgent) 99 | - hotfix (fixing issues that need to be merged ASAP) 100 | `; 101 | 102 | const pRExamplePattern = ` (A title to summarise what this is about) 103 | 104 | (A detailed description of your PR, feature idea or issue) 105 | 106 | Resolves #ISSUE_NUMBER`; 107 | 108 | const InfoSection = () => { 109 | return ( 110 |
111 | 112 | 113 | 114 | {/* eslint-disable-next-line */} 115 | Choose an {issuesLink} 116 | 117 | 118 |

119 | To get started, please feel free to start working on any issues we 120 | have created and that are not assigned to anyone yet. If you would 121 | like to pick up an issue, please leave a comment for the other 122 | contributors to see that this issue is already be worked on. 123 |

124 |

125 | For starters, we recommend to pick up issues labelled good first 126 | issue. 127 |

128 |
129 |
130 | 131 | Get setup 132 | 133 |
    134 |
  1. 135 | {/* eslint-disable-next-line */} 136 | Fork the {repoLink} 137 |
  2. 138 |
  3. Clone your forked repository onto your local machine
  4. 139 |
  5. 140 | inside your CLI (Command Line Interface), move into your working 141 | directory 142 |
  6. 143 |
  7. 144 | run `npm ci` inside the root, client *and* server folder to 145 | install all dependencies needed for this project 146 |
  8. 147 |
  9. 148 | inside the client folder, run `npm run dev` to start the 149 | development server 150 |
  10. 151 |
152 |
153 |
154 | 155 | Create a branch for your feature 156 | 157 |

Below you can find an overview of the branches we are using.

158 | {branchOverview} 159 |

An example for a ToDo list feature could look as following:

160 | -- feature/#1_todo-list 161 |
162 |
163 | 164 | Add your code 165 | 166 | 167 | Create a Pull Request 168 | 169 |

170 | For consistency and easier readability, we would like to ask 171 | everyone to use the templates for Pull Requests and Issues that we 172 | provide. They follow this pattern: 173 |

174 | {pRExamplePattern} 175 |

176 | Note: It is important to add the #number for the issue the PR is 177 | resolving in order to close the issue accordingly once the PR gets 178 | {/* eslint-disable-next-line */} 179 | merged. You can read more about this {linkingAPRToIssueLink} 180 |

181 |
182 |
183 |
184 |
185 | ); 186 | }; 187 | 188 | export default InfoSection; 189 | -------------------------------------------------------------------------------- /components/contributepage/ResourcesSection.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled, { css } from "styled-components"; 3 | import { breakpoint } from "../../frontend-config"; 4 | 5 | const Wrapper = styled.div` 6 | background: ${props => props.theme.colors.backgroundPrimary}; 7 | padding: 45px 25px 65px; 8 | 9 | @media (min-width: ${breakpoint("lg")}) { 10 | padding: 115px 25px 190px; 11 | } 12 | `; 13 | 14 | const Title = styled.h2` 15 | font-size: 39px; 16 | font-weight: bold; 17 | color: ${props => props.theme.colors.textPrimary}; 18 | `; 19 | 20 | const ContentWrapper = styled.div` 21 | max-width: 1000px; 22 | margin: 0 auto; 23 | `; 24 | 25 | const ButtonsWrapper = styled.div` 26 | display: grid; 27 | grid-template-columns: 1fr; 28 | gap: 48px; 29 | margin: 32px 0; 30 | 31 | button { 32 | width: 100%; 33 | } 34 | 35 | @media (min-width: ${breakpoint("md")}) { 36 | grid-template-columns: 1fr 1fr; 37 | } 38 | 39 | @media (min-width: ${breakpoint("lg")}) { 40 | display: flex; 41 | flex-flow: row wrap; 42 | align-items: center; 43 | justify-content: flex-start; 44 | } 45 | `; 46 | 47 | const Button = styled.button<{ 48 | light?: boolean; 49 | dark?: boolean; 50 | }>` 51 | background: transparent; 52 | border: 1px solid ${props => props.theme.colors.greenPrimary}; 53 | border-radius: 5px; 54 | cursor: pointer; 55 | font-size: 16px; 56 | line-height: 24px; 57 | margin-right: 24px; 58 | padding: 10px 20px; 59 | 60 | &:last-of-type { 61 | margin-right: 0; 62 | } 63 | 64 | ${props => 65 | props.light && 66 | css` 67 | background: ${props.theme.colors.greenPrimary}; 68 | color: ${props.theme.colors.backgroundPrimary}; 69 | 70 | &:hover { 71 | background: ${props.theme.colors.backgroundPrimary}; 72 | color: ${props.theme.colors.greenPrimary}; 73 | } 74 | `} 75 | 76 | ${props => 77 | props.dark && 78 | css` 79 | background: ${props.theme.colors.backgroundPrimary}; 80 | color: ${props.theme.colors.greenPrimary}; 81 | 82 | &:hover { 83 | background: ${props.theme.colors.greenPrimary}; 84 | color: ${props.theme.colors.backgroundPrimary}; 85 | } 86 | `} 87 | `; 88 | 89 | const Info = styled.p` 90 | color: ${props => props.theme.colors.textPrimary}; 91 | `; 92 | 93 | const HeroSection = () => { 94 | return ( 95 | 96 | 97 | Resources 98 | 99 | 100 | 105 | 106 | 107 | 108 | 113 | 114 | 115 | 116 | 121 | 122 | 123 | 124 | 129 | 130 | 131 | 132 | 133 | 134 | If you have any questions please comment on the issue you’re working 135 | on. Or for something more general join in on our Chingu Discord chat - 136 | #chinguquiz-contributors 137 | 138 | 139 | 140 | ); 141 | }; 142 | 143 | export default HeroSection; 144 | -------------------------------------------------------------------------------- /components/frontpage/ChinguSection.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import styled, { css } from "styled-components"; 4 | import { breakpoint } from "../../frontend-config"; 5 | import { Heading2, TextBody } from "../shared/styles"; 6 | 7 | 8 | const Wrapper = styled.div` 9 | background: ${props => props.theme.colors.backgroundPrimary}; 10 | padding: 30px 30px 50px; 11 | width: 100%; 12 | 13 | @media (min-width: ${breakpoint("lg")}) { 14 | padding: 55px 30px; 15 | } 16 | `; 17 | 18 | const ContentWrapper = styled.div` 19 | max-width: 730px; 20 | margin: 0 auto; 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | gap: 20px; 25 | `; 26 | 27 | const ContentSection = styled.div` 28 | h2 { 29 | color: ${props => props.theme.colors.textPrimary}; 30 | margin-bottom: 32px; 31 | } 32 | 33 | p { 34 | color: ${props => props.theme.colors.textPrimary}; 35 | } 36 | 37 | @media (max-width: ${breakpoint("xl")}) { 38 | h2, 39 | p { 40 | text-align: center; 41 | } 42 | } 43 | 44 | @media (min-width: ${breakpoint("xl")}) { 45 | min-width: 540px; 46 | } 47 | `; 48 | 49 | const ImageSection = styled.div` 50 | position: relative; 51 | display: none; 52 | 53 | @media (min-width: ${breakpoint("xl")}) { 54 | min-width: 540px; 55 | display: block; 56 | } 57 | `; 58 | 59 | export const ChinguImage = styled.img` 60 | position: relative; 61 | right: 80px; 62 | 63 | @media (min-width: ${breakpoint("xl")}) { 64 | padding-left: 70px; 65 | } 66 | `; 67 | 68 | const ButtonsWrapper = styled.div` 69 | display: flex; 70 | align-items: center; 71 | margin-top: 56px; 72 | 73 | @media (max-width: ${breakpoint("xl")}) { 74 | justify-content: center; 75 | } 76 | `; 77 | 78 | const Button = styled.button<{ 79 | light?: boolean 80 | dark?: boolean 81 | }>` 82 | background: transparent; 83 | border: 1px solid ${props => props.theme.colors.greenPrimary}; 84 | border-radius: 5px; 85 | cursor: pointer; 86 | font-size: 16px; 87 | line-height: 24px; 88 | margin-right: 24px; 89 | padding: 10px 20px; 90 | 91 | ${props => 92 | props.light && 93 | css` 94 | background: ${props.theme.colors.greenPrimary}; 95 | color: ${props.theme.colors.backgroundPrimary}; 96 | 97 | &:hover { 98 | background: ${props.theme.colors.backgroundPrimary}; 99 | color: ${props.theme.colors.greenPrimary}; 100 | } 101 | `} 102 | 103 | ${props => 104 | props.dark && 105 | css` 106 | background: ${props.theme.colors.backgroundPrimary}; 107 | color: ${props.theme.colors.greenPrimary}; 108 | 109 | &:hover { 110 | background: ${props.theme.colors.greenPrimary}; 111 | color: ${props.theme.colors.backgroundPrimary}; 112 | } 113 | `} 114 | `; 115 | 116 | const ChinguSection = () => { 117 | return ( 118 | 119 | 120 | 121 | 122 | 123 | 124 | What is Chingu? 125 | 126 | We place motivated people with similar goals together in project teams which allows them level-up in ways they couldn't otherwise do. When you join Chingu, you will collaborate with others to build & launch real projects. We match learners from all skill levels, all timezones, and a variety of different tech stacks. 127 | 128 | 129 | 130 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | ); 141 | }; 142 | 143 | export default ChinguSection; 144 | -------------------------------------------------------------------------------- /components/frontpage/HeroSection.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import styled, { css } from "styled-components"; 4 | import { breakpoint } from "../../frontend-config"; 5 | 6 | const Wrapper = styled.div` 7 | background: ${props => props.theme.colors.backgroundPrimary}; 8 | padding: 45px 25px 65px; 9 | 10 | @media (min-width: ${breakpoint("lg")}) { 11 | padding: 115px 25px 190px; 12 | } 13 | `; 14 | 15 | const ContentWrapper = styled.div` 16 | max-width: 730px; 17 | margin: 0 auto; 18 | `; 19 | 20 | const Headline = styled.h1` 21 | font-size: 25px; 22 | line-height: 30px; 23 | font-weight: 700; 24 | letter-spacing: 0.5px; 25 | text-align: center; 26 | color: ${props => props.theme.colors.greenPrimary}; 27 | margin-bottom: 32px; 28 | 29 | @media (min-width: ${breakpoint("md")}) { 30 | font-size: 40px; 31 | line-height: 44px; 32 | } 33 | 34 | @media (min-width: ${breakpoint("lg")}) { 35 | font-size: 53px; 36 | line-height: 63px; 37 | } 38 | `; 39 | 40 | const Subtitle = styled.p` 41 | font-size: 16px; 42 | line-height: 24px; 43 | text-align: center; 44 | color: ${props => props.theme.colors.textPrimary}; 45 | padding: 0 40px; 46 | 47 | @media (min-width: ${breakpoint("lg")}) { 48 | font-size: 18px; 49 | line-height: 28px; 50 | padding: 0; 51 | } 52 | `; 53 | 54 | const ButtonsWrapper = styled.div` 55 | display: flex; 56 | align-items: center; 57 | justify-content: center; 58 | margin-top: 40px; 59 | `; 60 | 61 | const Button = styled.button<{ 62 | light?: boolean 63 | dark?: boolean 64 | }>` 65 | background: transparent; 66 | border: 1px solid ${props => props.theme.colors.greenPrimary}; 67 | border-radius: 5px; 68 | cursor: pointer; 69 | font-size: 16px; 70 | line-height: 24px; 71 | margin-right: 24px; 72 | padding: 10px 20px; 73 | 74 | &:last-of-type { 75 | margin-right: 0; 76 | } 77 | 78 | &:hover { 79 | background: ${props => props.theme.colors.greenPrimary}; 80 | color: ${props => props.theme.colors.backgroundPrimary}; 81 | } 82 | 83 | ${props => 84 | props.light && 85 | css` 86 | background: ${props.theme.colors.greenPrimary}; 87 | color: ${props.theme.colors.backgroundPrimary}; 88 | 89 | &:hover { 90 | background: ${props.theme.colors.backgroundPrimary}; 91 | color: ${props.theme.colors.greenPrimary}; 92 | } 93 | `} 94 | 95 | ${props => 96 | props.dark && 97 | css` 98 | background: ${props.theme.colors.backgroundPrimary}; 99 | color: ${props.theme.colors.greenPrimary}; 100 | 101 | &:hover { 102 | background: ${props.theme.colors.greenPrimary}; 103 | color: ${props.theme.colors.backgroundPrimary}; 104 | } 105 | `} 106 | `; 107 | 108 | const HeroSection = () => { 109 | return ( 110 | 111 | 112 | {``} 113 | 114 | 115 | This is your place to get started studying in a fun way using Quizzes to sharpen your knowledge && || put it into practise by simply contributing to this project! 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | ); 130 | }; 131 | 132 | export default HeroSection; 133 | -------------------------------------------------------------------------------- /components/frontpage/InfoSection.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { breakpoint } from "../../frontend-config"; 4 | import { PuzzleIcon, GrowSkillsIcon, OpenSourceIcon } from "../shared/icons"; 5 | 6 | const Wrapper = styled.div` 7 | background: ${props => props.theme.colors.light}; 8 | padding: 30px 20px 50px; 9 | 10 | @media (min-width: ${breakpoint("xl")}) { 11 | padding: 55px 20px; 12 | } 13 | `; 14 | 15 | const InnerWrapper = styled.div` 16 | display: flex; 17 | flex-direction: column; 18 | justify-content: center; 19 | 20 | @media (min-width: ${breakpoint("xl")}) { 21 | flex-direction: row; 22 | } 23 | `; 24 | 25 | const InfoBox = styled.div` 26 | text-align: center; 27 | margin-bottom: 20px; 28 | 29 | @media (min-width: ${breakpoint("xl")}) { 30 | margin-right: 30px; 31 | max-width: 350px; 32 | 33 | &:last-of-type { 34 | margin-right: 0; 35 | } 36 | } 37 | `; 38 | 39 | const IconWrapper = styled.div` 40 | width: 40px; 41 | height: 40px; 42 | margin: 0 auto 25px; 43 | fill: ${props => props.theme.colors.grey}; 44 | `; 45 | 46 | const IconWrapperStroke = styled.div` 47 | width: 40px; 48 | height: 40px; 49 | margin: 0 auto 25px; 50 | fill: none; 51 | stroke: ${props => props.theme.colors.grey}; 52 | `; 53 | 54 | const Headline = styled.h3` 55 | font-size: 25px; 56 | line-height: 29px; 57 | margin-bottom: 12px; 58 | `; 59 | 60 | const Text = styled.p` 61 | font-size: 16px; 62 | line-height: 24px; 63 | max-width: 600px; 64 | margin: 0 auto; 65 | `; 66 | 67 | const InfoSection = () => { 68 | return ( 69 | 70 | 71 | 72 | 73 | 74 | 75 | Have Fun 76 | 77 | We believe that studying should be fun and provide all the support needed in order to grow your skills. 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | Grow Your Skills 86 | 87 | Test your knowledge in multiple languages or topics, and get a 88 | detailed summary upon completion, including explanations and 89 | resource links. 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | Contribute to Open Source 98 | 99 | Chingu believes in growing together. As a result, this project is 100 | purely open source, and everyone can contribute at any given time. 101 | 102 | 103 | 104 | 105 | ); 106 | }; 107 | 108 | export default InfoSection; 109 | -------------------------------------------------------------------------------- /components/layout/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | 4 | import { FooterWrapper, ContentWrapper, HighlightLink } from "./styles"; 5 | 6 | const Footer = () => { 7 | return ( 8 | 9 | 10 |
11 | Powered By 12 | 13 | 17 | Chingu 18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 | Data Privacy 26 | 27 | 28 |
29 | 30 |
31 |
32 | ); 33 | }; 34 | 35 | export default Footer; 36 | -------------------------------------------------------------------------------- /components/layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | This component is used in `/pages/_app.js` as a wrapper so it will remain mounted even when the 'page' changes 3 | */ 4 | import React, { useState, useEffect, Fragment } from "react"; 5 | import Head from "next/head"; 6 | import Link from "next/link"; 7 | import styled from "styled-components"; 8 | import Footer from "./Footer"; 9 | import { signIn, signOut, useSession } from "next-auth/client"; 10 | import { breakpointsRaw } from "../../frontend-config"; 11 | 12 | import MobileMenu from "./MobileMenu"; 13 | 14 | import { 15 | Wrapper, 16 | InnerWrapper, 17 | TopBarInnerWrapper, 18 | LogoWrapper, 19 | Logo, 20 | LogoText, 21 | Navbar, 22 | NavbarLink, 23 | NavbarToggleSwitch, 24 | ToggleSwitchSlider, 25 | } from "./styles"; 26 | 27 | const Main = styled.main` 28 | padding-top: 88px; // fixed header height 29 | min-height: calc(100vh - 66px); // Push footer to bottom when needed 30 | `; 31 | 32 | interface LayoutProps { 33 | children: any; 34 | toggleTheme(): void; 35 | isDarkTheme: boolean; 36 | } 37 | 38 | const Layout = ({ children, toggleTheme, isDarkTheme }: LayoutProps) => { 39 | const [session, loading] = useSession(); 40 | 41 | const [mobile, setMobile] = useState(false); 42 | const [headerShadow, setHeaderShadow] = useState(false); 43 | const [mobileMenuActive, setMobileMenuActive] = useState(false); 44 | 45 | useEffect(() => { 46 | // mount 47 | checkPageSize(); 48 | window.addEventListener("scroll", onScroll); 49 | window.addEventListener("resize", checkPageSize); 50 | 51 | // unmount 52 | return () => { 53 | window.removeEventListener("scroll", onScroll); 54 | window.removeEventListener("resize", checkPageSize); 55 | }; 56 | }); 57 | 58 | const onScroll = () => { 59 | const distanceFromTop = window.scrollY; 60 | 61 | if (headerShadow && distanceFromTop === 0) { 62 | setHeaderShadow(false); 63 | } else if (!headerShadow && distanceFromTop > 0) { 64 | setHeaderShadow(true); 65 | } 66 | }; 67 | 68 | const checkPageSize = () => { 69 | if (window.innerWidth >= breakpointsRaw("md") && mobile) { 70 | setMobile(false); 71 | setHeaderShadow(window.scrollY > 0); 72 | setMobileMenuActive(false); 73 | } else if (window.innerWidth < breakpointsRaw("md") && !mobile) { 74 | setMobile(true); 75 | setHeaderShadow(window.scrollY > 0); 76 | setMobileMenuActive(false); 77 | } 78 | }; 79 | 80 | const toggleMobileMenu = () => { 81 | setMobileMenuActive(!mobileMenuActive); 82 | }; 83 | 84 | const signOutCompletely = async () => { 85 | await signOut(); 86 | const searchParams = new URLSearchParams(); 87 | searchParams.set("returnTo", `${window.location.origin}`); 88 | searchParams.set( 89 | "client_id", 90 | process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID as string 91 | ); 92 | window.location.href = `https://${ 93 | process.env.NEXT_PUBLIC_AUTH0_DOMAIN 94 | }/v2/logout?${searchParams.toString()}`; 95 | }; 96 | 97 | return ( 98 |
99 | 100 | Chingu Quiz App 101 | 102 | 103 | 104 |
105 | 106 |
107 | {session?.user?.email ? ( 108 | 109 | 110 | 111 | Profile 112 | 113 | 114 | 115 | 116 | 117 | ) : ( 118 | 119 | )} 120 |
121 | {session?.user?.email && ( 122 |
123 | Signed in as: {session.user.email} 124 |
125 | )} 126 |
127 | 128 | 129 | 130 | 131 | Chingu Quiz 132 | 133 | 134 | {mobile ? ( 135 | 141 | ) : ( 142 | 143 | 144 | Quiz 145 | 146 | 147 | Contribute 148 | 149 | 150 | About Us 151 | 152 | 153 | 154 | 155 | 156 | )} 157 | 158 |
159 |
160 | 161 |
{children}
162 |
163 |
164 | ); 165 | }; 166 | 167 | export default Layout; 168 | -------------------------------------------------------------------------------- /components/layout/MobileMenu.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Link from "next/link"; 4 | 5 | import { 6 | MobileMenuPageOverlay, 7 | MobileMenuButtonWrapper, 8 | MobileMenuButton, 9 | MobileMenuWrapper, 10 | MobileMenuLink, 11 | MobileToggleSwitch, 12 | ToggleSwitchSlider, 13 | } from "./styles"; 14 | 15 | interface MobileMenuProps { 16 | active: boolean; 17 | toggleMobileMenu(): void; 18 | toggleTheme(): void; 19 | isDarkTheme: boolean; 20 | } 21 | const MobileMenu = ({ 22 | active, 23 | toggleMobileMenu, 24 | toggleTheme, 25 | isDarkTheme, 26 | }: MobileMenuProps) => { 27 | return ( 28 | <> 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | Home 37 | 38 | 39 | 40 | Quiz 41 | 42 | 43 | 44 | Contribute 45 | 46 | 47 | 48 | About Us 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ); 57 | }; 58 | 59 | export default MobileMenu; 60 | -------------------------------------------------------------------------------- /components/layout/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { breakpoint } from "../../frontend-config"; 3 | 4 | export const Wrapper = styled.header<{ 5 | withShadow?: boolean; 6 | }>` 7 | height: 88px; 8 | width: 100%; 9 | background: ${props => props.theme.colors.backgroundMenu}; 10 | position: fixed; 11 | top: 0; 12 | left: 0; 13 | transition: box-shadow ease 0.3s; 14 | box-shadow: ${props => 15 | props.withShadow 16 | ? "0px 6px 6px 3px rgba(0, 0, 0, 0.25)" 17 | : "0px 6px 6px 3px rgba(0, 0, 0, 0)"}; 18 | z-index: 10; 19 | `; 20 | 21 | export const InnerWrapper = styled.div` 22 | display: flex; 23 | align-items: center; 24 | justify-content: space-between; 25 | width: 1440px; 26 | max-width: calc(100% - 70px); 27 | margin: 0 auto; 28 | `; 29 | 30 | export const TopBarInnerWrapper = styled(InnerWrapper)` 31 | color: ${props => props.theme.colors.textMenu}; 32 | justify-content: flex-start; 33 | padding: 4px; 34 | flex-direction: row-reverse; 35 | 36 | & > div { 37 | font-size: 0.8rem; 38 | margin-left: 8px; 39 | } 40 | ` 41 | 42 | // LOGO // 43 | export const LogoWrapper = styled.a` 44 | display: flex; 45 | align-items: center; 46 | text-decoration: none; 47 | `; 48 | 49 | export const Logo = styled.img` 50 | width: 47px; 51 | height: 47px; 52 | margin-right: 20px; 53 | 54 | @media (min-width: ${breakpoint("md")}) { 55 | width: 58px; 56 | height: 58px; 57 | margin-right: 18px; 58 | } 59 | `; 60 | 61 | export const LogoText = styled.div` 62 | font-family: "Roboto", sans-serif; 63 | font-size: 18px; 64 | line-height: 32px; 65 | font-weight: bold; 66 | letter-spacing: 0.2px; 67 | text-transform: uppercase; 68 | color: ${props => props.theme.colors.greenPrimary}; 69 | 70 | @media (min-width: ${breakpoint("md")}) { 71 | font-size: 24px; 72 | } 73 | `; 74 | 75 | // NAVBAR // 76 | export const Navbar = styled.nav` 77 | display: flex; 78 | align-items: center; 79 | `; 80 | 81 | export const NavbarLink = styled.a` 82 | font-size: 16px; 83 | line-height: 24px; 84 | color: ${props => props.theme.colors.textMenu}; 85 | margin-right: 32px; 86 | text-decoration: none; 87 | 88 | &:last-of-type { 89 | margin-right: 0; 90 | } 91 | `; 92 | 93 | export const NavbarToggleSwitch = styled.label` 94 | position: relative; 95 | display: inline-block; 96 | width: 46px; 97 | height: 26px; 98 | margin-left: 32px; 99 | margin-right: 4px; 100 | `; 101 | 102 | export const ToggleSwitchSlider = styled.span<{ 103 | isDarkTheme?: boolean; 104 | }>` 105 | position: absolute; 106 | cursor: pointer; 107 | top: 0; 108 | left: 0; 109 | right: 0; 110 | bottom: 0; 111 | background: ${props => props.theme.colors.backgroundPrimary}; 112 | border-radius: 26px; 113 | ${props => props.isDarkTheme && "filter: contrast(1.3)"}; 114 | 115 | &:before { 116 | position: absolute; 117 | content: ""; 118 | height: 20px; 119 | width: 20px; 120 | left: 3px; 121 | bottom: 3px; 122 | background: ${props => props.theme.colors.greenPrimary}; 123 | -webkit-transition: 0.4s; 124 | transition: 0.4s; 125 | border-radius: 50%; 126 | ${props => 127 | props.isDarkTheme 128 | ? `` 129 | : `-webkit-transform: translateX(20px); 130 | -ms-transform: translateX(20px); 131 | transform: translateX(20px)`}; 132 | } 133 | `; 134 | 135 | // MOBILE MENU // 136 | export const MobileMenuPageOverlay = styled.div<{ 137 | active?: boolean; 138 | }>` 139 | position: fixed; 140 | top: 0; 141 | left: 0; 142 | height: 100%; 143 | width: 100%; 144 | z-index: 500; 145 | ${props => !props.active && `pointer-events: none`}; 146 | transition: all ease 0.3s; 147 | `; 148 | 149 | export const MobileMenuButtonWrapper = styled.div` 150 | position: relative; 151 | display: flex; 152 | align-items: center; 153 | justify-content: center; 154 | width: 30px; 155 | height: 27px; 156 | `; 157 | 158 | export const MobileMenuButton = styled.div` 159 | width: 100%; 160 | height: 5px; 161 | background: ${props => props.theme.colors.textMenu}; 162 | border-radius: 2px; 163 | 164 | &:before, 165 | &:after { 166 | content: ""; 167 | width: inherit; 168 | height: inherit; 169 | border-radius: 2px; 170 | background: inherit; 171 | position: absolute; 172 | left: 0; 173 | } 174 | 175 | &:before { 176 | top: 0; 177 | } 178 | 179 | &:after { 180 | bottom: 0; 181 | } 182 | `; 183 | 184 | export const MobileMenuWrapper = styled.div<{ 185 | active?: boolean; 186 | }>` 187 | position: fixed; 188 | top: 88px; 189 | right: 0; 190 | padding: 18px 0; 191 | background: ${props => props.theme.colors.backgroundMenu}; 192 | width: calc(100% - 80px); 193 | max-width: 350px; 194 | height: 100%; 195 | z-index: 600; 196 | transform: ${props => (props.active ? "translateX(0)" : "translateX(100%)")}; 197 | transition: all ease 0.3s; 198 | box-shadow: 3px 3px 3px 3px rgba(0, 0, 0, 0.25); 199 | `; 200 | 201 | export const MobileMenuLink = styled(NavbarLink)` 202 | display: block; 203 | text-align: left; 204 | font-size: 18px; 205 | margin: 0; 206 | padding: 18px 0 18px 44px; 207 | `; 208 | 209 | export const MobileToggleSwitch = styled(NavbarToggleSwitch)` 210 | display: block; 211 | margin: 18px 0 18px 44px; 212 | `; 213 | 214 | // Footer // 215 | export const FooterWrapper = styled.footer` 216 | width: 100%; 217 | background-color: ${props => props.theme.colors.backgroundPrimary}; 218 | height: 66px; 219 | display: flex; 220 | align-items: center; 221 | 222 | a { 223 | text-decoration: none; 224 | } 225 | `; 226 | 227 | export const ContentWrapper = styled.div` 228 | width: ${breakpoint("maxWidth")}; 229 | max-width: calc(100% - 70px); 230 | margin: 0 auto; 231 | color: ${props => props.theme.colors.textPrimary}; 232 | justify-content: space-between; 233 | display: flex; 234 | `; 235 | 236 | export const HighlightLink = styled.span` 237 | color: ${props => props.theme.colors.greenPrimary}; 238 | margin-left: 5px; 239 | `; 240 | -------------------------------------------------------------------------------- /components/quizSelection/QuizTile.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import { Heading4, TextBodySmall, TextBodyMicro } from "../shared/styles"; 4 | import { TileContainer, TileTagContainer, QuizTileTag } from "./styles"; 5 | import type { ChinguQuiz } from "../../models"; 6 | 7 | interface QuizTileProps { 8 | quizData: ChinguQuiz.Quiz; 9 | animationDelay: number | string; 10 | } 11 | export default function QuizTile({ quizData, animationDelay }: QuizTileProps) { 12 | return ( 13 | 14 | 15 |
16 | {quizData.title} 17 | {quizData.description} 18 |
19 | 20 | {quizData.tag.map(tag => ( 21 | 22 | {tag} 23 | 24 | ))} 25 | 26 |
27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /components/quizSelection/TopicSelection.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import TopicSelectionChoice from "./TopicSelectionChoice"; 3 | import { TopicSelectionContainer, TopicSelectionList } from "./styles"; 4 | import type { ChinguQuiz, UI } from "../../models"; 5 | 6 | interface TopicSelectionProps { 7 | subjectsAndTopics: UI.Quizzes.SubjectAndTopic[]; 8 | chosenSubject: string; 9 | setChosenSubject(subject: string): void; 10 | chosenTopics: string[]; 11 | setChosenTopics: React.Dispatch>; 12 | } 13 | export default function TopicSelection({ 14 | subjectsAndTopics, 15 | chosenSubject, 16 | setChosenSubject, 17 | chosenTopics, 18 | setChosenTopics, 19 | }: TopicSelectionProps) { 20 | const toggleTopics = (topic: string) => { 21 | if (chosenTopics.includes(topic)) { 22 | setChosenTopics(topics => topics.filter(t => t !== topic)); 23 | } else { 24 | setChosenTopics(topics => [...topics, topic]); 25 | } 26 | }; 27 | 28 | const handlePrimaryButtonClick = (newSubject: string) => { 29 | setChosenSubject(newSubject); 30 | setChosenTopics([]); 31 | }; 32 | 33 | return ( 34 | 35 | 36 | 41 | {subjectsAndTopics.map(subject => ( 42 | 48 | ))} 49 | 50 | 51 | 52 | {chosenSubject !== "All" && 53 | subjectsAndTopics 54 | .filter(subj => subj.title === chosenSubject)[0] 55 | .tag.map(topic => ( 56 | 63 | ))} 64 | 65 | 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /components/quizSelection/TopicSelectionChoice.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { PrimaryButton, TextBodySmall } from "../shared/styles"; 3 | import { TopicSelectionItem } from "./styles"; 4 | 5 | interface TopicSelectionChoiceProps { 6 | buttonSize?: "small" | "default"; 7 | currentlySelected: string[] | string; 8 | thisSelection: string; 9 | handleSetThisSelection(value: string): void; 10 | } 11 | export default function TopicSelectionChoice({ 12 | buttonSize, 13 | currentlySelected, 14 | thisSelection, 15 | handleSetThisSelection, 16 | }: TopicSelectionChoiceProps) { 17 | return ( 18 | 19 | { 28 | handleSetThisSelection(thisSelection); 29 | }} 30 | > 31 | {thisSelection} 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /components/quizSelection/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { breakpoint } from "../../frontend-config"; 3 | import { PrimaryButton, riseUp } from "../shared/styles"; 4 | 5 | export const TileSection = styled.section` 6 | display: grid; 7 | grid-template-columns: repeat(auto-fill, minmax(375px, 1fr)); 8 | grid-gap: 20px; 9 | justify-content: space-evenly; 10 | justify-items: start; 11 | align-items: center; 12 | width: 100%; 13 | margin: 10px auto; 14 | 15 | @media (min-width: ${breakpoint("md")}) { 16 | grid-gap: 50px 30px; 17 | width: 95%; 18 | max-width: ${breakpoint("maxWidth")}; 19 | margin: 25px auto; 20 | } 21 | `; 22 | 23 | export const TileContainer = styled.div<{ 24 | animationDelay: number | string; 25 | }>` 26 | opacity: 0; 27 | display: flex; 28 | flex-flow: column nowrap; 29 | justify-content: space-between; 30 | align-items: start; 31 | width: 90%; 32 | height: 130px; 33 | padding: 8px 12px; 34 | margin: 10px auto; 35 | background: transparent; 36 | border-left: 10px solid ${props => props.theme.colors.darkGreen}; 37 | border-radius: 5px; 38 | box-shadow: 1px 1px 10px ${props => props.theme.colors.lightGrey}; 39 | transition-duration: 350ms; 40 | animation: ${riseUp} 400ms ease-in-out; 41 | animation-fill-mode: forwards; 42 | animation-delay: ${props => props.animationDelay}; 43 | 44 | &:hover { 45 | box-shadow: 10px 3px 18px ${props => props.theme.colors.lightGrey}; 46 | cursor: pointer; 47 | } 48 | 49 | @media (min-width: ${breakpoint("md")}) { 50 | width: 375px; 51 | margin: 0; 52 | } 53 | `; 54 | 55 | export const TileTagContainer = styled.span` 56 | display: flex; 57 | fled-flow: row nowrap; 58 | `; 59 | 60 | export const TopicSelectionContainer = styled.section` 61 | display: flex; 62 | flex-flow: column nowrap; 63 | justify-content: start; 64 | align-items: start; 65 | width: 100%; 66 | max-width: ${breakpoint("maxWidth")}; 67 | padding: 0; 68 | margin: 25px auto; 69 | 70 | @media (min-width: ${breakpoint("md")}) { 71 | width: 95%; 72 | } 73 | `; 74 | 75 | export const TopicSelectionList = styled.ul` 76 | display: flex; 77 | flex-flow: row nowrap; 78 | justify-content: start; 79 | align-items: center; 80 | max-width: 100%; 81 | min-height: 30px; 82 | margin: 5px 0; 83 | list-style: none; 84 | overflow-x: auto; 85 | 86 | @media (max-width: ${breakpoint("md")}) { 87 | padding-left: 15px; 88 | scrollbar-width: none; /* Hide horizontal scrollbar on mobile for Firefox */ 89 | 90 | &::-webkit-scrollbar { 91 | display: none; /* Hide horizontal scrollbar on mobile for Chrome, Safari, Opera */ 92 | } 93 | } 94 | `; 95 | 96 | export const TopicSelectionItem = styled.li` 97 | position: relative; 98 | width: max-content; 99 | padding: 0; 100 | margin-right: 15px; 101 | 102 | &:last-child:after { 103 | position: absolute; 104 | right: -15px; 105 | bottom: 5px; 106 | content: ""; 107 | display: block; 108 | width: 15px; 109 | height: 1px; 110 | } 111 | `; 112 | 113 | export const QuizTileTag = styled(PrimaryButton)` 114 | padding: 1px 5px; 115 | margin: 5px 5px 0 0; 116 | `; 117 | -------------------------------------------------------------------------------- /components/quizSingle/AnswerTileContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { AnswerTileContainerStyled } from "./styles"; 3 | import AnswerTileMark from "./AnswerTileMark"; 4 | import AnswerTileText from "./AnswerTileText"; 5 | import type { ChinguQuiz } from "../../models"; 6 | 7 | interface AnswerTileContainerProps { 8 | mark?: string; 9 | answerData?: ChinguQuiz.Answer; 10 | selected?: boolean; 11 | } 12 | export default function AnswerTileContainer({ 13 | mark, 14 | answerData, 15 | selected, 16 | }: AnswerTileContainerProps) { 17 | return mark && answerData && answerData.prompt ? ( 18 | 19 | 20 | 21 | 22 | ) : null; 23 | } 24 | -------------------------------------------------------------------------------- /components/quizSingle/AnswerTileMark.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { AnswerTileMarkStyled } from "./styles"; 3 | 4 | export interface AnswerTileTextProps { 5 | mark: string; 6 | } 7 | export default function AnswerTileText({ mark }: AnswerTileTextProps) { 8 | return {mark}; 9 | } 10 | -------------------------------------------------------------------------------- /components/quizSingle/AnswerTileText.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TextBodyBold } from "../shared/styles"; 3 | import { AnswerTileTextStyled } from "./styles"; 4 | 5 | interface AnswerTileTextProps { 6 | text: string; 7 | } 8 | 9 | export default function AnswerTileText({ text }: AnswerTileTextProps) { 10 | return ( 11 | 12 | {text} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /components/quizSingle/NextQuestionBtn.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Heading4 } from "../shared/styles"; 3 | import { NextQuestionBtnStyled, DisabledNextQuestionBtnStyled } from "./styles"; 4 | 5 | interface NextQuestionBtnProps { 6 | disabled?: Boolean; 7 | } 8 | 9 | export default function NextQuestionBtn({ disabled }: NextQuestionBtnProps) { 10 | return disabled ? ( 11 | 12 | NEXT 13 | 14 | ) : ( 15 | 16 | NEXT 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/quizSingle/QuestionHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Heading3, TextBodyBold } from "../shared/styles"; 3 | import { 4 | QuestionHeaderContainer, 5 | QuestionProgressBar, 6 | QuestionProgressBarFiller, 7 | QuestionProgressBarText, 8 | } from "./styles"; 9 | import type { ChinguQuiz } from "../../models"; 10 | 11 | interface QuestionHeaderProps { 12 | questionData: ChinguQuiz.Question; 13 | questionIndex: number; 14 | questionCount: number; 15 | animationDelay: number; 16 | } 17 | export default function QuestionHeader({ 18 | questionData, 19 | questionIndex, 20 | questionCount, 21 | animationDelay, 22 | }: QuestionHeaderProps) { 23 | return ( 24 | 25 | 26 | 27 | {`Question ${questionIndex} / ${questionCount}`} 28 | 29 | 30 | 31 | 34 | 35 |
36 | {questionData.prompt} 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /components/quizSingle/ResultView.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { QuizRecord } from "../../models/ChinguQuiz/QuizRecord"; 3 | import { Heading4, TextBodyBold, TextBodySmallBold } from "../shared/styles"; 4 | import { 5 | ResultPageContainer, 6 | ResultTitleContainer, 7 | ResultTileContainer, 8 | ResultTile, 9 | CodeBlock, 10 | TileNumber, 11 | SubmitQuizBtnStyled, 12 | } from "./styles"; 13 | import ScoreGraph from "./ScoreGraph"; 14 | import { useContext } from "react"; 15 | import { QuizContext } from "../../contexts/quiz-context"; 16 | 17 | export default function ResultView({ 18 | quizTitle, 19 | quizRecord, 20 | }: { 21 | quizTitle: string; 22 | quizRecord: QuizRecord[]; 23 | }) { 24 | const { timer } = useContext(QuizContext); 25 | const percentage = Math.round( 26 | (quizRecord.filter(r => r.correct).length * 100) / quizRecord.length 27 | ); 28 | const totalQs = quizRecord.length; 29 | 30 | return ( 31 | 32 |
33 | 34 |

35 | Total Time: {timer} seconds 36 |

37 |
38 | 39 |
40 | 41 | {quizTitle} 42 | 43 | 44 | 45 | {quizRecord.map((record, i) => ( 46 | 51 |
52 | 53 | Question:{" "} 54 | 55 | {i + 1}/{totalQs} 56 | 57 | 58 | {record.question} 59 | Correct Answer: 60 | {record.correctAnswer} 61 | Your Answer: 62 | {record.userAnswer} 63 |
64 |
65 | ))} 66 |
67 | 68 | 69 | 70 | 71 | {"Try Another >"} 72 | 73 | 74 | 75 |
76 |
77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /components/quizSingle/ScoreGraph.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import {breakpoint} from '../../frontend-config' 3 | 4 | import { GraphPathBG, GraphPath, GraphText } from "./styles"; 5 | 6 | export const ScoreGraphCore = ({ className, percentage }: { className?: string, percentage: number }) => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 20 | = 95 ? "#18e28c" : "url(#gradient)"} 23 | d="M18 2.0845 24 | a 15.9155 15.9155 0 0 1 0 31.831 25 | a 15.9155 15.9155 0 0 1 0 -31.831" 26 | /> 27 | 28 | {percentage}% 29 | 30 | 31 | ); 32 | } 33 | 34 | const ScoreGraph = styled(ScoreGraphCore)` 35 | display: block; 36 | margin: 10px auto; 37 | max-width: 80%; 38 | max-height: 250px; 39 | 40 | @media (min-width: ${breakpoint("lg")}) { 41 | transform: scale(1.1); 42 | position: sticky; 43 | top: 200px; 44 | width: 100%; 45 | margin-top: 78px; 46 | } 47 | `; 48 | 49 | export default ScoreGraph; -------------------------------------------------------------------------------- /components/quizSingle/SubmitQuizBtn.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Heading4 } from "../shared/styles"; 3 | import { SubmitQuizBtnStyled, DisabledSubmitQuizBtnStyled } from "./styles"; 4 | 5 | interface SubmitQuizBtnProps { 6 | disabled?: Boolean; 7 | } 8 | 9 | export default function SubmitQuizBtn({ disabled }: SubmitQuizBtnProps) { 10 | return disabled ? ( 11 | 12 | Submit 13 | 14 | ) : ( 15 | 16 | Submit 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/quizSingle/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from "styled-components"; 2 | import { breakpoint } from "../../frontend-config"; 3 | import { PrimaryButton, riseUp } from "../shared/styles"; 4 | 5 | // -- Animations -- // 6 | export const progress = keyframes` 7 | 0% { 8 | stroke-dasharray: 0 100; 9 | } 10 | `; 11 | 12 | export const QuestionHeaderContainer = styled.section<{ 13 | animationDelay: number; 14 | }>` 15 | display: flex; 16 | flex-flow: column; 17 | justify-content: center; 18 | align-items: center; 19 | text-align: center; 20 | width: 90%; 21 | margin: 10px auto 40px auto; 22 | transition-duration: 350ms; 23 | animation: ${riseUp} 400ms ease-in-out; 24 | animation-fill-mode: forwards; 25 | animation-delay: ${props => props.animationDelay}; 26 | 27 | @media (min-width: ${breakpoint("md")}) { 28 | max-width: 850px; 29 | margin: 40px auto; 30 | } 31 | `; 32 | 33 | export const QuestionProgressBar = styled.div` 34 | display: flex; 35 | flex-flow: column; 36 | align-items: center; 37 | width: 100%; 38 | justify-content: center; 39 | height: 16px; 40 | margin-bottom: 40px; 41 | border: 2px black solid; 42 | border-radius: 10px; 43 | 44 | @media (min-width: ${breakpoint("md")}) { 45 | width: 95%; 46 | max-width: ${breakpoint("maxWidth")}; 47 | margin-bottom: 40px; 48 | } 49 | `; 50 | 51 | export const QuestionProgressBarFiller = styled.div<{ 52 | completion: number; 53 | }>` 54 | display: flex; 55 | flex-flow: row nowrap; 56 | align-items: center; 57 | width: 100%; 58 | height: 10px; 59 | border-radius: 10px; 60 | background: rgb(24, 226, 140); 61 | background: linear-gradient( 62 | 90deg, 63 | rgba(24, 226, 140, 1) ${props => props.completion}%, 64 | rgba(255, 255, 255, 1) ${props => props.completion}% 65 | ); 66 | `; 67 | 68 | export const QuestionProgressBarText = styled.div` 69 | display: flex; 70 | flex-flow: row nowrap; 71 | justify-content: flex-end; 72 | width: 100%; 73 | margin-top: 40px; 74 | font-weight: bolder; 75 | 76 | @media (min-width: ${breakpoint("md")}) { 77 | width: 95%; 78 | max-width: ${breakpoint("maxWidth")}; 79 | } 80 | `; 81 | 82 | export const AnswersTileSection = styled.section` 83 | position: relative; 84 | display: grid; 85 | grid-template-columns: repeat(auto-fit, min(98%, 375px)); 86 | grid-gap: 20px; 87 | justify-content: center; 88 | align-items: center; 89 | width: 100%; 90 | margin: 10px auto; 91 | 92 | @media (min-width: ${breakpoint("md")}) { 93 | grid-template-columns: repeat(2, min(45%, 406px)); 94 | grid-gap: 50px 30px; 95 | justify-items: center; 96 | width: 95%; 97 | max-width: ${breakpoint("maxWidth")}; 98 | margin: 25px auto; 99 | } 100 | `; 101 | 102 | export const AnswerTileContainerLink = styled.a` 103 | width: 100%; 104 | `; 105 | 106 | export const AnswerTileContainerStyled = styled.div<{ 107 | animationDelay?: string; 108 | selected?: boolean; 109 | }>` 110 | position: relative; 111 | display: flex; 112 | flex-flow: column nowrap; 113 | justify-content: space-around; 114 | align-items: start; 115 | max-width: 375px; 116 | width: 100%; 117 | min-height: 90px; 118 | padding: 8px 12px; 119 | margin: 10px auto; 120 | border-radius: 5px; 121 | box-shadow: 1px 1px 10px ${props => props.theme.colors.lightGrey}; 122 | transition-duration: 350ms; 123 | animation: ${riseUp} 400ms ease-in-out; 124 | animation-fill-mode: forwards; 125 | animation-delay: ${props => props.animationDelay}; 126 | background-color: ${props => 127 | props.selected ? props.theme.colors.midGreen : "white"}; 128 | color: ${props => (props.selected ? "white" : "black")}; 129 | 130 | &:hover { 131 | box-shadow: 10px 3px 18px ${props => props.theme.colors.lightGrey}; 132 | cursor: pointer; 133 | } 134 | 135 | @media (min-width: ${breakpoint("md")}) { 136 | max-width: 406px; 137 | width: 100%; 138 | margin: 0; 139 | } 140 | `; 141 | export const AnswerTileMarkStyled = styled.div` 142 | position: absolute; 143 | display: flex; 144 | flex-flow: column nowrap; 145 | justify-content: center; 146 | align-items: center; 147 | top: 0; 148 | left: 0; 149 | width: 20%; 150 | height: 100%; 151 | background-color: ${props => props.theme.colors.darkGreen}; 152 | color: white; 153 | font-size: 39px; 154 | font-weight: bold; 155 | border-radius: 5px; 156 | `; 157 | 158 | export const AnswerTileTextStyled = styled.div` 159 | display: flex; 160 | flex-flow: column nowrap; 161 | justify-content: center; 162 | align-items: center; 163 | top: 0; 164 | margin-left: 20%; 165 | width: 80%; 166 | `; 167 | 168 | export const NextQuestionBtnContainer = styled.div` 169 | display: flex; 170 | flex-flow: column nowrap; 171 | justify-content: center; 172 | align-items: center; 173 | `; 174 | 175 | export const NextQuestionBtnStyled = styled(PrimaryButton)` 176 | width: 255px; 177 | margin: 40px auto; 178 | border: ${props => props.theme.colors.darkGreen} 5px solid; 179 | `; 180 | 181 | export const DisabledNextQuestionBtnStyled = styled(PrimaryButton)` 182 | width: 255px; 183 | margin: 40px auto; 184 | border: ${props => props.theme.colors.lightGrey} 5px solid; 185 | color: ${props => props.theme.colors.lightGrey}; 186 | cursor: not-allowed; 187 | `; 188 | 189 | export const SubmitQuizBtnContainer = styled.div` 190 | display: flex; 191 | flex-flow: column nowrap; 192 | justify-content: center; 193 | align-items: center; 194 | `; 195 | 196 | export const SubmitQuizBtnStyled = styled(PrimaryButton)` 197 | width: 255px; 198 | margin: 40px auto; 199 | border: ${props => props.theme.colors.darkGreen} 5px solid; 200 | `; 201 | 202 | export const DisabledSubmitQuizBtnStyled = styled(PrimaryButton)` 203 | width: 255px; 204 | margin: 40px auto; 205 | border: ${props => props.theme.colors.lightGrey} 5px solid; 206 | color: ${props => props.theme.colors.lightGrey}; 207 | cursor: not-allowed; 208 | `; 209 | 210 | export const ContentWrapper = styled.div` 211 | width: ${breakpoint("maxWidth")}; 212 | max-width: calc(100% - 70px); 213 | margin: 0 auto; 214 | color: ${props => props.theme.colors.light}; 215 | `; 216 | 217 | export const ResultTitleContainer = styled.div` 218 | display: flex; 219 | justify-content: center; 220 | width: 100%; 221 | margin-top: 25px; 222 | `; 223 | 224 | export const ResultPageContainer = styled.section` 225 | display: flex; 226 | flex-flow: column nowrap; 227 | justify-content: space-evenly; 228 | max-width: 1100px; 229 | margin: 0 auto; 230 | 231 | @media (min-width: ${breakpoint("lg")}) { 232 | flex-flow: row-reverse nowrap; 233 | } 234 | `; 235 | 236 | export const ResultTileContainer = styled.ul` 237 | display: flex; 238 | flex-flow: column nowrap; 239 | max-width: 700px; 240 | padding: 0; 241 | margin: 10px auto; 242 | `; 243 | 244 | export const ResultTile = styled.li<{ 245 | animationDelay: number | string; 246 | correct: boolean; 247 | }>` 248 | opacity: 0; 249 | display: flex; 250 | flex-flow: column nowrap; 251 | width: 90%; 252 | height: max-content; 253 | padding: 8px 12px; 254 | margin: 20px auto; 255 | border-left: 10px solid 256 | ${props => 257 | props.correct 258 | ? props.theme.colors.darkGreen 259 | : props.theme.colors.lightGrey}; 260 | border-radius: 5px; 261 | box-shadow: 1px 1px 10px ${props => props.theme.colors.lightGrey}; 262 | transition-duration: 400ms; 263 | animation: ${riseUp} 400ms ease-in-out; 264 | animation-fill-mode: forwards; 265 | animation-delay: ${props => props.animationDelay}; 266 | `; 267 | 268 | export const CodeBlock = styled.div` 269 | width: 100%; 270 | padding: 10px; 271 | margin: 5px 0 10px 0; 272 | background: ${props => props.theme.colors.lightGrey}; 273 | border-radius: 5px; 274 | font-family: monospace; 275 | `; 276 | 277 | export const TileNumber = styled.span` 278 | position: absolute; 279 | top: 8px; 280 | right: 15px; 281 | `; 282 | 283 | export const GraphPathBG = styled.path` 284 | fill: none; 285 | stroke: #eee; 286 | stroke-width: 3.8; 287 | `; 288 | 289 | export const GraphPath = styled.path` 290 | fill: none; 291 | stroke-width: 2.8; 292 | stroke-linecap: round; 293 | animation: ${progress} 2s ease-out forwards; 294 | `; 295 | 296 | export const GraphText = styled.text` 297 | fill: #666; 298 | font-family: sans-serif; 299 | font-size: 0.5em; 300 | text-anchor: middle; 301 | `; 302 | -------------------------------------------------------------------------------- /components/shared/DisplayMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FullWidthContainer, MessageContainer, Heading3 } from "./styles"; 3 | 4 | interface DisplayMessageProps { 5 | message: string; 6 | } 7 | export default function DisplayMessage({ message }: DisplayMessageProps) { 8 | return ( 9 | 10 | 11 | {message} 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /components/shared/PageHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { breakpoint } from "../../frontend-config"; 4 | import { Heading1 } from "./styles"; 5 | 6 | interface PageHeaderProps { 7 | children: React.ReactNode; 8 | } 9 | const HeaderWrapper = styled.section` 10 | display: flex; 11 | flex-flow: row nowrap; 12 | justify-content: center; 13 | align-items: center; 14 | width: 100%; 15 | padding: 25px 0; 16 | margin: 0 0 25px 0; 17 | background: ${props => props.theme.colors.backgroundPrimary}; 18 | 19 | @media (min-width: ${breakpoint("md")}) { 20 | padding: 40px 0; 21 | margin: 0 0 40px 0; 22 | } 23 | `; 24 | 25 | const PageH1 = styled(Heading1)` 26 | color: ${props => props.theme.colors.greenPrimary}; 27 | padding: 0 10px; 28 | margin: 20px auto; 29 | `; 30 | export default function PageHeader({ children }: PageHeaderProps) { 31 | return ( 32 | 33 | {children} 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /components/shared/icons.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const PuzzleIcon = () => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export const GrowSkillsIcon = () => ( 10 | 11 | 17 | 18 | ); 19 | 20 | export const OpenSourceIcon = () => ( 21 | 22 | 28 | 29 | ); 30 | -------------------------------------------------------------------------------- /components/shared/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { css, keyframes } from "styled-components"; 2 | import { breakpoint } from "../../frontend-config"; 3 | 4 | // -- TEXT ELEMENTS -- // 5 | 6 | // HEADINGS // 7 | export const HugeText = styled.h2` 8 | font-style: normal; 9 | font-weight: bold; 10 | font-size: 69px; 11 | line-height: 100%; 12 | `; 13 | 14 | export const Heading1 = styled.h1` 15 | font-style: normal; 16 | font-weight: bold; 17 | font-size: 35px; 18 | 19 | @media (min-width: ${breakpoint("md")}) { 20 | font-size: 53px; 21 | line-height: 120%; 22 | } 23 | `; 24 | 25 | export const Heading2 = styled.h2` 26 | font-style: normal; 27 | font-weight: bold; 28 | font-size: 39px; 29 | line-height: 120%; 30 | `; 31 | 32 | export const Heading3 = styled.h3` 33 | font-style: normal; 34 | font-weight: bold; 35 | font-size: 31px; 36 | line-height: 36px; 37 | `; 38 | 39 | export const Heading4 = styled.h4` 40 | font-style: normal; 41 | font-weight: bold; 42 | font-size: 24px; 43 | line-height: 28px; 44 | `; 45 | 46 | // BODY TEXT // 47 | export const TextBody = styled.p` 48 | font-style: normal; 49 | font-weight: normal; 50 | font-size: 18px; 51 | line-height: 28px; 52 | `; 53 | 54 | export const TextBodyBold = styled.p` 55 | font-style: normal; 56 | font-weight: bold; 57 | font-size: 18px; 58 | line-height: 28px; 59 | `; 60 | 61 | export const TextBodySmall = styled.p` 62 | font-style: normal; 63 | font-weight: normal; 64 | font-size: 16px; 65 | line-height: 150%; 66 | `; 67 | 68 | export const TextBodySmallBold = styled.p` 69 | font-style: normal; 70 | font-weight: bold; 71 | font-size: 16px; 72 | line-height: 150%; 73 | `; 74 | 75 | export const TextBodyMicro = styled.p` 76 | font-style: normal; 77 | font-weight: normal; 78 | font-size: 14px; 79 | line-height: 150%; 80 | `; 81 | 82 | export const TextBodyMicroBold = styled.p` 83 | font-style: normal; 84 | font-weight: bold; 85 | font-size: 14px; 86 | line-height: 150%; 87 | `; 88 | 89 | // -- Buttons -- // 90 | 91 | export const PrimaryButton = styled.button<{ 92 | mod?: "fillLight" | "fillDark" | "ghost" | false; 93 | size?: "small" | "default"; 94 | }>` 95 | width: max-content; 96 | padding: 8px 15px; 97 | margin: 0; 98 | background: transparent; 99 | border: 1px solid ${props => props.theme.colors.darkGreen}; 100 | border-radius: 5px; 101 | color: ${props => props.theme.colors.darkGreen}; 102 | cursor: pointer; 103 | transition-duration: 200ms; 104 | 105 | ${props => 106 | props.mod === "fillLight" && 107 | css` 108 | background: ${props.theme.colors.darkGreen}; 109 | color: ${props.theme.colors.light}; 110 | filter: none; 111 | `} 112 | 113 | ${props => 114 | props.mod === "fillDark" && 115 | css` 116 | background: ${props.theme.colors.darkGreen}; 117 | color: ${props.theme.colors.grey}; 118 | filter: none; 119 | `} 120 | 121 | ${props => 122 | props.mod === "ghost" && 123 | css` 124 | filter: contrast(50%); 125 | opacity: 0.7; 126 | `} 127 | 128 | ${props => 129 | props.size === "small" && 130 | css` 131 | padding: 2px 12px; 132 | `} 133 | `; 134 | 135 | // -- Loading and Error -- // 136 | export const FullWidthContainer = styled.section` 137 | display: flex; 138 | flex-flow: row nowrap; 139 | justify-content: center; 140 | align-items: center; 141 | width: 100%; 142 | background: ${props => props.theme.colors.light}; 143 | color: ${props => props.theme.colors.grey}; 144 | `; 145 | 146 | export const MessageContainer = styled.div` 147 | display: flex; 148 | flex-flow: row nowrap; 149 | justify-content: center; 150 | align-items: center; 151 | max-width: 100%; 152 | padding: 10px; 153 | `; 154 | 155 | // -- Animations -- // 156 | export const riseUp = keyframes` 157 | from { 158 | opacity: 0; 159 | transform: translate3d(0, 25px, 0); 160 | } 161 | 162 | to { 163 | opacity: 1; 164 | transform: translate3d(0, 0, 0); 165 | } 166 | `; 167 | -------------------------------------------------------------------------------- /contexts/quiz-context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, Dispatch, SetStateAction, useEffect, useState } from "react"; 2 | 3 | export const QuizContext = createContext<{ 4 | timer: number; 5 | setPaused: Dispatch>; 6 | 7 | }>({ 8 | timer: 0, 9 | setPaused: () => {}, 10 | }); 11 | 12 | export const QuizContextProvider: React.FC = ({ children }) => { 13 | const [timer, setTimer] = useState(0); 14 | const [paused, setPaused] = useState(false) 15 | 16 | useEffect(() => { 17 | const interval = setInterval(() => { 18 | if(!paused) setTimer(t => t+1); 19 | }, 1000) 20 | 21 | return () => { 22 | clearInterval(interval); 23 | } 24 | }, [setTimer, paused]) 25 | 26 | return ( 27 | 28 | {children} 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /db-setup.ts: -------------------------------------------------------------------------------- 1 | import Dotenv from "dotenv"; 2 | import { getConnection, pool } from "./db"; 3 | import { createUsersTable } from "./db/users"; 4 | import { createRolesTable, createUsersRolesTable } from "./db/roles"; 5 | 6 | Dotenv.config({ path: ".env.db-setup" }); 7 | 8 | async function main() { 9 | await createUsersTable(); 10 | await createRolesTable(); 11 | await createUsersRolesTable(); 12 | const client = await getConnection(); 13 | await client.release(); 14 | await pool.end(); 15 | } 16 | 17 | main(); 18 | -------------------------------------------------------------------------------- /db/config.ts: -------------------------------------------------------------------------------- 1 | import { PoolConfig } from "pg"; 2 | 3 | export default { 4 | username: process.env.PGUSER, 5 | host: process.env.PGHOST, 6 | database: process.env.PGDATABASE, 7 | password: process.env.PGPASSWORD, 8 | port: process.env.PGPORT, 9 | } as PoolConfig; 10 | -------------------------------------------------------------------------------- /db/index.ts: -------------------------------------------------------------------------------- 1 | import { Pool, PoolClient } from "pg"; 2 | import config from "./config"; 3 | 4 | export const pool = new Pool(config); 5 | 6 | export default { 7 | async query(text: string, params?: string[]) { 8 | const client = await pool.connect(); 9 | try { 10 | const start = Date.now(); 11 | const result = await client.query(text, params); 12 | const duration = Date.now() - start; 13 | // eslint-disable-next-line no-console 14 | console.log("executed query", { text, duration, rows: result.rowCount }); 15 | return result; 16 | } finally { 17 | client.release(); 18 | } 19 | }, 20 | }; 21 | 22 | let poolClient: PoolClient | null = null; 23 | 24 | export const getConnection = async () => { 25 | if(!poolClient) { 26 | poolClient = await pool.connect() 27 | } 28 | return poolClient; 29 | } -------------------------------------------------------------------------------- /db/roles.ts: -------------------------------------------------------------------------------- 1 | import { getConnection } from "./index"; 2 | import { IRole } from "../models/User/role"; 3 | 4 | export async function createRolesTable() { 5 | const client = await getConnection(); 6 | 7 | const { 8 | rows: [{ exists: rolesExists }], 9 | } = await client.query(` 10 | SELECT EXISTS( SELECT 1 FROM pg_tables WHERE schemaname='public' and tablename='roles'); 11 | `); 12 | 13 | if (!rolesExists) { 14 | await client.query( 15 | ` 16 | CREATE TABLE roles ( 17 | "roleId" uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 18 | "roleName" varchar (64) UNIQUE NOT NULL 19 | ) 20 | ` 21 | ); 22 | return true; 23 | } 24 | return false; 25 | } 26 | 27 | export async function createUsersRolesTable() { 28 | const client = await getConnection(); 29 | 30 | const { 31 | rows: [{ exists: usersRolesExists }], 32 | } = await client.query(` 33 | SELECT EXISTS( SELECT 1 FROM pg_tables WHERE schemaname='public' and tablename='users_roles'); 34 | `); 35 | 36 | if (usersRolesExists) return false; 37 | 38 | await client.query( 39 | ` 40 | CREATE TABLE users_roles ( 41 | "userId" int NOT NULL, 42 | "roleId" uuid NOT NULL, 43 | PRIMARY KEY ("userId", "roleId") 44 | ) 45 | ` 46 | ); 47 | return true; 48 | } 49 | 50 | export async function insertNewRole(name: string) { 51 | const client = await getConnection(); 52 | 53 | const matchRows = await client.query( 54 | `SELECT FROM ONLY roles WHERE "roleName" = $1`, 55 | [name] 56 | ); 57 | 58 | if (matchRows.rowCount !== 0) return null; 59 | 60 | return client.query( 61 | ` 62 | INSERT INTO roles ("roleId", "roleName") 63 | VALUES (DEFAULT, $1) 64 | `, 65 | [name] 66 | ); 67 | } 68 | 69 | export async function getRoles() { 70 | const client = await getConnection(); 71 | 72 | const { rows } = await client.query( 73 | `SELECT * 74 | FROM roles` 75 | ); 76 | 77 | return rows; 78 | } 79 | -------------------------------------------------------------------------------- /db/users.ts: -------------------------------------------------------------------------------- 1 | import { QuizResult, UserData } from "../models/User/user"; 2 | import { getConnection } from "./index"; 3 | 4 | export const checkEmailExists = async (email: string) => { 5 | const client = await getConnection(); 6 | 7 | const { rows: userRows } = await client.query( 8 | "SELECT email FROM users WHERE email=$1", 9 | [email] 10 | ); 11 | 12 | if (userRows.length > 1) 13 | throw new Error(`More than one matching email for ${email}`); 14 | return userRows.length === 1; 15 | }; 16 | 17 | export const insertUser = async (nickname: string, email: string) => { 18 | const client = await getConnection(); 19 | 20 | const emailExists = await checkEmailExists(email); 21 | 22 | if (emailExists) return null; 23 | 24 | return client.query("INSERT INTO users(nickname, email) VALUES ($1, $2)", [ 25 | nickname, 26 | email, 27 | ]); 28 | }; 29 | 30 | export const getUserData = async (email: string) => { 31 | const client = await getConnection(); 32 | 33 | const { 34 | rows: [{ data }], 35 | } = await client.query("SELECT data FROM users WHERE email=$1", [email]); 36 | 37 | return data as UserData; 38 | }; 39 | 40 | export const addQuizResult = async (email: string, quizResult: QuizResult) => { 41 | const emailExists = await checkEmailExists(email); 42 | if (!emailExists) throw new Error("User does not exist"); 43 | 44 | const client = await getConnection(); 45 | const { 46 | rows: [{ data }], 47 | } = await client.query("SELECT data FROM users WHERE email=$1", [email]); 48 | 49 | const updatedData: UserData = data ? data : {}; 50 | if (!updatedData.quizResults) updatedData.quizResults = []; 51 | const updatedQuizResults = [...updatedData.quizResults, quizResult]; 52 | 53 | updatedData.quizResults = updatedQuizResults; 54 | 55 | return client.query("UPDATE users SET data=$2 WHERE email=$1", [ 56 | email, 57 | updatedData, 58 | ]); 59 | }; 60 | 61 | export async function createUsersTable() { 62 | const client = await getConnection(); 63 | 64 | const { 65 | rows: [{ exists: usersExists }], 66 | } = await client.query(` 67 | SELECT EXISTS( SELECT 1 FROM pg_tables WHERE schemaname='public' and tablename='users'); 68 | `); 69 | 70 | if (usersExists) return false; 71 | 72 | return client.query( 73 | ` 74 | CREATE TABLE users ( 75 | uid serial PRIMARY KEY, 76 | nickname varchar (64) UNIQUE NOT NULL, 77 | email varchar (320) UNIQUE NOT NULL, 78 | data json 79 | ) 80 | ` 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | # client: 5 | # image: node:14 6 | # restart: always 7 | # user: node 8 | # working_dir: /home/node/app 9 | # volumes: 10 | # - ./:/home/node/app 11 | # ports: 12 | # - "3000:3000" 13 | # command: bash -c "npm i && npm run dev" 14 | 15 | db: 16 | image: postgres 17 | container_name: moonshot-chingu-quiz_db 18 | restart: always 19 | environment: 20 | POSTGRES_PASSWORD: docker 21 | POSTGRES_USER: docker 22 | POSTGRES_DB: docker 23 | volumes: 24 | - "quizdata:/var/lib/postgresql/data" 25 | - "./quiz_db.sql:/docker-entrypoint-initdb.d/quiz_db.sql" 26 | ports: 27 | - "5432:5432" 28 | 29 | test_db: 30 | image: postgres 31 | container_name: moonshot-chingu-quiz_test_db 32 | restart: always 33 | environment: 34 | POSTGRES_PASSWORD: docker 35 | POSTGRES_USER: docker 36 | POSTGRES_DB: docker 37 | volumes: 38 | - "moonshot_test_db:/var/lib/postgresql/data" 39 | - "./quiz_db.sql:/docker-entrypoint-initdb.d/quiz_db.sql" 40 | ports: 41 | - "15432:5432" 42 | 43 | volumes: 44 | quizdata: 45 | moonshot_test_db: 46 | -------------------------------------------------------------------------------- /frontend-config.ts: -------------------------------------------------------------------------------- 1 | const sizesRaw = { 2 | maxWidth: 1440, 3 | xs: 375, 4 | md: 600, 5 | lg: 900, 6 | xl: 1200, 7 | }; 8 | 9 | const sizes = { 10 | maxWidth: `${sizesRaw.maxWidth}px`, 11 | xs: `${sizesRaw.xs}px`, 12 | md: `${sizesRaw.md}px`, 13 | lg: `${sizesRaw.lg}px`, 14 | xl: `${sizesRaw.xl}px`, 15 | }; 16 | const breakpoint = (size: keyof typeof sizes) => { 17 | return sizes[size]; 18 | }; 19 | 20 | const breakpointsRaw = (size: keyof typeof sizesRaw) => { 21 | return sizesRaw[size]; 22 | }; 23 | 24 | export { breakpoint, breakpointsRaw }; 25 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | module.exports = { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/tmp/jest_rs", 15 | 16 | // Automatically clear mock calls and instances between every test 17 | // clearMocks: false, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | // collectCoverage: false, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | // coverageDirectory: undefined, 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "/node_modules/" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | coverageProvider: "v8", 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: undefined, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: undefined, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | // globals: {}, 64 | 65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 66 | // maxWorkers: "50%", 67 | 68 | // An array of directory names to be searched recursively up from the requiring module's location 69 | // moduleDirectories: [ 70 | // "node_modules" 71 | // ], 72 | 73 | // An array of file extensions your modules use 74 | // moduleFileExtensions: [ 75 | // "js", 76 | // "jsx", 77 | // "ts", 78 | // "tsx", 79 | // "json", 80 | // "node" 81 | // ], 82 | 83 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 84 | // moduleNameMapper: {}, 85 | 86 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 87 | // modulePathIgnorePatterns: [], 88 | 89 | // Activates notifications for test results 90 | // notify: false, 91 | 92 | // An enum that specifies notification mode. Requires { notify: true } 93 | // notifyMode: "failure-change", 94 | 95 | // A preset that is used as a base for Jest's configuration 96 | // preset: undefined, 97 | 98 | // Run tests from one or more projects 99 | // projects: undefined, 100 | 101 | // Use this configuration option to add custom reporters to Jest 102 | // reporters: undefined, 103 | 104 | // Automatically reset mock state between every test 105 | // resetMocks: false, 106 | 107 | // Reset the module registry before running each individual test 108 | // resetModules: false, 109 | 110 | // A path to a custom resolver 111 | // resolver: undefined, 112 | 113 | // Automatically restore mock state between every test 114 | // restoreMocks: false, 115 | 116 | // The root directory that Jest should scan for tests and modules within 117 | // rootDir: undefined, 118 | 119 | // A list of paths to directories that Jest should use to search for files in 120 | // roots: [ 121 | // "" 122 | // ], 123 | 124 | // Allows you to use a custom runner instead of Jest's default test runner 125 | // runner: "jest-runner", 126 | 127 | // The paths to modules that run some code to configure or set up the testing environment before each test 128 | // setupFiles: [], 129 | 130 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 131 | // setupFilesAfterEnv: [], 132 | 133 | // The number of seconds after which a test is considered as slow and reported as such in the results. 134 | // slowTestThreshold: 5, 135 | 136 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 137 | // snapshotSerializers: [], 138 | 139 | // The test environment that will be used for testing 140 | // testEnvironment: "jest-environment-node", 141 | 142 | // Options that will be passed to the testEnvironment 143 | // testEnvironmentOptions: {}, 144 | 145 | // Adds a location field to test results 146 | // testLocationInResults: false, 147 | 148 | // The glob patterns Jest uses to detect test files 149 | // testMatch: [ 150 | // "**/__tests__/**/*.[jt]s?(x)", 151 | // "**/?(*.)+(spec|test).[tj]s?(x)" 152 | // ], 153 | 154 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 155 | // testPathIgnorePatterns: [ 156 | // "/node_modules/" 157 | // ], 158 | 159 | // The regexp pattern or array of patterns that Jest uses to detect test files 160 | // testRegex: [], 161 | 162 | // This option allows the use of a custom results processor 163 | // testResultsProcessor: undefined, 164 | 165 | // This option allows use of a custom test runner 166 | // testRunner: "jest-circus/runner", 167 | 168 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 169 | // testURL: "http://localhost", 170 | 171 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 172 | // timers: "real", 173 | 174 | // A map from regular expressions to paths to transformers 175 | // transform: undefined, 176 | 177 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 178 | // transformIgnorePatterns: [ 179 | // "/node_modules/", 180 | // "\\.pnp\\.[^\\/]+$" 181 | // ], 182 | 183 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 184 | // unmockedModulePathPatterns: undefined, 185 | 186 | // Indicates whether each individual test should be reported during the run 187 | // verbose: undefined, 188 | 189 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 190 | // watchPathIgnorePatterns: [], 191 | 192 | // Whether to use watchman for file crawling 193 | // watchman: true, 194 | }; 195 | -------------------------------------------------------------------------------- /models/ChinguQuiz/Answer.ts: -------------------------------------------------------------------------------- 1 | export interface Answer { 2 | prompt: string; 3 | id: string; 4 | question: string; 5 | is_correct: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /models/ChinguQuiz/Question.ts: -------------------------------------------------------------------------------- 1 | import type { Answer } from "./Answer"; 2 | 3 | export interface Question { 4 | id: string; 5 | prompt: string; 6 | answers: Answer[]; 7 | } 8 | -------------------------------------------------------------------------------- /models/ChinguQuiz/Quiz.ts: -------------------------------------------------------------------------------- 1 | export interface Quiz { 2 | id: string; 3 | subject: string; 4 | description: string; 5 | tag: string[]; 6 | title: string; 7 | } 8 | -------------------------------------------------------------------------------- /models/ChinguQuiz/QuizRecord.ts: -------------------------------------------------------------------------------- 1 | export interface QuizRecord { 2 | correctAnswer: string; 3 | userAnswer: string; 4 | question: string; 5 | correct: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /models/UI/Quizzes.ts: -------------------------------------------------------------------------------- 1 | export interface SubjectAndTopic { 2 | key: string; 3 | title: string; 4 | tag: string[]; 5 | } 6 | -------------------------------------------------------------------------------- /models/User/role.ts: -------------------------------------------------------------------------------- 1 | export interface IRole { 2 | roleId: string; 3 | roleName: string; 4 | } 5 | -------------------------------------------------------------------------------- /models/User/user.ts: -------------------------------------------------------------------------------- 1 | export interface QuizResult { 2 | date: string; 3 | numberCorrect: number; 4 | totalQuestions: number; 5 | name: string; 6 | secondsToComplete: number; 7 | } 8 | 9 | export interface UserData { 10 | quizResults?: QuizResult[] 11 | } 12 | 13 | export interface User { 14 | uid: number; 15 | nickname: string; 16 | email: string; 17 | data?: UserData; 18 | } -------------------------------------------------------------------------------- /models/index.ts: -------------------------------------------------------------------------------- 1 | import type { Quiz } from "./ChinguQuiz/Quiz"; 2 | import type { Question } from "./ChinguQuiz/Question"; 3 | import type { Answer } from "./ChinguQuiz/Answer"; 4 | 5 | import type { SubjectAndTopic } from './UI/Quizzes' 6 | 7 | export declare namespace ChinguQuiz { 8 | export { Quiz, Question, Answer }; 9 | } 10 | export declare namespace UI { 11 | export namespace Quizzes { 12 | export { SubjectAndTopic } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | images: { 3 | domains: ['avatars.githubusercontent.com'], 4 | }, 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moonshot-chingu-quiz", 3 | "version": "1.0.0", 4 | "description": "Chingu Moonshot - Quiz App", 5 | "private": true, 6 | "engines": { 7 | "npm": ">= 7.0.0" 8 | }, 9 | "dependencies": { 10 | "@babel/node": "^7.14.7", 11 | "@types/jest": "^26.0.24", 12 | "concurrently": "^7.2.2", 13 | "dotenv": "^10.0.0", 14 | "next": "^10.0.7", 15 | "next-auth": "^3.29.10", 16 | "pg": "^8.6.0", 17 | "react": "^17.0.2", 18 | "react-dom": "^17.0.2", 19 | "styled-components": "^5.3.0" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "^7.14.6", 23 | "@babel/preset-env": "^7.14.7", 24 | "@babel/preset-typescript": "^7.14.5", 25 | "@types/node": "^16.0.0", 26 | "@types/pg": "^8.6.0", 27 | "@types/react": "^17.0.13", 28 | "@types/styled-components": "^5.1.11", 29 | "babel-jest": "^27.0.6", 30 | "babel-plugin-styled-components": "^1.13.1", 31 | "eslint": "^7.30.0", 32 | "eslint-config-airbnb": "^18.2.0", 33 | "eslint-config-next": "^11.0.1", 34 | "eslint-config-prettier": "^8.3.0", 35 | "eslint-plugin-import": "^2.23.4", 36 | "eslint-plugin-jsx-a11y": "^6.3.1", 37 | "eslint-plugin-react": "^7.24.0", 38 | "eslint-plugin-react-hooks": "^4.0.0", 39 | "husky": "^7.0.1", 40 | "jest": "^27.0.6", 41 | "lint-staged": "^11.0.0", 42 | "prettier": "^2.3.2", 43 | "typescript": "^4.3.5" 44 | }, 45 | "scripts": { 46 | "local": "concurrently \"npm:docker-dev\" \"npm:dev\"", 47 | "docker-dev": "docker-compose up", 48 | "docker-reset-db": "docker container rm moonshot-chingu-quiz_db && docker volume rm moonshot-chingu-quiz_quizdata", 49 | "docker-dump-db": "docker exec moonshot-chingu-quiz_db pg_dump -U docker docker > quiz_db.sql", 50 | "dev": "next dev", 51 | "build": "next build", 52 | "start": "next start", 53 | "lint:ts": "tsc --noEmit --skipLibCheck -p ./tsconfig.json", 54 | "db-setup": "babel-node -r dotenv/config -x .ts -- db-setup.ts", 55 | "test": "jest" 56 | }, 57 | "husky": { 58 | "hooks": { 59 | "pre-commit": "tsc --noEmit --skipLibCheck -p ./tsconfig.json && lint-staged" 60 | } 61 | }, 62 | "lint-staged": { 63 | "**/*.{js,jsx}": [ 64 | "prettier --write", 65 | "eslint **/*.js --fix-dry-run" 66 | ] 67 | }, 68 | "repository": { 69 | "type": "git", 70 | "url": "git+https://github.com/chingu-voyages/moonshot-chingu-quiz.git" 71 | }, 72 | "keywords": [], 73 | "author": "", 74 | "license": "ISC", 75 | "bugs": { 76 | "url": "https://github.com/chingu-voyages/moonshot-chingu-quiz/issues" 77 | }, 78 | "homepage": "https://github.com/chingu-voyages/moonshot-chingu-quiz#readme" 79 | } 80 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | This file allows us to add things like a header that will appear on every page. 3 | 'Component' below = the page currently loaded (as in /quizzes). 4 | */ 5 | 6 | import React, { useState } from "react"; 7 | import { ThemeProvider } from "styled-components"; 8 | import Layout from "../components/layout/Layout"; 9 | import "../styles/reset.css"; 10 | import "../styles/globals.css"; 11 | import { AppProps } from "next/app"; 12 | 13 | const darkTheme = { 14 | colors: { 15 | light: "#fff", 16 | dark: "#000", 17 | grey: "#333", 18 | lightGrey: "#ccc", 19 | midGreen: "#18e28c", 20 | darkGreen: "#057a55", 21 | link: "inherit", 22 | backgroundPrimary: "#333", 23 | backgroundMenu: "#333", 24 | textPrimary: "#fff", 25 | textMenu: "#fff", 26 | greenPrimary: "#18e28c", 27 | }, 28 | }; 29 | 30 | const lightTheme = { 31 | colors: { 32 | light: "#fff", 33 | dark: "#000", 34 | grey: "#333", 35 | lightGrey: "#ccc", 36 | midGreen: "#18e28c", 37 | darkGreen: "#057a55", 38 | link: "inherit", 39 | backgroundPrimary: "#EBEFF3", 40 | backgroundMenu: "#fff", 41 | textPrimary: "#333", 42 | textMenu: "#057a55", 43 | greenPrimary: "#057a55", 44 | }, 45 | }; 46 | 47 | function MyApp({ Component, pageProps }: AppProps) { 48 | const [isDarkTheme, setIsDarkTheme] = useState(true); 49 | const toggleTheme = () => setIsDarkTheme(!isDarkTheme); 50 | 51 | return ( 52 | 53 | 54 | 55 | 56 | 57 | ); 58 | } 59 | 60 | export default MyApp; 61 | -------------------------------------------------------------------------------- /pages/about.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Hero from "../components/aboutUs/HeroSection"; 3 | import Info from "../components/aboutUs/InfoSection"; 4 | import Contributors from "../components/aboutUs/Contributors"; 5 | 6 | export default function Home() { 7 | return ( 8 | <> 9 | 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /pages/api/admin/roles/get.ts: -------------------------------------------------------------------------------- 1 | import { NextApiHandler } from "next"; 2 | import {getSession } from 'next-auth/client'; 3 | import { getRoles } from "~/db/roles"; 4 | 5 | 6 | const handler: NextApiHandler = async (req, res) => { 7 | if(req.method !== 'GET') { 8 | res.status(403).end(); 9 | return; 10 | } 11 | 12 | const session = await getSession({req: req}); 13 | 14 | console.log({session}); 15 | 16 | if(!session) { 17 | res.status(401).end(); 18 | return; 19 | } 20 | 21 | const roles = await getRoles(); 22 | 23 | return res.status(200).send({ roles }); 24 | } 25 | 26 | export default handler; -------------------------------------------------------------------------------- /pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import Providers from "next-auth/providers"; 3 | import { checkEmailExists, insertUser } from "~/db/users"; 4 | 5 | export default NextAuth({ 6 | providers: [ 7 | Providers.Auth0({ 8 | clientId: process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID, 9 | clientSecret: process.env.AUTH0_CLIENT_SECRET, 10 | domain: process.env.NEXT_PUBLIC_AUTH0_DOMAIN, 11 | }), 12 | ], 13 | callbacks: { 14 | async signIn(user, account, profile) { 15 | const isAllowedToSignIn = true 16 | if (isAllowedToSignIn) { 17 | const email = user.email as string; 18 | const emailExists = await checkEmailExists(email); 19 | if(!emailExists) { 20 | await insertUser(email, email); 21 | } 22 | return true 23 | } 24 | return false; 25 | } 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /pages/api/quiz-result.ts: -------------------------------------------------------------------------------- 1 | import { NextApiHandler } from "next"; 2 | import {getSession } from 'next-auth/client'; 3 | import { addQuizResult } from "~/db/users"; 4 | 5 | 6 | const handler: NextApiHandler = async (req, res) => { 7 | if(req.method !== 'POST') { 8 | res.status(403).end(); 9 | return; 10 | } 11 | 12 | const session = await getSession({req: req}); 13 | 14 | console.log({session}); 15 | 16 | if(!session) { 17 | res.status(401).end(); 18 | return; 19 | } 20 | 21 | const email = session.user?.email as string; 22 | 23 | const {date, name, numberCorrect, totalQuestions, secondsToComplete} = req.body; 24 | 25 | await addQuizResult(email, { 26 | date, 27 | name, 28 | numberCorrect, 29 | totalQuestions, 30 | secondsToComplete 31 | }) 32 | 33 | res.json({success: true}) 34 | } 35 | 36 | export default handler; -------------------------------------------------------------------------------- /pages/contribute.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PageHeader from "../components/shared/PageHeader"; 3 | import Info from "../components/contributepage/InfoSection"; 4 | import Resources from "../components/contributepage/ResourcesSection"; 5 | 6 | export default function Home() { 7 | return ( 8 | <> 9 | How To Contribute 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /pages/data-privacy.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { breakpoint } from "../frontend-config"; 4 | import PageHeader from "../components/shared/PageHeader"; 5 | 6 | const Headline = styled.h3` 7 | font-size: 25px; 8 | line-height: 29px; 9 | margin-bottom: 12px; 10 | margin-top: 30px; 11 | `; 12 | 13 | const Section = styled.div` 14 | padding: 35px 75px 38px; 15 | line-height: 1.5em; 16 | 17 | @media (min-width: ${breakpoint("lg")}) { 18 | padding: 40px 75px 62px; 19 | } 20 | `; 21 | 22 | export default function DataPrivacy() { 23 | return ( 24 | <> 25 | Data Privacy 26 |
27 | 1. Privacy Notice 28 |

This privacy notice discloses the privacy practices for our website. This privacy notice applies solely to information collected by this website. It will notify you of the following:

29 | 30 |

What personally identifiable information is collected from you through the website, how it is used and with whom it may be shared. 31 | What choices are available to you regarding the use of your data. 32 | The security procedures in place to protect the misuse of your information. 33 | How you can correct any inaccuracies in the information.

34 | 35 | 2. Information Collection, Use, and Sharing 36 |

We are the sole owners of the information collected on this site. We only have access to/collect information that you voluntarily give us via email or other direct contact from you.

37 | 38 |

We will use your information to respond to you, regarding the reason you contacted us. We will not share your information with any third party outside of our organization, other than as necessary to fulfill requests from you, and we will not sell your information to anyone.

39 | 40 |
41 | 42 |

If you you have questions about this privacy policy contact us at support@chingu.io

43 |
44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Hero from "../components/frontpage/HeroSection"; 3 | import Info from "../components/frontpage/InfoSection"; 4 | import Chingu from "../components/frontpage/ChinguSection"; 5 | 6 | export default function Home() { 7 | return ( 8 | <> 9 | 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /pages/profile.tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from "next"; 2 | import { getSession } from "next-auth/client"; 3 | import React from "react"; 4 | import styled from "styled-components"; 5 | import { getUserData } from "~/db/users"; 6 | import { UserData } from "~/models/User/user"; 7 | import { Headline, Wrapper } from "../components/aboutUs/styles"; 8 | import { ContentWrapper } from "../components/quizSingle/styles"; 9 | import { ScoreGraphCore } from "../components/quizSingle/ScoreGraph"; 10 | 11 | const QuizResultListItem = styled.li` 12 | border: 1px dotted #ccc; 13 | margin: 8px 0px; 14 | border-radius: 8px; 15 | 16 | @media (min-width: 768px) { 17 | margin: 0px; 18 | } 19 | 20 | & > div { 21 | margin: 0; 22 | display: grid; 23 | grid-template-columns: 1fr 100px; 24 | } 25 | h3 { 26 | font-weight: bold; 27 | margin: 12px 0; 28 | } 29 | p { 30 | margin: 12px 0; 31 | line-height: 1.4rem; 32 | } 33 | 34 | align-items: center; 35 | 36 | padding: 12px; 37 | 38 | footer { 39 | font-style: italic; 40 | font-size: 0.8rem; 41 | color: rgba(0, 0, 0, 0.8); 42 | margin: 8px 0; 43 | } 44 | `; 45 | 46 | const QuizResultList = styled.ul` 47 | @media (min-width: 768px) { 48 | display: grid; 49 | grid-template-columns: repeat(2, minmax(0, 1fr)); 50 | column-gap: 12px; 51 | row-gap: 12px; 52 | } 53 | 54 | @media (min-width: 1024px) { 55 | display: grid; 56 | grid-template-columns: repeat(3, minmax(0, 1fr)); 57 | } 58 | `; 59 | 60 | const ProfilePage = ({ userData }: { userData: UserData | null }) => { 61 | return ( 62 | 63 | 64 | Your Quiz Results 65 | 66 | 67 | {userData?.quizResults?.map(result => { 68 | const percentCorrect = 69 | (100 * result.numberCorrect) / result.totalQuestions; 70 | 71 | return ( 72 | 73 |
74 |
75 |

{result.name}

76 |

77 | You got {result.numberCorrect} out of{" "} 78 | {result.totalQuestions} correct in{" "} 79 | {result.secondsToComplete} seconds! 80 |

81 |
82 | 83 | 84 |
85 | 86 |
Taken {result.date}
87 |
88 | ); 89 | })} 90 |
91 |
92 |
93 | ); 94 | }; 95 | 96 | export const getServerSideProps: GetServerSideProps = async context => { 97 | const session = await getSession({ req: context.req }); 98 | 99 | if (!session) { 100 | return { 101 | props: {}, 102 | notFound: true, 103 | }; 104 | } 105 | 106 | const userData = await getUserData(session.user?.email as string); 107 | 108 | return { 109 | props: { 110 | userData, 111 | }, 112 | }; 113 | }; 114 | 115 | export default ProfilePage; 116 | -------------------------------------------------------------------------------- /pages/quiz/[slug].tsx: -------------------------------------------------------------------------------- 1 | /* 2 | This page will load at the url "/quiz/:slug" 3 | */ 4 | 5 | import { useState, useEffect, useContext } from "react"; 6 | import { 7 | AnswersTileSection, 8 | NextQuestionBtnContainer, 9 | SubmitQuizBtnContainer, 10 | AnswerTileContainerLink, 11 | ContentWrapper, 12 | } from "../../components/quizSingle/styles"; 13 | import PageHeader from "../../components/shared/PageHeader"; 14 | import QuestionHeader from "../../components/quizSingle/QuestionHeader"; 15 | import AnswerTileContainer from "../../components/quizSingle/AnswerTileContainer"; 16 | import NextQuestionBtn from "../../components/quizSingle/NextQuestionBtn"; 17 | import SubmitQuizBtn from "../../components/quizSingle/SubmitQuizBtn"; 18 | import ResultView from "../../components/quizSingle/ResultView"; 19 | import db from "../../db"; 20 | import { Question } from "../../models/ChinguQuiz/Question"; 21 | import { QuizRecord } from "../../models/ChinguQuiz/QuizRecord"; 22 | import { Answer } from "../../models/ChinguQuiz/Answer"; 23 | import { QuizContext, QuizContextProvider } from "../../contexts/quiz-context"; 24 | import { QuizResult } from "~/models/User/user"; 25 | 26 | interface QuizProps { 27 | quizTitle: string; 28 | quizQuestions: Question[]; 29 | } 30 | 31 | // Shuffles an array using Fisher-Yates algorithm 32 | // See: https://www.juniordevelopercentral.com/how-to-shuffle-an-array-in-javascript/ 33 | const shuffleArray = (array: any) => { 34 | for (let i = array.length - 1; i > 0; i--) { 35 | const j = Math.floor(Math.random() * (i + 1)); 36 | const temp = array[i]; 37 | array[i] = array[j]; 38 | array[j] = temp; 39 | } 40 | }; 41 | 42 | const saveQuizResult = async (quizResult: QuizResult) => { 43 | const response = await fetch("/api/quiz-result", { 44 | method: "POST", 45 | headers: { "Content-Type": "application/json" }, 46 | body: JSON.stringify(quizResult), 47 | }); 48 | const data = await response.json(); 49 | return data; 50 | }; 51 | 52 | function Quiz({ quizTitle, quizQuestions: originalQuizQuestions }: QuizProps) { 53 | const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); 54 | const [selectedAnswers, setSelectedAnswers] = useState([]); 55 | const [quizRecord, setQuizRecord] = useState([]); 56 | const [quizSubmitted, setQuizSubmitted] = useState(false); 57 | const [quizQuestions, setQuizQuestions] = useState([]); 58 | const { timer, setPaused } = useContext(QuizContext); 59 | 60 | const submittedPageHeaderText = "You did it!"; 61 | 62 | useEffect(() => { 63 | const randomizedQuestions = [...originalQuizQuestions]; 64 | randomizedQuestions.forEach(question => { 65 | const randomizedAnswers = [...question.answers]; 66 | shuffleArray(randomizedAnswers); 67 | question.answers = randomizedAnswers; 68 | }); 69 | shuffleArray(randomizedQuestions); 70 | setQuizQuestions(randomizedQuestions); 71 | }, [originalQuizQuestions]); 72 | 73 | useEffect(() => { 74 | if (quizSubmitted && quizQuestions.length === quizRecord.length) { 75 | saveQuizResult({ 76 | date: new Date().toISOString(), 77 | name: quizTitle, 78 | numberCorrect: quizRecord.reduce((acc, curr) => acc + +curr.correct, 0), 79 | totalQuestions: quizRecord.length, 80 | secondsToComplete: timer, 81 | }); 82 | } 83 | }, [quizQuestions, quizRecord, timer, quizSubmitted, quizTitle]); 84 | 85 | const toggleSelectedAnswer = (answerId: string, questionIndex: number) => { 86 | setSelectedAnswers([answerId]); 87 | }; 88 | 89 | const nextQuestion = () => { 90 | updateQuizRecord(); 91 | setSelectedAnswers([]); 92 | setCurrentQuestionIndex(currentQuestionIndex + 1); 93 | }; 94 | 95 | const submitQuiz = () => { 96 | updateQuizRecord(); 97 | setQuizSubmitted(true); 98 | setPaused(true); 99 | }; 100 | 101 | const updateQuizRecord = () => { 102 | const correctAnswer = quizQuestions[currentQuestionIndex].answers.filter( 103 | a => a.is_correct === true 104 | )[0].prompt; 105 | const userAnswer = quizQuestions[currentQuestionIndex].answers.filter( 106 | a => a.id === selectedAnswers[0] 107 | )[0].prompt; 108 | setQuizRecord(current => [ 109 | ...current, 110 | { 111 | question: quizQuestions[currentQuestionIndex].prompt, 112 | correctAnswer, 113 | userAnswer, 114 | correct: correctAnswer === userAnswer, 115 | }, 116 | ]); 117 | }; 118 | 119 | return ( 120 | <> 121 | {quizSubmitted ? `Your Results` : quizTitle} 122 | {quizSubmitted && ( 123 | 124 | )} 125 | {!quizSubmitted && 126 | quizQuestions[currentQuestionIndex] && 127 | quizQuestions[currentQuestionIndex].answers && ( 128 |
129 |

Elapsed Time: {timer}

130 | 136 | 137 | 138 | {quizQuestions[currentQuestionIndex].answers.map((answer, i) => ( 139 | { 142 | toggleSelectedAnswer(answer.id, currentQuestionIndex); 143 | }} 144 | > 145 | 150 | 151 | ))} 152 | 153 | {currentQuestionIndex !== quizQuestions.length - 1 ? ( 154 | 155 | 156 | {selectedAnswers.length >= 1 ? ( 157 | 163 | 164 | 165 | ) : ( 166 | 167 | )} 168 | 169 | 170 | ) : ( 171 | 172 | 173 | {selectedAnswers.length >= 1 ? ( 174 | 180 | 181 | 182 | ) : ( 183 | 184 | )} 185 | 186 | 187 | )} 188 |
189 | )} 190 | 191 | ); 192 | } 193 | 194 | export default function QuizWithContext({ 195 | quizTitle, 196 | quizQuestions, 197 | }: QuizProps) { 198 | return ( 199 | 200 | 201 | 202 | ); 203 | } 204 | 205 | interface Ids { 206 | id: string; 207 | } 208 | 209 | export async function getStaticPaths() { 210 | const { rows: ids } = await db.query("SELECT id FROM quiz"); 211 | const paths = ids.map(({ id }: Ids) => ({ 212 | params: { 213 | slug: id, 214 | }, 215 | })); 216 | 217 | return { 218 | paths, 219 | fallback: false, 220 | }; 221 | } 222 | 223 | export async function getStaticProps({ 224 | params: { slug }, 225 | }: { 226 | params: { slug: string }; 227 | }) { 228 | const { 229 | rows: [{ title }], 230 | } = await db.query("SELECT title FROM quiz WHERE id = $1", [slug]); 231 | const { rows: questions } = await db.query( 232 | "SELECT * FROM question WHERE quiz = $1", 233 | [slug] 234 | ); 235 | const { rows: answers } = await db.query( 236 | "SELECT id, question, prompt, quiz, is_correct FROM answer WHERE quiz = $1", 237 | [slug] 238 | ); 239 | 240 | const quizQuestions = questions.map((question: Question) => ({ 241 | id: question.id, 242 | prompt: question.prompt, 243 | answers: answers.filter( 244 | (answer: Answer) => answer.question === question.id 245 | ), 246 | })); 247 | 248 | return { 249 | props: { 250 | quizTitle: title, 251 | quizQuestions, 252 | }, 253 | }; 254 | } 255 | -------------------------------------------------------------------------------- /pages/quizzes.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | This page will load at the url "/quizzes" 3 | */ 4 | 5 | import React, { useState, useEffect, useCallback } from "react"; 6 | import QuizTile from "../components/quizSelection/QuizTile"; 7 | import TopicSelection from "../components/quizSelection/TopicSelection"; 8 | import { TileSection } from "../components/quizSelection/styles"; 9 | import PageHeader from "../components/shared/PageHeader"; 10 | import type { ChinguQuiz, UI } from "../models"; 11 | import db from "../db"; 12 | 13 | export default function Quizzes({ 14 | quizzes, 15 | subjectsAndTopics, 16 | }: { 17 | quizzes: ChinguQuiz.Quiz[]; 18 | subjectsAndTopics: UI.Quizzes.SubjectAndTopic[]; 19 | }) { 20 | const [chosenSubject, setChosenSubject] = useState("All"); 21 | const [chosenTopics, setChosenTopics] = useState([]); 22 | const [filteredQuizzes, setFilteredQuizzes] = useState([]); 23 | 24 | const subjectFilterCallback = useCallback( 25 | (quizTagArray: string[]) => { 26 | let totalMatches = 0; 27 | for (let i = 0; i < quizTagArray.length; i += 1) { 28 | if (chosenTopics.indexOf(quizTagArray[i]) >= 0) { 29 | totalMatches += 1; 30 | } 31 | } 32 | // Only includes quiz if all quiz tag are matched to topic selection in the UI 33 | return totalMatches === quizTagArray.length; 34 | }, 35 | [chosenTopics] 36 | ); 37 | 38 | // Handle filtering of quizzes 39 | useEffect(() => { 40 | if (chosenSubject === "All") { 41 | setFilteredQuizzes(quizzes); 42 | } else if (chosenSubject !== "All" && chosenTopics.length === 0) { 43 | setFilteredQuizzes( 44 | quizzes.filter(quiz => 45 | quiz.subject.includes(chosenSubject.toLowerCase()) 46 | ) 47 | ); 48 | } else if (chosenSubject !== "All" && chosenTopics.length > 0) { 49 | setFilteredQuizzes( 50 | quizzes 51 | .filter(quiz => quiz.subject.includes(chosenSubject.toLowerCase())) 52 | .filter(quiz => subjectFilterCallback(quiz.tag)) 53 | ); 54 | } 55 | }, [chosenTopics, quizzes, chosenSubject, subjectFilterCallback]); 56 | 57 | return ( 58 | <> 59 | Quizzes 60 | 67 | {!!filteredQuizzes && ( 68 | 69 | {filteredQuizzes.map((quiz, i) => ( 70 | 75 | ))} 76 | 77 | )} 78 | 79 | ); 80 | } 81 | 82 | export async function getStaticProps() { 83 | interface Subject { 84 | id: string; 85 | title: string; 86 | } 87 | interface Tag { 88 | id: string; 89 | title: string; 90 | } 91 | 92 | const { rows: quizzes } = await db.query("SELECT * FROM quiz"); 93 | const { rows: subjects } = await db.query("SELECT * FROM subject"); 94 | const { rows: tag } = await db.query("SELECT * FROM tag"); 95 | 96 | const subjectsMap: { 97 | [index: string]: string; 98 | } = {}; 99 | subjects.forEach((subject: Subject) => { 100 | subjectsMap[subject.id] = subject.title; 101 | }); 102 | const tagMap: { 103 | [index: string]: string; 104 | } = {}; 105 | tag.forEach((tag: Tag) => { 106 | tagMap[tag.id] = tag.title; 107 | }); 108 | const subjectsAndTopics: UI.Quizzes.SubjectAndTopic[] = []; 109 | quizzes.forEach((quiz: ChinguQuiz.Quiz) => { 110 | const { subject, tag } = quiz; 111 | const searchResult = subjectsAndTopics.find( 112 | item => item.title === subjectsMap[subject] 113 | ); 114 | if (searchResult) { 115 | tag.forEach((id: string | number) => { 116 | searchResult.tag.push(tagMap[id]); 117 | }); 118 | } else { 119 | subjectsAndTopics.push({ 120 | key: subjectsMap[subject], 121 | title: subjectsMap[subject], 122 | tag: tag.map((id: string | number) => tagMap[id]), 123 | }); 124 | } 125 | }); 126 | 127 | return { 128 | props: { 129 | quizzes: quizzes.map((quiz: ChinguQuiz.Quiz) => ({ 130 | ...quiz, 131 | subject: subjectsMap[quiz.subject], 132 | tag: quiz.tag.map(id => tagMap[id]), 133 | })), 134 | subjectsAndTopics, 135 | }, 136 | }; 137 | } 138 | -------------------------------------------------------------------------------- /public/Home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chingu-voyages/moonshot-chingu-quiz/0a2e1a285ba4b8df68ec0210b942797ec712eb9f/public/Home.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chingu-voyages/moonshot-chingu-quiz/0a2e1a285ba4b8df68ec0210b942797ec712eb9f/public/favicon.ico -------------------------------------------------------------------------------- /public/home-chingu-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chingu-voyages/moonshot-chingu-quiz/0a2e1a285ba4b8df68ec0210b942797ec712eb9f/public/home-chingu-image.png -------------------------------------------------------------------------------- /public/home-chingu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chingu-voyages/moonshot-chingu-quiz/0a2e1a285ba4b8df68ec0210b942797ec712eb9f/public/logo.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /quiz_db.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- PostgreSQL database dump 3 | -- 4 | 5 | -- Dumped from database version 13.2 (Debian 13.2-1.pgdg100+1) 6 | -- Dumped by pg_dump version 13.3 (Ubuntu 13.3-1.pgdg18.04+1) 7 | 8 | SET statement_timeout = 0; 9 | SET lock_timeout = 0; 10 | SET idle_in_transaction_session_timeout = 0; 11 | SET client_encoding = 'UTF8'; 12 | SET standard_conforming_strings = on; 13 | SELECT pg_catalog.set_config('search_path', '', false); 14 | SET check_function_bodies = false; 15 | SET xmloption = content; 16 | SET client_min_messages = warning; 17 | SET row_security = off; 18 | 19 | -- 20 | -- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: - 21 | -- 22 | 23 | CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public; 24 | 25 | 26 | -- 27 | -- Name: EXTENSION pgcrypto; Type: COMMENT; Schema: -; Owner: 28 | -- 29 | 30 | COMMENT ON EXTENSION pgcrypto IS 'cryptographic functions'; 31 | 32 | 33 | -- 34 | -- Name: uuid-ossp; Type: EXTENSION; Schema: -; Owner: - 35 | -- 36 | 37 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public; 38 | 39 | 40 | -- 41 | -- Name: EXTENSION "uuid-ossp"; Type: COMMENT; Schema: -; Owner: 42 | -- 43 | 44 | COMMENT ON EXTENSION "uuid-ossp" IS 'generate universally unique identifiers (UUIDs)'; 45 | 46 | 47 | SET default_tablespace = ''; 48 | 49 | SET default_table_access_method = heap; 50 | 51 | -- 52 | -- Name: answer; Type: TABLE; Schema: public; Owner: docker 53 | -- 54 | 55 | CREATE TABLE public.answer ( 56 | id uuid DEFAULT public.uuid_generate_v4() NOT NULL, 57 | question uuid NOT NULL, 58 | prompt character varying(1000) NOT NULL, 59 | is_correct boolean NOT NULL, 60 | quiz uuid NOT NULL 61 | ); 62 | 63 | 64 | ALTER TABLE public.answer OWNER TO docker; 65 | 66 | -- 67 | -- Name: question; Type: TABLE; Schema: public; Owner: docker 68 | -- 69 | 70 | CREATE TABLE public.question ( 71 | id uuid DEFAULT public.uuid_generate_v4() NOT NULL, 72 | quiz uuid NOT NULL, 73 | prompt character varying(1000) NOT NULL 74 | ); 75 | 76 | 77 | ALTER TABLE public.question OWNER TO docker; 78 | 79 | -- 80 | -- Name: quiz; Type: TABLE; Schema: public; Owner: docker 81 | -- 82 | 83 | CREATE TABLE public.quiz ( 84 | description character varying(1000) NOT NULL, 85 | title character varying(240) NOT NULL, 86 | id uuid DEFAULT public.uuid_generate_v4() NOT NULL, 87 | tag uuid[] NOT NULL, 88 | subject uuid NOT NULL 89 | ); 90 | 91 | 92 | ALTER TABLE public.quiz OWNER TO docker; 93 | 94 | -- 95 | -- Name: subject; Type: TABLE; Schema: public; Owner: docker 96 | -- 97 | 98 | CREATE TABLE public.subject ( 99 | id uuid DEFAULT public.uuid_generate_v4() NOT NULL, 100 | title character varying(240) NOT NULL 101 | ); 102 | 103 | 104 | ALTER TABLE public.subject OWNER TO docker; 105 | 106 | -- 107 | -- Name: tag; Type: TABLE; Schema: public; Owner: docker 108 | -- 109 | 110 | CREATE TABLE public.tag ( 111 | id uuid DEFAULT public.uuid_generate_v4() NOT NULL, 112 | title character varying(240) NOT NULL 113 | ); 114 | 115 | 116 | ALTER TABLE public.tag OWNER TO docker; 117 | 118 | -- 119 | -- Name: users; Type: TABLE; Schema: public; Owner: docker 120 | -- 121 | 122 | CREATE TABLE public.users ( 123 | uid integer NOT NULL, 124 | nickname character varying(64) NOT NULL, 125 | email character varying(320) NOT NULL, 126 | data json 127 | ); 128 | 129 | 130 | ALTER TABLE public.users OWNER TO docker; 131 | 132 | -- 133 | -- Name: users_uid_seq; Type: SEQUENCE; Schema: public; Owner: docker 134 | -- 135 | 136 | CREATE SEQUENCE public.users_uid_seq 137 | AS integer 138 | START WITH 1 139 | INCREMENT BY 1 140 | NO MINVALUE 141 | NO MAXVALUE 142 | CACHE 1; 143 | 144 | 145 | ALTER TABLE public.users_uid_seq OWNER TO docker; 146 | 147 | -- 148 | -- Name: users_uid_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: docker 149 | -- 150 | 151 | ALTER SEQUENCE public.users_uid_seq OWNED BY public.users.uid; 152 | 153 | 154 | -- 155 | -- Name: users uid; Type: DEFAULT; Schema: public; Owner: docker 156 | -- 157 | 158 | ALTER TABLE ONLY public.users ALTER COLUMN uid SET DEFAULT nextval('public.users_uid_seq'::regclass); 159 | 160 | 161 | -- 162 | -- Data for Name: answer; Type: TABLE DATA; Schema: public; Owner: docker 163 | -- 164 | 165 | COPY public.answer (id, question, prompt, is_correct, quiz) FROM stdin; 166 | 8fb4e291-ad29-4031-9632-ae1336c11578 df29dcc7-8669-48df-815d-fee382dae18b null f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 167 | 0650d305-3936-4e12-8834-962b6bead4ce df29dcc7-8669-48df-815d-fee382dae18b undefined t c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 168 | 8298ed6e-2ed9-4ecd-8d72-eae88ff46e2e df29dcc7-8669-48df-815d-fee382dae18b typeError f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 169 | 16f23de5-d91d-4ffb-8dbe-b59518d8749c df29dcc7-8669-48df-815d-fee382dae18b NaN f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 170 | caf3a4c0-bc36-46c2-b143-fa91541ad84f f5910387-f471-4ed5-a967-686b33ac1867 8**1 f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 171 | dc6285d7-ddc2-4688-9f24-2f6214cd77a3 f5910387-f471-4ed5-a967-686b33ac1867 Math.pow(16, .75) f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 172 | 800bc667-d070-4f20-97ff-1982ee92598b f5910387-f471-4ed5-a967-686b33ac1867 2 * 2 * 'two' t c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 173 | b24bcc68-3d60-49ba-9711-ae6c61992bd1 f5910387-f471-4ed5-a967-686b33ac1867 2**+'3' f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 174 | 42d35d62-e51b-4cbf-8f3b-d5cb96f85f5f 9c0adfb0-06b1-4591-ae1e-7f2208dac26b The typeof is a unary operator that is placed before its single operand, which can be of any type. f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 175 | 15747f03-fea9-421c-bdc1-91640297c734 9c0adfb0-06b1-4591-ae1e-7f2208dac26b Its value is a string indicating the data type of the operand. f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 176 | aa911118-5503-4d38-9a3d-7ff9b0d6d332 9c0adfb0-06b1-4591-ae1e-7f2208dac26b Both of the above. t c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 177 | 4b08184e-0976-4257-b2af-80bc24ae0f12 9c0adfb0-06b1-4591-ae1e-7f2208dac26b None of the above. f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 178 | b84e3e48-bdce-43ac-b8dc-a5760765081d 57bf3808-907b-4b2c-be9d-187ba62c0c81 changeOrder(order) f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 179 | 7b025636-d72c-4846-ae4d-f6a414bf1ea0 57bf3808-907b-4b2c-be9d-187ba62c0c81 reverse() t c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 180 | 635b8545-3d07-4d42-ae8f-9c478600b124 57bf3808-907b-4b2c-be9d-187ba62c0c81 sort(order) f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 181 | c1f3b602-94ae-4d46-a9c3-057bda16b1cf 57bf3808-907b-4b2c-be9d-187ba62c0c81 none of the above f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 182 | 95ad4780-746b-4774-88f4-cc9290527a8d 5e9928bd-3e1b-42b2-9c5a-6017546a4b0a charAt() f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 183 | a7b9caa9-5382-46bb-9d6b-43a6b8112c9d 5e9928bd-3e1b-42b2-9c5a-6017546a4b0a concat() f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 184 | e26ce8b4-cf7c-477f-82d8-3561b3090562 5e9928bd-3e1b-42b2-9c5a-6017546a4b0a indexOf() f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 185 | 236bbeb4-a8e8-4cc4-9f56-6d18f5754cd1 ae0f5697-962b-45f6-b2c4-d556e573fa5c search() f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 186 | 1357c9db-cec4-44d0-842f-e5092137b733 ae0f5697-962b-45f6-b2c4-d556e573fa5c lastIndexOf() t c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 187 | fa99451f-1654-4cdb-a27d-f991d7550419 ae0f5697-962b-45f6-b2c4-d556e573fa5c substr() f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 188 | e3e811f3-e6e5-45f3-94cd-54677e4c4636 ae0f5697-962b-45f6-b2c4-d556e573fa5c indexOf() f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 189 | 25c9942e-3334-4c80-b24a-4ccdabc7c222 c30cf8f2-e13e-43b9-80cc-032385f059f2 concat() f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 190 | 8f4efe94-ead8-41db-88ea-2b8d00c83b67 c30cf8f2-e13e-43b9-80cc-032385f059f2 join() t c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 191 | 4385d09e-23ef-4f6d-9be4-64a0e689b8a6 c30cf8f2-e13e-43b9-80cc-032385f059f2 pop() f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 192 | 47c698f2-3e27-48e0-982d-cf029845e17b c30cf8f2-e13e-43b9-80cc-032385f059f2 map() f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 193 | 2b4176cd-f01c-4344-ada1-4499bbb70294 55880180-bfd6-4874-ac05-4e6b4e6d8829 interface f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 194 | ffb67950-0ed4-4d5e-876b-7d636ab3e7a3 55880180-bfd6-4874-ac05-4e6b4e6d8829 throws f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 195 | eaefadba-007d-43ec-bc4f-621395938982 55880180-bfd6-4874-ac05-4e6b4e6d8829 program f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 196 | acb14fb9-18a4-46e0-8cd7-fa0542d745a8 55880180-bfd6-4874-ac05-4e6b4e6d8829 short t c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 197 | 08d04d6f-568a-47e0-897b-773b2db0fd78 1f779a0f-4234-466c-b4a7-3c8f923164f2 let t c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 198 | 76661b12-1430-4ea8-937c-e7ae57397bcf 1f779a0f-4234-466c-b4a7-3c8f923164f2 define f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 199 | 8fab635e-70a2-421b-aae4-c7e3fd53a0ae 1f779a0f-4234-466c-b4a7-3c8f923164f2 variable f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 200 | dcba8da1-2bc3-4e84-bdf3-7f8aae1194e1 1f779a0f-4234-466c-b4a7-3c8f923164f2 const f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 201 | 2a5701ad-ec0c-42ae-9a87-2a048680744f 6c4338b5-e51a-4f60-8c13-12bb09fa3a92 JavaScript can be used for functional programming. f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 202 | d5adaf85-37d2-4904-beee-8aebbd84ef45 6c4338b5-e51a-4f60-8c13-12bb09fa3a92 JavaScript can be used for file reading and writing on client machines. t c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 203 | 9e862c4a-8705-487a-98d8-5c91e97163bd 6c4338b5-e51a-4f60-8c13-12bb09fa3a92 JavaScript does not require frameworks or libraries to be used. f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 204 | 71961cb4-1bbe-4224-82b8-3b51b329b6fe 6c4338b5-e51a-4f60-8c13-12bb09fa3a92 Though not class-based, JavaScript is an Object-Oriented Programming Language. f c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 205 | 6a4109d1-d1ac-48b3-918e-03eab340d56a 5e9928bd-3e1b-42b2-9c5a-6017546a4b0a charCodeAt() t c393d58f-c966-4dcb-a7fc-f1c726f4b1c8 206 | 469b08d5-7cd7-422d-9269-c9cee1cf2f42 b27cbd0e-5ddc-4770-8cc9-a86e8cb47118 #sm-col f 6ee607e2-8128-48ec-982d-b711e5f1ab87 207 | 38157e80-8495-4591-97e1-3c6d5653fd5f b27cbd0e-5ddc-4770-8cc9-a86e8cb47118 .sm, .col f 6ee607e2-8128-48ec-982d-b711e5f1ab87 208 | ebe663f0-8a86-4c4e-b617-d8ca32f8bb25 b27cbd0e-5ddc-4770-8cc9-a86e8cb47118 [class~=col] t 6ee607e2-8128-48ec-982d-b711e5f1ab87 209 | 78a6f387-a50f-480d-bdd2-487681642111 b27cbd0e-5ddc-4770-8cc9-a86e8cb47118 .sm_col f 6ee607e2-8128-48ec-982d-b711e5f1ab87 210 | 9a9b205c-ac97-48d3-b51d-1d5092161e32 10282eb0-e189-400d-8cb6-563e30737ce8 viewfinder height f 6ee607e2-8128-48ec-982d-b711e5f1ab87 211 | 075d73c4-6dbd-4fff-868a-567665deca4b 10282eb0-e189-400d-8cb6-563e30737ce8 viewport width f 6ee607e2-8128-48ec-982d-b711e5f1ab87 212 | 85d2e1f6-d548-42ff-b431-db7c66443b55 10282eb0-e189-400d-8cb6-563e30737ce8 visible height f 6ee607e2-8128-48ec-982d-b711e5f1ab87 213 | 85bd8fc2-c568-4f5f-8234-0c9f2b17d465 10282eb0-e189-400d-8cb6-563e30737ce8 viewport height t 6ee607e2-8128-48ec-982d-b711e5f1ab87 214 | f5c1716b-9632-4f46-aa8f-a9eefcf20276 3ab172ce-895a-4f69-a98e-a6d6e1784fbe id t 6ee607e2-8128-48ec-982d-b711e5f1ab87 215 | de894143-80bb-47eb-9548-81d09b2adaaa 3ab172ce-895a-4f69-a98e-a6d6e1784fbe class f 6ee607e2-8128-48ec-982d-b711e5f1ab87 216 | f3599536-8b0c-4ca5-a5fa-2ab2cb3b38c7 3ab172ce-895a-4f69-a98e-a6d6e1784fbe text f 6ee607e2-8128-48ec-982d-b711e5f1ab87 217 | aa0a7488-4a45-4bbe-8882-5a8e63a099d9 3ab172ce-895a-4f69-a98e-a6d6e1784fbe name f 6ee607e2-8128-48ec-982d-b711e5f1ab87 218 | 3a0efa9c-bb69-4bfc-848c-73518b7d8d14 79927764-c774-4837-8331-fb6be3ea660f margin f 6ee607e2-8128-48ec-982d-b711e5f1ab87 219 | 0ba4ee58-47b7-4bfb-8f1d-d3b80301d2e5 79927764-c774-4837-8331-fb6be3ea660f clear t 6ee607e2-8128-48ec-982d-b711e5f1ab87 220 | dfb04eae-0dd7-4c60-af4c-a423ff6b2d84 79927764-c774-4837-8331-fb6be3ea660f float f 6ee607e2-8128-48ec-982d-b711e5f1ab87 221 | 16843d9e-330f-4d44-8898-96ff47281c8f 79927764-c774-4837-8331-fb6be3ea660f floating f 6ee607e2-8128-48ec-982d-b711e5f1ab87 222 | ebddc93f-292d-484b-b0ad-067cefedb9c7 01224e75-cc9d-4950-933f-894f4d9e9390 wrap f 6ee607e2-8128-48ec-982d-b711e5f1ab87 223 | 1033c81d-65be-42c4-8a60-5c16d3081e92 01224e75-cc9d-4950-933f-894f4d9e9390 push f 6ee607e2-8128-48ec-982d-b711e5f1ab87 224 | 72cd67da-deb0-484a-a712-e2acc85fb124 01224e75-cc9d-4950-933f-894f4d9e9390 float t 6ee607e2-8128-48ec-982d-b711e5f1ab87 225 | d07a9f80-9a3b-46a5-9197-5969092cf5f1 01224e75-cc9d-4950-933f-894f4d9e9390 align f 6ee607e2-8128-48ec-982d-b711e5f1ab87 226 | f995747e-9f0c-4134-82f0-79f4c39667e6 1f759393-60c4-49fd-aff7-c8af7f945a55 pointer f 6ee607e2-8128-48ec-982d-b711e5f1ab87 227 | e33e5ba4-bd58-4e6a-b838-8aa8d3b650a9 1f759393-60c4-49fd-aff7-c8af7f945a55 default t 6ee607e2-8128-48ec-982d-b711e5f1ab87 228 | e58f0b65-e73a-4f8f-a26a-81166fada274 1f759393-60c4-49fd-aff7-c8af7f945a55 arrow f 6ee607e2-8128-48ec-982d-b711e5f1ab87 229 | 519efee9-f548-490f-b285-d5a358fcaa3b 1f759393-60c4-49fd-aff7-c8af7f945a55 arr f 6ee607e2-8128-48ec-982d-b711e5f1ab87 230 | f5b15d8e-d596-408c-9bea-6c4b01b0d171 31780751-15da-4ec9-8866-f122c871ba25 border-color f 6ee607e2-8128-48ec-982d-b711e5f1ab87 231 | 82ad08d7-b855-4d44-bd76-ccefc6b8f448 31780751-15da-4ec9-8866-f122c871ba25 border-decoration f 6ee607e2-8128-48ec-982d-b711e5f1ab87 232 | 93328a4c-c7df-4243-b250-016ea8836477 31780751-15da-4ec9-8866-f122c871ba25 border-style t 6ee607e2-8128-48ec-982d-b711e5f1ab87 233 | 02f747ff-5a20-4bca-b123-90c7ff232465 31780751-15da-4ec9-8866-f122c871ba25 border-line f 6ee607e2-8128-48ec-982d-b711e5f1ab87 234 | 1113f5d5-d6ea-46d5-91e4-efc9587f4d7d b841f5fd-8ef0-434b-a2ee-41e5dda510ce empty-cell t 6ee607e2-8128-48ec-982d-b711e5f1ab87 235 | dcb482d0-391a-4446-a42b-a9fd77952525 b841f5fd-8ef0-434b-a2ee-41e5dda510ce blank-cell f 6ee607e2-8128-48ec-982d-b711e5f1ab87 236 | b1af0454-43ca-4f30-9c1d-e92f64ef3ac3 b841f5fd-8ef0-434b-a2ee-41e5dda510ce noncontent-cell f 6ee607e2-8128-48ec-982d-b711e5f1ab87 237 | 310936a6-5dd2-4fff-a38d-c605b91f454b b841f5fd-8ef0-434b-a2ee-41e5dda510ce void-cell f 6ee607e2-8128-48ec-982d-b711e5f1ab87 238 | 0638fd89-48d8-412b-89bd-7842afb819a2 3a61640e-a59e-4497-9497-5d05fb1c78ef 640 pixels t 6ee607e2-8128-48ec-982d-b711e5f1ab87 239 | 89b360b1-82f5-47bb-b1ec-49402f8fdc34 3a61640e-a59e-4497-9497-5d05fb1c78ef 100% f 6ee607e2-8128-48ec-982d-b711e5f1ab87 240 | 8f62fa54-00e3-4c43-90f3-7f903b7a0e12 3a61640e-a59e-4497-9497-5d05fb1c78ef full-screen f 6ee607e2-8128-48ec-982d-b711e5f1ab87 241 | 3e7d919b-e507-43d1-8b04-30ad0a9d10de 3a61640e-a59e-4497-9497-5d05fb1c78ef 1024px f 6ee607e2-8128-48ec-982d-b711e5f1ab87 242 | 33fed74d-c80f-4d0f-a29b-8d430497b86f 6409ea1a-d9e5-40b1-bcb7-bee17e31cdd6 /* a comment */ t 6ee607e2-8128-48ec-982d-b711e5f1ab87 243 | fb8f267c-8fb5-47ec-adc0-e196507c2256 6409ea1a-d9e5-40b1-bcb7-bee17e31cdd6 // a comment // f 6ee607e2-8128-48ec-982d-b711e5f1ab87 244 | 054f4211-4573-43e4-b679-4322c977bd3b 6409ea1a-d9e5-40b1-bcb7-bee17e31cdd6 / a comment / f 6ee607e2-8128-48ec-982d-b711e5f1ab87 245 | c5bc803d-94f9-47d9-978e-ebdd00798b8d 6409ea1a-d9e5-40b1-bcb7-bee17e31cdd6 <' a comment'> f 6ee607e2-8128-48ec-982d-b711e5f1ab87 246 | a6ba6d92-76d3-43b6-8e1f-194f35694d75 75a1e7a6-a020-417c-961b-2c58d7e3cf48 spacing f 6ee607e2-8128-48ec-982d-b711e5f1ab87 247 | 5bff7492-ec05-498a-b9ab-37f296947775 75a1e7a6-a020-417c-961b-2c58d7e3cf48 margin f 6ee607e2-8128-48ec-982d-b711e5f1ab87 248 | 4d02e6d1-8279-4678-9a94-366645a8f2de 75a1e7a6-a020-417c-961b-2c58d7e3cf48 padding t 6ee607e2-8128-48ec-982d-b711e5f1ab87 249 | ec4b9aac-a2f4-4025-a468-8ac7586f6838 75a1e7a6-a020-417c-961b-2c58d7e3cf48 inner-margin f 6ee607e2-8128-48ec-982d-b711e5f1ab87 250 | 710fb500-d06e-4412-8af5-f7ac2ab8cac8 81e59fa3-0aa4-4205-9190-47d3d85e293c A javascript file linked into the with a