├── .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 |
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 |
320 |
321 | );
322 | };
323 |
--------------------------------------------------------------------------------