├── .gitignore ├── src ├── styles │ ├── layouts │ │ ├── quizpage.css │ │ ├── editCardPage.css │ │ ├── editCardsetPage.css │ │ └── homePage.css │ ├── components │ │ ├── links.css │ │ ├── flashcardControls.css │ │ ├── editCardControls.css │ │ ├── footer.css │ │ ├── header.css │ │ ├── flashcard.css │ │ ├── textAreaInput.css │ │ ├── cardlistCard.css │ │ └── buttons.css │ ├── utilities │ │ ├── effects.css │ │ └── container.css │ ├── abstracts │ │ ├── test.css │ │ └── variables.css │ └── base │ │ └── typography.css ├── pages │ ├── index.js │ ├── EditCardPage.js │ ├── QuizPage.js │ ├── HomePage.js │ └── EditCardsetPage.js ├── components │ ├── Footer.js │ ├── index.js │ ├── Flashcard.js │ ├── TextAreaInput.js │ ├── Header.js │ ├── FlashcardControls.js │ ├── CardlistCard.js │ ├── EditCardControls.js │ └── App.js ├── index.js └── styles.css ├── .vercel ├── project.json └── README.txt ├── server ├── routes │ ├── userRouter.js │ ├── cardsetsRouter.js │ └── cardsRouter.js ├── models │ └── flashcardModel.js ├── server.js └── controllers │ └── flashcardController.js ├── postcss.config.js ├── index.html ├── webpack.config.js ├── readme.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .vercel 3 | -------------------------------------------------------------------------------- /src/styles/layouts/quizpage.css: -------------------------------------------------------------------------------- 1 | .cardsetName { 2 | text-transform: capitalize; 3 | } -------------------------------------------------------------------------------- /src/styles/components/links.css: -------------------------------------------------------------------------------- 1 | .edit-link { 2 | color: darkblue; 3 | margin: .5rem; 4 | } -------------------------------------------------------------------------------- /.vercel/project.json: -------------------------------------------------------------------------------- 1 | {"projectId":"prj_OAS8EH3p6tKsqVadXp0J5HwWCQZo","orgId":"cN8nVGERFGD0a3liWnl9kS2U"} -------------------------------------------------------------------------------- /src/styles/components/flashcardControls.css: -------------------------------------------------------------------------------- 1 | edit-link { 2 | color: var(--color-primary-dark); 3 | } -------------------------------------------------------------------------------- /src/styles/layouts/editCardPage.css: -------------------------------------------------------------------------------- 1 | .editCard { 2 | justify-content: center; 3 | align-items: center; 4 | 5 | & h1 { 6 | margin-bottom: 2rem; 7 | } 8 | } -------------------------------------------------------------------------------- /src/styles/layouts/editCardsetPage.css: -------------------------------------------------------------------------------- 1 | .editCardset { 2 | justify-content: center; 3 | align-items: center; 4 | 5 | & h1 { 6 | margin-bottom: 2rem; 7 | } 8 | } -------------------------------------------------------------------------------- /src/styles/utilities/effects.css: -------------------------------------------------------------------------------- 1 | .fade-in { 2 | opacity: 1; 3 | transition: opacity .8s ease; 4 | } 5 | 6 | .fade-out { 7 | opacity: 0; 8 | transition: opacity .8s ease; 9 | } -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | export { HomePage } from "./HomePage"; 2 | export { QuizPage } from "./QuizPage"; 3 | export { EditCardPage } from "./EditCardPage"; 4 | export { EditCardsetPage } from "./EditCardsetPage"; 5 | -------------------------------------------------------------------------------- /src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const Footer = (props) => { 4 | return ( 5 |
6 |

© 2023 Michael Gacetta

7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/styles/abstracts/test.css: -------------------------------------------------------------------------------- 1 | /* GLOBAL RESET */ 2 | *, 3 | *::before, 4 | *::after { 5 | margin: 0; 6 | padding: 0; 7 | box-sizing: border-box; 8 | } 9 | 10 | input, 11 | textarea, 12 | button { 13 | font: inherit; 14 | } -------------------------------------------------------------------------------- /src/styles/abstracts/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-primary-dark: #14213d; 3 | --color-primary-light: ; 4 | --color-accent: #fca311; 5 | --color-gray-light: #e5e5e5; 6 | --color-gray-dark: #979797; 7 | --color-white: #efefef; 8 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { App } from "./components/App"; 4 | import "./styles.css"; 5 | 6 | const domNode = document.querySelector("#root"); 7 | const root = createRoot(domNode); 8 | 9 | root.render(); 10 | -------------------------------------------------------------------------------- /src/styles/components/editCardControls.css: -------------------------------------------------------------------------------- 1 | .editCardControls-container { 2 | } 3 | 4 | .editCardControls-subControls { 5 | justify-content: space-between; 6 | 7 | & .saveButton, 8 | .deleteButton { 9 | margin: 0; 10 | } 11 | 12 | & .saveButton { 13 | margin-right: 1rem; 14 | } 15 | } -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export { CardlistCard } from "./CardlistCard"; 2 | export { EditCardControls } from "./EditCardControls"; 3 | export { Footer } from "./Footer"; 4 | export { Flashcard } from "./Flashcard"; 5 | export { FlashcardControls } from "./FlashcardControls"; 6 | export { Header } from "./Header"; 7 | export { TextAreaInput } from "./TextAreaInput"; 8 | -------------------------------------------------------------------------------- /src/styles/utilities/container.css: -------------------------------------------------------------------------------- 1 | .flex-container-col { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | .flex-container-row { 6 | display: flex; 7 | } 8 | 9 | .main-container { 10 | user-select: none; 11 | align-items: center; 12 | justify-content: flex-start; 13 | width: 80vw; 14 | margin: 0 auto; 15 | margin-bottom: 6rem; 16 | } -------------------------------------------------------------------------------- /server/routes/userRouter.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const flashcardController = require("../controllers/flashcardController"); 3 | 4 | // create router for '/users' requests 5 | const router = express.Router(); 6 | 7 | router.get("/", flashcardController.getUsers, (req, res) => { 8 | return res.status(200).json(res.locals.users); 9 | }); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /src/styles/components/footer.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | width: 100vw; 3 | height: 1rem; 4 | position: fixed; 5 | bottom: 0; 6 | 7 | display: flex; 8 | justify-content: flex-end; 9 | align-items: center; 10 | padding: 2rem; 11 | background-color: var(--color-primary-dark); 12 | color: var(--color-gray-dark); 13 | & p { 14 | font-size: 1rem; 15 | } 16 | } -------------------------------------------------------------------------------- /server/routes/cardsetsRouter.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const flashcardController = require("../controllers/flashcardController"); 3 | 4 | // create router for '/users' requests 5 | const router = express.Router(); 6 | 7 | router.get("/:cardset_id", flashcardController.getCards, (req, res, next) => { 8 | res.status(200).json(res.locals.cards); 9 | }); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const cssnano = require("cssnano"); 2 | const postcssPresetEnv = require("postcss-preset-env"); 3 | const autoprefixer = require("autoprefixer"); 4 | const postcssImport = require("postcss-import"); 5 | 6 | module.exports = { 7 | plugins: [ 8 | cssnano({ 9 | preset: "default", 10 | }), 11 | postcssPresetEnv({ 12 | stage: 1, 13 | }), 14 | autoprefixer, 15 | postcssImport, 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Flashcards 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/Flashcard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const Flashcard = (props) => { 4 | return ( 5 |
11 |
12 |
{props.sideA}
13 |
{props.sideB}
14 |
15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/styles/layouts/homePage.css: -------------------------------------------------------------------------------- 1 | .editCardset { 2 | justify-content: center; 3 | align-items: center; 4 | 5 | & h1 { 6 | margin-bottom: 2rem; 7 | } 8 | } 9 | 10 | .cardsets-container { 11 | width: 100%; 12 | margin: 2rem; 13 | display: flex; 14 | flex-direction: column; 15 | justify-item: center; 16 | align-items: center; 17 | } 18 | 19 | .login-container { 20 | padding: 1rem; 21 | margin: 2rem; 22 | 23 | & h3 { 24 | margin-bottom: 1rem; 25 | text-align: center; 26 | } 27 | } -------------------------------------------------------------------------------- /.vercel/README.txt: -------------------------------------------------------------------------------- 1 | > Why do I have a folder named ".vercel" in my project? 2 | The ".vercel" folder is created when you link a directory to a Vercel project. 3 | 4 | > What does the "project.json" file contain? 5 | The "project.json" file contains: 6 | - The ID of the Vercel project that you linked ("projectId") 7 | - The ID of the user or team your Vercel project is owned by ("orgId") 8 | 9 | > Should I commit the ".vercel" folder? 10 | No, you should not share the ".vercel" folder with anyone. 11 | Upon creation, it will be automatically added to your ".gitignore" file. 12 | -------------------------------------------------------------------------------- /src/components/TextAreaInput.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const TextAreaInput = (props) => { 4 | const isSideA = props.labelID === "sideA"; 5 | const isEmpty = props.value === ""; 6 | 7 | return ( 8 |
9 | 10 | 18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /server/routes/cardsRouter.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const flashcardController = require("../controllers/flashcardController"); 3 | 4 | // create router for '/users' requests 5 | const router = express.Router(); 6 | 7 | router.put("/:card_id", flashcardController.updateCard, (req, res) => { 8 | res.sendStatus(200); 9 | }); 10 | 11 | router.post("/", flashcardController.createCard, (req, res) => { 12 | res.status(200).json(res.locals.newCard); 13 | }); 14 | 15 | router.delete("/:card_id", flashcardController.deleteCard, (req, res) => { 16 | res.sendStatus(200); 17 | }); 18 | 19 | module.exports = router; 20 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | 4 | // Edit Card 5 | 6 | export const Header = (props) => { 7 | return ( 8 |
9 | 10 | Home 11 | 12 |

Flashcards

13 |
14 | 15 | Play 16 | 17 | 21 | Edit Cardset 22 | 23 |
24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | @import 'styles/abstracts/test.css'; 2 | @import 'styles/abstracts/variables.css'; 3 | 4 | @import 'styles/base/typography.css'; 5 | 6 | @import 'styles/components/buttons.css'; 7 | @import 'styles/components/cardlistCard.css'; 8 | @import 'styles/components/editCardControls.css'; 9 | @import 'styles/components/flashcard.css'; 10 | @import 'styles/components/flashcardControls.css'; 11 | @import 'styles/components/footer.css'; 12 | @import 'styles/components/header.css'; 13 | @import 'styles/components/textAreaInput.css'; 14 | @import 'styles/components/links.css'; 15 | 16 | @import 'styles/layouts/editCardPage.css'; 17 | @import 'styles/layouts/editCardsetPage.css'; 18 | @import 'styles/layouts/homePage.css'; 19 | @import 'styles/layouts/quizpage.css'; 20 | 21 | @import 'styles/utilities/container.css'; 22 | @import 'styles/utilities/effects.css'; 23 | -------------------------------------------------------------------------------- /server/models/flashcardModel.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require("pg"); 2 | const dotenv = require("dotenv"); 3 | 4 | dotenv.config(); 5 | console.log("flashcardModel.js - process.env.PG_URI:", process.env.PG_URI); 6 | 7 | // create new pool using connection string above 8 | const pool = new Pool({ 9 | connectionString: process.env.PG_URI, 10 | }); 11 | 12 | // Schema for flashcards is available here: https://drawsql.app/teams/gacetta/diagrams/flashcards 13 | // will upload image into this repo when it is solidified 14 | 15 | // export an object that contains a property called query which is a function 16 | // It returns the result of pool.query() after logging the query to the SERVER console. 17 | // THIS IS THE ACCESS POINT TO THE DATABASE 18 | module.exports = { 19 | query: (text, params, callback) => { 20 | // console.log("executed query: ", text); 21 | return pool.query(text, params, callback); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/styles/components/header.css: -------------------------------------------------------------------------------- 1 | .header { 2 | width: 100vw; 3 | height: 6rem; 4 | display: flex; 5 | justify-content: space-between; 6 | align-items: center; 7 | padding: 2rem; 8 | margin-bottom: 5rem; 9 | background-color: var(--color-primary-dark); 10 | color: var(--color-gray-light); 11 | box-shadow: 0px -51px 43px 45px rgba(0,0,0,1); 12 | } 13 | 14 | .navElement { 15 | text-decoration: none; 16 | width: 30rem; 17 | color: var(--color-gray-light); 18 | } 19 | 20 | .navHome { 21 | justify-self: flex-start; 22 | } 23 | 24 | .navHome, .navQuiz, .navEditCardset { 25 | letter-spacing: 1.2px; 26 | } 27 | 28 | .navTitle { 29 | text-transform: uppercase; 30 | justify-self: center; 31 | flex-grow: 1; 32 | font-size: 3rem; 33 | text-align: center; 34 | } 35 | 36 | .navQuiz, .navEditCardset { 37 | text-align: right; 38 | height: 100%; 39 | } 40 | 41 | .navLinkContainer { 42 | display: flex; 43 | } -------------------------------------------------------------------------------- /src/components/FlashcardControls.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | export const FlashcardControls = (props) => { 5 | return ( 6 |
7 |
8 | 11 | 14 |
15 | 21 | 22 | Edit Card 23 | 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/pages/EditCardPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TextAreaInput } from "../components"; 3 | import { EditCardControls } from "../components"; 4 | 5 | export const EditCardPage = (props) => { 6 | return ( 7 |
8 |

9 | {props.card_id ? "Edit Card" : "Create New Card"} 10 |

11 | 17 | 23 | 29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/styles/components/flashcard.css: -------------------------------------------------------------------------------- 1 | /* FLASHCARD STYLING */ 2 | .flashcard__container { 3 | width: 500px; 4 | height: 300px; 5 | perspective: 1000px; 6 | margin: 3rem; 7 | } 8 | 9 | .flashcard { 10 | position: relative; 11 | width: 500px; 12 | height: 300px; 13 | transition: transform .8s; 14 | transform-style: preserve-3d; 15 | } 16 | 17 | .flashcard__container.flip .flashcard { 18 | transform: rotateY(180deg); 19 | } 20 | 21 | .sideA, .sideB { 22 | position: absolute; 23 | width: 100%; 24 | height: 100%; 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | backface-visibility: hidden; 29 | border-radius: 5px; 30 | } 31 | 32 | .sideA { 33 | background-color: var(--color-accent); 34 | color: var(--color-white); 35 | border: 5px solid var(--color-primary-dark); 36 | } 37 | 38 | .sideB { 39 | background-color: var(--color-primary-dark); 40 | color: var(--color-gray-light); 41 | border: 5px solid var(--color-accent); 42 | transform: rotateY(180deg); 43 | } 44 | -------------------------------------------------------------------------------- /src/pages/QuizPage.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Flashcard, FlashcardControls } from "../components"; 3 | 4 | export const QuizPage = (props) => { 5 | // fringe case of navigating to /quiz while making new card 6 | if (!props.card_id) { 7 | const { sideA, sideB, card_id, cardset_id } = props.getNewCard(); 8 | } 9 | 10 | return ( 11 |
12 |

{props.cardsetName}

13 | 19 | 26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/pages/HomePage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | 4 | export const HomePage = () => { 5 | const navigate = useNavigate(); 6 | const handleCardsetClick = (e) => { 7 | e.preventDefault(); 8 | navigate("/quiz"); 9 | }; 10 | 11 | return ( 12 |
13 |

Home

14 |
15 |

Coming Soon: Login Users

16 | 20 | 24 | 25 |
26 |
27 |

Coming Soon: choose your cardset

28 | 31 |
32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/components/CardlistCard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | export const CardlistCard = (props) => { 5 | const onClickEdit = () => { 6 | props.loadSpecificCard(props.card_id); 7 | }; 8 | 9 | const onClickHandlerDeleteCard = () => { 10 | props.loadSpecificCard(props.card_id); 11 | props.deleteCard(props.card_id); 12 | }; 13 | 14 | return ( 15 |
  • 16 |
    {props.sideA}
    {" "} 17 |
    {props.sideB}
    18 |
    19 | 24 | Edit 25 | 26 | 31 | Delete 32 | 33 |
    34 |
  • 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/styles/base/typography.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Nanum+Pen+Script&family=Rock+Salt&family=Schoolbell&family=Shadows+Into+Light&display=swap'); 2 | @import url('https://fonts.googleapis.com/css2?family=Antic+Slab&family=Carter+One&family=Noto+Sans+Bassa+Vah:wght@400;500;600&display=swap'); 3 | @import url('https://fonts.googleapis.com/css2?family=Barlow+Condensed:ital,wght@0,100;0,200;0,300;0,400;0,500;1,200;1,300&family=Carter+One&display=swap'); 4 | 5 | html { 6 | font-size: 62.5%; 7 | } 8 | 9 | body { 10 | font-size: 1.6rem; 11 | font-family: 'Antic Slab', serif; 12 | /* font-family: 'Carter One', cursive; */ 13 | /* font-family: 'Barlow Condensed', sans-serif; */ 14 | /* font-family: 'Noto Sans Bassa Vah', sans-serif; */ 15 | } 16 | 17 | .sideA, .sideB, .flashcard { 18 | font-family: 'Nanum Pen Script', cursive; 19 | letter-spacing: 3px; 20 | font-size: 6rem; 21 | font-weight: 600; 22 | text-transform: uppercase; 23 | text-align: center; 24 | /* font-family: 'Shadows Into Light', cursive; */ 25 | } 26 | 27 | .heavyText { 28 | font-family: 'Carter One', cursive; 29 | } 30 | 31 | button { 32 | font-size: 1.4rem; 33 | } 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/styles/components/textAreaInput.css: -------------------------------------------------------------------------------- 1 | .textAreaInput { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: flex-start; 5 | margin-bottom: 2rem; 6 | 7 | & textarea { 8 | resize: none; 9 | padding-top: 22%; 10 | text-align: center; 11 | height: 15rem; 12 | width: 25rem; 13 | font-family: 'Nanum Pen Script', cursive; 14 | letter-spacing: .15rem; 15 | line-height: .8; 16 | font-size: 3rem; 17 | font-weight: 600; 18 | text-transform: uppercase; 19 | text-align: center; 20 | outline: none; 21 | margin-top: .5rem; 22 | } 23 | 24 | & #sideA { 25 | background-color: var(--color-accent); 26 | color: var(--color-white); 27 | border: 5px solid var(--color-primary-dark); 28 | } 29 | 30 | & #sideB { 31 | background-color: var(--color-primary-dark); 32 | color: var(--color-gray-light); 33 | border: 5px solid var(--color-accent); 34 | } 35 | 36 | & #cardsetName { 37 | border: 3px solid var(--color-primary-dark); 38 | text-transform: capitalize; 39 | font-family: 'Carter One', cursive; 40 | padding-top: 18%; 41 | font-size: 2.5rem; 42 | line-height: 1.1; 43 | letter-spacing: 0; 44 | } 45 | } -------------------------------------------------------------------------------- /src/components/EditCardControls.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | 4 | export const EditCardControls = (props) => { 5 | const navigate = useNavigate(); 6 | 7 | const onClickHandleNewCard = () => { 8 | props.clearCardData(); 9 | navigate("/cards"); 10 | }; 11 | 12 | const onClickHandleSaveCard = () => { 13 | props.saveCard(); 14 | navigate("/quiz"); 15 | }; 16 | 17 | const onClickHandleDeleteCard = () => { 18 | props.deleteCard(props.card_id); 19 | navigate(`/quiz`); 20 | }; 21 | 22 | return ( 23 |
    24 |
    25 | 28 | {props.card_id ? ( 29 | 32 | ) : null} 33 |
    34 | {props.card_id && ( 35 | 38 | )} 39 |
    40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | 4 | module.exports = { 5 | mode: process.env.NODE_ENV, 6 | entry: { 7 | src: "./src/index.js", 8 | }, 9 | output: { 10 | filename: "bundle.js", 11 | path: path.resolve(__dirname, "build"), 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.jsx?/, 17 | exclude: /node_modules/, 18 | loader: "babel-loader", 19 | options: { 20 | presets: ["@babel/env", "@babel/react"], 21 | }, 22 | }, 23 | { 24 | test: /\.css$/i, 25 | use: [ 26 | "style-loader", 27 | { 28 | loader: "css-loader", 29 | options: { 30 | importLoaders: 1, 31 | }, 32 | }, 33 | "postcss-loader", 34 | ], 35 | }, 36 | ], 37 | }, 38 | plugins: [ 39 | new HtmlWebpackPlugin({ 40 | title: "Development", 41 | template: "index.html", 42 | }), 43 | ], 44 | devServer: { 45 | static: { 46 | publicPath: "/build", 47 | directory: path.resolve(__dirname, "build"), 48 | }, 49 | port: 8080, 50 | proxy: { 51 | "/user": "http://localhost:3000", 52 | "/cardsets": "http://localhost:3000", 53 | "/cards": "http://localhost:3000", 54 | }, 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /src/styles/components/cardlistCard.css: -------------------------------------------------------------------------------- 1 | .cardlist-container { 2 | width: 75%; 3 | } 4 | 5 | .cardlistSide-container { 6 | height: 100%; 7 | width: 40%; 8 | background-color: var(--color-gray-light); 9 | padding: 1rem; 10 | display: flex; 11 | align-items: center; 12 | -webkit-text-stroke: .5px #fff; 13 | } 14 | 15 | .cardlistHeader { 16 | background: transparent; 17 | border: none; 18 | text-transform: uppercase; 19 | margin-bottom: 1rem; 20 | display: flex; 21 | justify-content: center; 22 | } 23 | 24 | .cardlistLink-container { 25 | align-items: center; 26 | } 27 | 28 | .cardlistLink { 29 | text-decoration: none; 30 | } 31 | 32 | .cardlist-card { 33 | justify-content: space-between; 34 | height: 5rem; 35 | align-items: center; 36 | margin-bottom: 2rem; 37 | padding-right: 1rem; 38 | } 39 | 40 | .cardlistSideA, 41 | .cardlistSideB { 42 | flex-grow: 1; 43 | border-radius: 2px; 44 | } 45 | 46 | .cardlistSideA { 47 | background-color: #fca311; 48 | color: #fff; 49 | border: 2px solid #14213d; 50 | } 51 | 52 | .cardlistSideB { 53 | margin: 0 1rem; 54 | background-color: #14213d; 55 | color: #e5e5e5; 56 | border: 2px solid #fca311; 57 | } 58 | 59 | .hidden { 60 | display: hidden; 61 | color: transparent; 62 | /* border: 1px magenta solid; */ 63 | } 64 | 65 | .sideA-header, 66 | .sideB-header { 67 | flex-grow: 1; 68 | padding: 1rem; 69 | font-size: 1.8rem; 70 | } 71 | 72 | .sideB-header { 73 | padding-left: 0rem; 74 | } -------------------------------------------------------------------------------- /src/styles/components/buttons.css: -------------------------------------------------------------------------------- 1 | button { 2 | width: 5rem; 3 | height: 5rem; 4 | margin: .25rem; 5 | } 6 | 7 | .create-new-card { 8 | width: 100%; 9 | } 10 | 11 | .buttonContainer { 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | } 16 | 17 | .controller-container { 18 | justify-content: center; 19 | align-items: center; 20 | } 21 | 22 | .flip-all { 23 | width: 100%; 24 | height: 100%; 25 | padding: 1rem .5rem; 26 | margin: 0; 27 | border-radius: 5px; 28 | } 29 | 30 | .a-team { 31 | background-color: #fca311; 32 | color: #fff; 33 | border: 2px solid #14213d; 34 | } 35 | 36 | .b-sides { 37 | background-color: #14213d; 38 | color: #e5e5e5; 39 | border: 2px solid #fca311; 40 | } 41 | 42 | .create-new { 43 | margin: 0; 44 | margin-right: 2rem; 45 | width: 100%; 46 | padding: 1rem .5rem; 47 | border-radius: 2px; 48 | } 49 | 50 | .incorrectGuess { 51 | color: red; 52 | font-size: 6rem; 53 | text-align: center; 54 | line-height: .9; 55 | font-family: 'Nanum Pen Script', cursive; 56 | margin-right: 2rem; 57 | } 58 | 59 | .correctGuess { 60 | color: forestgreen; 61 | font-size: 4rem; 62 | font-family: 'Nanum Pen Script', cursive; 63 | } 64 | 65 | .incorrectGuess, 66 | .correctGuess { 67 | margin-bottom: 1rem; 68 | } 69 | 70 | .loginButton { 71 | width: 100%; 72 | height: 2rem; 73 | } 74 | 75 | .cardsetButton { 76 | width: 15rem; 77 | } 78 | 79 | .create-new-card { 80 | margin: 0; 81 | margin-top: 1rem; 82 | height: 2.5rem; 83 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## QUIZCARDS 2 | 3 | An educational flashcard application to explore web development technologies and concepts. 4 | 5 | --- 6 | 7 | Demo: 8 | 9 | https://github.com/gacetta/Quizcards-Educational-App/assets/78240758/2cc99c0d-3dd7-47d3-8667-647a3a3b49df 10 | 11 | 12 | --- 13 | 14 | To run in a local development environment, run: 15 | 16 | ``` 17 | npm install 18 | npm run dev 19 | ``` 20 | 21 | --- 22 | 23 | To use application: 24 | 25 | ### HOME 26 | 27 | Select cardset to study. At the moment, only National Capitals is available. 28 | 29 | ### QUIZ 30 | 31 | The study/quiz is self-scoring. Click on the card to show the flip side 32 | 33 | - `x` - incorrect guess, keep the card in the pile to appear again 34 | - `✓` - correct guess, remove the card from the pile not to appear again 35 | - `show the B-Sides` (or `show the A-Team`) - toggle which side of the card to quiz from 36 | - `edit card` - takes you to card edit page 37 | 38 | ### EDIT CARDSET 39 | 40 | View the cardset name and a list of all cards. 41 | 42 | - Cardset name can be changed by selecting and replacing the text with desired updated name. Name will automatically be updated and saved. 43 | - Each individual card can be deleted or edited 44 | - Create a new card by clicking the button 45 | 46 | --- 47 | 48 | Technologies and concepts explored: 49 | 50 | - Full-stack web development using: 51 | - JavaScript, React (functional components), hooks (useEffect, useState) 52 | - React Router 53 | - Node.js, Express.js 54 | - SQL 55 | - [Data Modeling](https://drawsql.app/teams/gacetta/diagrams/flashcards) 56 | 57 | --- 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cs-solo-project", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "webpack.config.js", 6 | "scripts": { 7 | "start": "NODE_ENV=production node server/server.js", 8 | "build": "webpack", 9 | "build:css": "postcss src/styles.css --dir build --watch", 10 | "dev": "NODE_ENV=development nodemon server/server.js & NODE_ENV=development webpack serve", 11 | "server": "nodemon ./server/server.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/gacetta/CS-solo-project.git" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/gacetta/CS-solo-project/issues" 21 | }, 22 | "homepage": "https://github.com/gacetta/CS-solo-project#readme", 23 | "dependencies": { 24 | "cors": "^2.8.5", 25 | "dotenv": "^16.3.1", 26 | "express": "^4.18.2", 27 | "pg": "^8.9.0", 28 | "react": "^18.2.0", 29 | "react-dom": "^18.2.0", 30 | "react-router-dom": "^6.8.2", 31 | "webpack": "^5.75.0" 32 | }, 33 | "devDependencies": { 34 | "@babel/preset-env": "^7.20.2", 35 | "@babel/preset-react": "^7.18.6", 36 | "autoprefixer": "^10.4.13", 37 | "babel-loader": "^9.1.2", 38 | "css-loader": "^6.7.3", 39 | "cssnano": "^5.1.15", 40 | "html-webpack-plugin": "^5.5.0", 41 | "nodemon": "^2.0.20", 42 | "postcss": "^8.4.21", 43 | "postcss-cli": "^10.1.0", 44 | "postcss-import": "^15.1.0", 45 | "postcss-loader": "^7.0.2", 46 | "postcss-preset-env": "^8.0.1", 47 | "style-loader": "^3.3.1", 48 | "webpack-cli": "^5.0.1", 49 | "webpack-dev-server": "^4.11.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const express = require("express"); 3 | const userRouter = require("./routes/userRouter"); 4 | const cardsRouter = require("./routes/cardsRouter"); 5 | const cardsetsRouter = require("./routes/cardsetsRouter"); 6 | const cors = require("cors"); 7 | 8 | // setup app and port 9 | const app = express(); 10 | const PORT = process.env.PORT || 3000; 11 | 12 | // handle parsing request body 13 | app.use(express.json()); 14 | app.use(express.urlencoded({ extended: true })); 15 | 16 | // enable ALL CORS requests 17 | app.use(cors()); 18 | 19 | // handle requests for static files (bundle.js) 20 | app.use("/build", express.static(path.resolve(__dirname, "../build"))); 21 | 22 | // define route handlers 23 | app.use("/user", (req, res, next) => { 24 | userRouter(req, res, next); 25 | }); 26 | 27 | app.use("/cards", (req, res, next) => { 28 | cardsRouter(req, res, next); 29 | }); 30 | 31 | app.use("/cardsets", (req, res, next) => { 32 | cardsetsRouter(req, res, next); 33 | }); 34 | 35 | app.get("/", (req, res) => { 36 | res.status(200).sendFile(path.resolve(__dirname, "../index.html")); 37 | }); 38 | 39 | // define catch-all route handler for requests to an unknown route 40 | app.use((req, res) => res.status(404).send("No page found at that location")); 41 | 42 | // global error handler 43 | app.use((err, req, res, next) => { 44 | const defaultErr = { 45 | log: "Express error handler caught unknown middleware error", 46 | status: 500, 47 | message: { err: "An error occurred" }, 48 | }; 49 | const errorObj = Object.assign({}, defaultErr, err); 50 | console.log(errorObj.log); 51 | return res.status(errorObj.status).json(errorObj.message); 52 | }); 53 | 54 | // start server 55 | app.listen(PORT, () => console.log(`Server listening on port ${PORT}...`)); 56 | -------------------------------------------------------------------------------- /src/pages/EditCardsetPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link, useNavigate } from "react-router-dom"; 3 | import { TextAreaInput, CardlistCard } from "../components"; 4 | 5 | /** 6 | * 12 | */ 13 | 14 | export const EditCardsetPage = (props) => { 15 | const navigate = useNavigate(); 16 | 17 | const onClickHandler = () => { 18 | props.clearCardData(); 19 | navigate("/cards/"); 20 | }; 21 | 22 | return ( 23 |
    24 |

    Edit Cardset

    25 | 31 |
    32 |

    Card list:

    33 |
      34 |
    • 35 |

      Side A

      36 |

      Side B

      37 |
      Delete
      38 |
    • 39 | {props.entireArr.map((card) => { 40 | return ( 41 | 50 | ); 51 | })} 52 |
    • 53 | 56 |
      Delete
      57 |
    • 58 |
    59 |
    60 |
    61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /server/controllers/flashcardController.js: -------------------------------------------------------------------------------- 1 | const db = require("../models/flashcardModel"); 2 | 3 | const flashcardController = {}; 4 | 5 | flashcardController.getUsers = (req, res, next) => { 6 | // query: select all data from users table 7 | const querySelector = ` 8 | SELECT * 9 | FROM users; 10 | `; 11 | 12 | // make a request to DB 13 | db.query(querySelector, (err, result) => { 14 | // error handler 15 | if (err) { 16 | return next({ 17 | log: "flashcardController.getUsers caught unknown error", 18 | status: 500, 19 | message: { err }, 20 | }); 21 | } 22 | 23 | // console.log("db query result: ", result.rows); 24 | res.locals.users = result.rows; 25 | return next(); 26 | }); 27 | }; 28 | 29 | flashcardController.getCards = (req, res, next) => { 30 | // query: select all cards from cardset_id 31 | const querySelector = ` 32 | SELECT * 33 | FROM cards 34 | WHERE cardset_id='${req.params.cardset_id}' 35 | `; 36 | 37 | // make a request to DB 38 | db.query(querySelector, (err, result) => { 39 | // error handler 40 | if (err) { 41 | return next({ 42 | log: "flashcardController.getCards caught unknown error", 43 | status: 500, 44 | message: { err }, 45 | }); 46 | } 47 | 48 | // console.log("db query result: ", result.rows); 49 | res.locals.cards = result.rows; 50 | return next(); 51 | }); 52 | }; 53 | 54 | flashcardController.updateCard = (req, res, next) => { 55 | // grab data from req.body to update card in query 56 | const { sidea, sideb, cardset_id } = req.body; 57 | 58 | // query: update card WHERE card_id === req.params.card_id 59 | const querySelector = ` 60 | UPDATE cards 61 | SET sidea = '${sidea}', sideb = '${sideb}' 62 | WHERE card_id='${req.params.card_id}' 63 | `; 64 | 65 | // make a request to DB 66 | db.query(querySelector, (err, result) => { 67 | // error handler 68 | if (err) { 69 | return next({ 70 | log: "flashcardController.updateCard caught unknown error", 71 | status: 500, 72 | message: { err }, 73 | }); 74 | } 75 | 76 | return next(); 77 | }); 78 | }; 79 | 80 | flashcardController.createCard = (req, res, next) => { 81 | // grab data from req.body to create card in query 82 | const { sidea, sideb, cardset_id } = req.body; 83 | 84 | // query: create new card 85 | const querySelector = ` 86 | INSERT INTO cards (sidea, sideb, cardset_id) 87 | VALUES ('${sidea}', '${sideb}', ${cardset_id}) 88 | RETURNING card_id, sidea, sideb, cardset_id; 89 | `; 90 | 91 | // make a request to DB 92 | db.query(querySelector, (err, result) => { 93 | // error handler 94 | if (err) { 95 | return next({ 96 | log: "flashcardController.createCard caught unknown error", 97 | status: 500, 98 | message: { err }, 99 | }); 100 | } 101 | 102 | res.locals.newCard = result.rows[0]; 103 | return next(); 104 | }); 105 | }; 106 | 107 | flashcardController.deleteCard = (req, res, next) => { 108 | // query: delete card matching card_id 109 | const querySelector = ` 110 | DELETE FROM cards 111 | WHERE card_id=${req.params.card_id} 112 | `; 113 | 114 | // make a request to DB 115 | db.query(querySelector, (err, result) => { 116 | // error handler 117 | if (err) { 118 | return next({ 119 | log: "flashcardController.deleteCard caught unknown error", 120 | status: 500, 121 | message: { err }, 122 | }); 123 | } 124 | 125 | return next(); 126 | }); 127 | }; 128 | 129 | // export flashcardController 130 | module.exports = flashcardController; 131 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { 3 | BrowserRouter as Router, 4 | Route, 5 | Routes, 6 | useNavigate, 7 | } from "react-router-dom"; 8 | import { HomePage, EditCardPage, EditCardsetPage, QuizPage } from "../pages"; 9 | import { Header } from "./Header"; 10 | import { Footer } from "./Footer"; 11 | 12 | export const App = () => { 13 | //------------------// 14 | // STATE // 15 | //------------------// 16 | // cardset info 17 | const [cardset_id, setCardset_id] = useState(1); 18 | const [cardsetName, setCardsetName] = useState("national capitals"); 19 | 20 | // array of cards 21 | const [entireArr, setEntireArr] = useState([]); 22 | const [cardArr, setCardArr] = useState([]); 23 | 24 | // flip cards state 25 | const [flipAllCards, setFlipAllCards] = useState(false); 26 | const [flipped, setFlipped] = useState(false); 27 | 28 | // individual card state 29 | const [sideA, setSideA] = useState(""); 30 | const [sideB, setSideB] = useState(""); 31 | const [card_id, setCard_id] = useState(""); 32 | const [creatingNewCard, setCreatingNewCard] = useState(false); 33 | 34 | //------------------// 35 | // INITIALIZE CARDS // 36 | //------------------// 37 | useEffect(() => { 38 | let responseClone; 39 | fetch(`/cardsets/${cardset_id}`) 40 | .then((res) => { 41 | responseClone = res.clone(); 42 | return res.json(); 43 | }) 44 | .then((data) => { 45 | // load cards into state 46 | setCardArr(data); 47 | setEntireArr(data); 48 | 49 | // initialize card state with random card 50 | const { 51 | sidea, 52 | sideb, 53 | card_id: cardID, 54 | } = data[Math.floor(Math.random() * data.length)]; 55 | 56 | setSideA(sidea); 57 | setSideB(sideb); 58 | setCard_id(cardID); 59 | }) 60 | .catch((err) => { 61 | console.log("fetch error: ", err, responseClone); 62 | responseClone.text().then((body) => { 63 | console.log("response (not valid JSON):", body); 64 | }); 65 | }); 66 | }, []); 67 | 68 | //------------------// 69 | // HANDLERS // 70 | //------------------// 71 | function getNewCard() { 72 | // edge case for 1 or less cards 73 | if ((cardArr.length <= 1) & card_id) return; 74 | 75 | // get new random card from cardArr (new card) 76 | let newCard; 77 | let newCard_id = card_id; 78 | while (newCard_id === card_id) { 79 | const randomCardIndex = Math.floor(Math.random() * cardArr.length); 80 | newCard = cardArr[randomCardIndex]; 81 | newCard_id = newCard.card_id; 82 | } 83 | 84 | // update state with new card 85 | setSideA(newCard.sidea); 86 | setSideB(newCard.sideb); 87 | setCard_id(newCard.card_id); 88 | 89 | return { 90 | sideA: newCard.sidea, 91 | sideB: newCard.sideb, 92 | card_id: newCard.card_id, 93 | cardset_id, 94 | }; 95 | } 96 | 97 | function loadSpecificCard(card_id) { 98 | const specificCard = entireArr.find((card) => card.card_id === card_id); 99 | setSideA(specificCard.sidea); 100 | setSideB(specificCard.sideb); 101 | setCard_id(specificCard.card_id); 102 | } 103 | 104 | function handleCorrectGuess() { 105 | // edge case for array of 1 or less cards 106 | if (cardArr.length <= 1) return; 107 | 108 | // remove currentCard from cardArr and update state 109 | const newArr = cardArr.filter((card) => { 110 | return card.card_id !== card_id; 111 | }); 112 | setCardArr(newArr); 113 | 114 | // revert card to show preferred side 115 | // setTimeout to wait until card is flipped to get new card 116 | if (flipped !== flipAllCards) setTimeout(getNewCard, 250); 117 | else getNewCard(); 118 | setFlipped(flipAllCards); 119 | } 120 | 121 | function handleIncorrectGuess() { 122 | // revert card to show preferred side 123 | // setTimeout to wait until card is flipped to get new card 124 | if (flipped !== flipAllCards) setTimeout(getNewCard, 250); 125 | else getNewCard(); 126 | setFlipped(flipAllCards); 127 | } 128 | 129 | function onChangeHandlerSideA(e) { 130 | setSideA(e.target.value); 131 | } 132 | 133 | function onChangeHandlerSideB(e) { 134 | setSideB(e.target.value); 135 | } 136 | 137 | function onChangeHandlerCardsetName(e) { 138 | setCardsetName(e.target.value); 139 | } 140 | 141 | function toggleCreatingNewCard() { 142 | setCreatingNewCard(!creatingNewCard); 143 | } 144 | 145 | function toggleFlip() { 146 | setFlipped(!flipped); 147 | } 148 | 149 | function toggleFlipAllCards() { 150 | setTimeout(getNewCard, 250); 151 | setFlipAllCards(!flipAllCards); 152 | toggleFlip(); 153 | setFlipped(!flipAllCards); 154 | } 155 | 156 | function saveCard() { 157 | // create card object with sideA, sideB and cardID 158 | const updating = card_id ? true : false; 159 | const newCard = { sidea: sideA, sideb: sideB, cardset_id }; 160 | const alertMsg = `Card ${updating ? "updated" : "created"} successfully`; 161 | 162 | if (updating) { 163 | newCard.card_id = card_id; 164 | updateCard(newCard); 165 | } else { 166 | createCard(newCard); 167 | } 168 | 169 | alert(alertMsg); 170 | setFlipped(flipAllCards); 171 | getNewCard(); 172 | } 173 | 174 | function updateCard(newCard) { 175 | // DB Update record with PUT request to 'cards/card_id' 176 | fetch(`/cards/${card_id}`, { 177 | method: "PUT", 178 | headers: { "Content-Type": "application/json" }, 179 | body: JSON.stringify(newCard), 180 | }) 181 | .then(() => { 182 | // update currentCardArr - for quiz 183 | const filteredCardArr = cardArr.filter( 184 | (card) => newCard.card_id !== card.card_id 185 | ); 186 | setCardArr([...filteredCardArr, newCard]); 187 | 188 | // update entireArr - for edit cardset 189 | const filteredEntireArr = entireArr.filter( 190 | (card) => newCard.card_id !== card.card_id 191 | ); 192 | setEntireArr([...filteredEntireArr, newCard]); 193 | setFlipped(flipAllCards); 194 | }) 195 | .catch((err) => { 196 | console.log("err: ", err); 197 | }); 198 | } 199 | 200 | function createCard(newCard) { 201 | // DB CREATE record with POST request to '/cards' 202 | fetch(`/cards/`, { 203 | method: "POST", 204 | headers: { "Content-Type": "application/json" }, 205 | body: JSON.stringify(newCard), 206 | }) 207 | .then((response) => response.json()) 208 | .then((newCardResponse) => { 209 | setCardArr([...cardArr, newCardResponse]); 210 | setEntireArr([...entireArr, newCardResponse]); 211 | setFlipped(flipAllCards); 212 | }) 213 | .catch((err) => { 214 | console.log("err: ", err); 215 | }); 216 | } 217 | 218 | function deleteCard(card_id) { 219 | if (!card_id) return; 220 | 221 | // DB Update record with PUT request to 'cards/card_id' 222 | fetch(`/cards/${card_id}`, { 223 | method: "DELETE", 224 | }).catch((err) => { 225 | console.log("err: ", err); 226 | }); 227 | 228 | alert("Card deleted successfully"); 229 | 230 | const newCardArr = cardArr.filter((card) => card.card_id !== card_id); 231 | const newEntireArr = entireArr.filter((card) => card.card_id !== card_id); 232 | setCardArr(newCardArr); 233 | setEntireArr(newEntireArr); 234 | 235 | clearCardData(); 236 | setFlipped(flipAllCards); 237 | getNewCard(); 238 | } 239 | 240 | function clearCardData() { 241 | setSideA(""); 242 | setSideB(""); 243 | setCard_id(null); 244 | } 245 | 246 | return ( 247 | 248 |
    249 | 250 | } /> 251 | 267 | } 268 | /> 269 | 284 | } 285 | /> 286 | 299 | } 300 | /> 301 | 316 | } 317 | /> 318 | 319 |