<repo_name>bpeskett/flashcards<file_sep>/src/Layout/Study/Study.js
import React, { useEffect, useState } from "react";
import { useHistory } from "react-router";
import NotEnoughCards from "../Cards/NotEnoughCards";

export default function Study({ deck, cards }) {
  const history = useHistory();
  const [cardNumber, setCardNumber] = useState(0); //used to decide which card to display from cards array
  const enoughCards = cards.length > 2; //boolean used to determine if there are enough cards to study (there must be at least 3)
  const [cardSide, setCardSide] = useState("front"); //front or back. always start with front.
  const [nextButtonDisplay, setNextButtonDisplay] = useState(""); //next button is hidden when cardSide === 'front'
  const [shuffledCards, setShuffledCards] = useState([...cards]);
  const [card, setCard] = useState(cards[0]); //current card object to be displayed
  const [cardClass, setCardClass] = useState("text-dark bg-light");

  //on first render, shuffle the cards
  useEffect(() => {
    setShuffledCards(shuffle(cards))
    // eslint-disable-next-line react-hooks/exhaustive-deps
  },[])
  //when cards are shuffled, set first card to first shuffled card
  useEffect(() => {
    setCard(shuffledCards[0]);
  },[shuffledCards])

  //function to make a copy of the cards array and shuffle it
  function shuffle(cards) {
      const cardsToShuffle = [...cards];
      for (let i = cardsToShuffle.length - 1; i > 0; i--){
        const j = Math.floor(Math.random() * (i + 1));
        [cardsToShuffle[i], cardsToShuffle[j]] = [cardsToShuffle[j], cardsToShuffle[i]];
      }
      return cardsToShuffle
  }



  function nextHandler() {
    //handles clicking next
    if (cardNumber + 1 < cards.length) {
      //what to do if the current card if not the last card
      setNextButtonDisplay(""); //hides the next button
      setCardSide("front"); //switches the next card to front
      setCard(shuffledCards[cardNumber + 1]); //updates the 'card _ of _' number
      setCardNumber(cardNumber + 1); //moves to the next card
      setCardClass("text-dark bg-light");
    } else {
      //what to do if the deck is finished
      const message =
        "Restart cards? \n \n Clisck 'cancel' to return to the home page."; //displays message
      if (window.confirm(message)) {
        //if message is confirmed, reset the deck
        setCardSide("front");
        setCardNumber(0);
        setShuffledCards(shuffle(cards));
        setCard(shuffledCards[0]);
        setNextButtonDisplay("");
        setCardClass("text-dark bg-light");
      } else {
        //if pushed cancel, go home.
        history.push("/");
      }
    }
  }

  function flipHandler() {
    //handles flip button
    if (cardSide === "back") {
      //if previous state of the card was the backside...
      setCardSide("front"); //switch to front
      setCardClass("text-dark bg-light");
      setNextButtonDisplay(""); //hide the next button
    } else {
      //if previous state of the card was the frontside...
      setCardSide("back"); //switch to backside
      setCardClass("text-white bg-dark");
      setNextButtonDisplay(
        //display the next  button
        <button onClick={nextHandler} className="btn btn-primary mx-2">
          Next
          <span className="oi oi-chevron-right ml-2" />
        </button>
      );
    }
  }

const progressbarStyles = {
  display: 'flex',
  height: '.5rem',
  overflow: 'hidden',
  margin: '-1px'
}


  return enoughCards ? (
    <>
      <h2>Study: {deck.name}</h2>
      
      
      



      <div
        className={`card mb-3 ${cardClass}`}
        style={{ maxWidth: "40rem", minHeight: "14rem" }}
      >

<div style = {progressbarStyles}>
                  <div 
                    className="progress-bar  bg-success" 
                    role="progressbar" 
                    style={{width: `${(((cardNumber) / (cards.length )) + (1 / cards.length) ) * 100}%`}} 
                    aria-valuenow={(((cardNumber) / (cards.length )) + (1 / cards.length) ) * 100}
                    aria-valuemin="0"
                    aria-valuemax="100"
                  >
                  </div>
                </div>


        <div className="card-header">{`Card ${cardNumber + 1} of ${
          cards.length
        }`}
        
        </div>
        <div className="card-body">
          <p className="card-text">{card[cardSide]} </p>
        </div>
      </div>
      <button onClick={flipHandler} className="btn btn-secondary">
        <span className="oi oi-loop-circular mr-2" />
        Flip
      </button>
      {/* next button ony displays when the back of a card is showing */}
      {nextButtonDisplay}
    </>
  ) : (
    /* display NotEnoughCards component if the deck has less than 3 cards */
    <NotEnoughCards deck={deck} />
  );
}
<file_sep>/src/Layout/Cards/CardCard.js
import React from "react";
import { Link } from "react-router-dom";
import { deleteCard } from "../../utils/api";

//individual card componant displaying both sides and edit/ delete buttons (not what is shown when studying)
export default function CardCard({ card, deckUpdated }) {

  //trunkate card text if longer than 150 characters.
  let trunkatedFront = card.front;
  let trunkatedBack = card.back;
  if (card.front.length > 150) {
    trunkatedFront = card.front.slice(0, 150) + "...";
  }
  if (card.back.length > 150) {
    trunkatedBack = card.back.slice(0, 150) + "...";
  }

  //confirm deletion than go to the deck view and refresh
  const deleteHandler = () => {
    const message =
      "Delete this card? \n \n You will not be able to recover it.";
    if (window.confirm(message)) {
      deleteCard(card.id).then(() => {
        deckUpdated();
      });
    }
  };

  return (
    <>
      <div className="my-3 col col-12 col-md-6 ">
        {/*front of card*/}
        <div
          className="card text-dark bg-light mb-1"
          style={{ minHeight: "11rem" }}
        >
          <div className="card-header">Front</div>
          <div className="card-body ">
            <p className="card-text ">{trunkatedFront} </p>
          </div>
        </div>
        {/*back of card*/}
        <div className="card text-white bg-dark" style={{ minHeight: "11rem" }}>
          <div className="card-header">Back</div>
          <div className="card-body">
            <p className="card-text">{trunkatedBack} </p>
          </div>
        </div>
        <div className="col mt-2">
          {/*delete button*/}
          <button
            to="/"
            className="btn btn-danger mx-2 float-right"
            onClick={deleteHandler}
          >
            <span className="oi oi-trash ml-1" />
          </button>
          {/*edit button*/}
          <Link
            to={`/decks/${card.deckId}/cards/${card.id}/edit`}
            className="btn btn-secondary float-right"
          >
            <span className="oi oi-pencil mr-2" />
            Edit
          </Link>
        </div>
      </div>
    </>
  );
}
<file_sep>/src/Layout/Decks/DeckList.js
import React, { useEffect, useState } from "react";
import DeckCard from "./DeckCard";

//list of all the decks (as shown on the home page)
export default function DeckList({ deckList, deckView, deckListUpdated }) {
  const [listAllDecks, setListAllDecks] = useState("");
  //map the deckList into DeckCard components wrapped in bootstrap columns.
  useEffect(() => {
    setListAllDecks(
      deckList.map((deck, index) => {
        return (
          <div key={index} className="col col-12 col-lg-6 col-xl-4">
            <DeckCard deck={deck} deckView={deckView} deckListUpdated={deckListUpdated} />
            
          </div>
        );
      })
    );
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [deckList, deckView]);

  //render list of decks wrapped in a bootstrap row
  return (
    <>
      <h2>Decks</h2>
      <div className="row">{listAllDecks}</div>
    </>
  );
}
<file_sep>/src/Layout/Decks/DeckForm.js
import React, { useEffect, useState } from "react";
import { Link, useHistory } from "react-router-dom";
import { createDeck, updateDeck } from "../../utils/api";
import SubmitAlert from "../Common/SubmitAlert";

//form for creating and editing decks
//props: deck being edited (if editing) formType: (edit or create) depending on how the form is being used
export default function DeckForm({ deck, formType, deckUpdated }) {
  //name and description of the deck (being created or edited)
  const [name, setName] = useState("");
  const [description, setDescription] = useState("");
  const [alertType, setAlertType] = useState("");
  const history = useHistory();

  //if editing a deck, set the state variables to the name and description of the deck being edited
  useEffect(() => {
    if (formType === "Edit") {
      setName(deck.name);
      setDescription(deck.description);
    }
  }, [deck, formType]);

  //submit handler
  const handleSubmit = (event) => {
    event.preventDefault();
    setAlertType("warning");
    //create a new deck object using form values
    const newDeck = { name: name, description: description };
    //update the or create the new deck
    if (formType === "Edit") {
      const updatedDeck = { ...newDeck, id: deck.id };
      updateDeck(updatedDeck).then((result) => {
        history.push(`/decks/${result.id}/success`);
        deckUpdated();
      })
      .catch(() => {
        setAlertType("danger");
      });
    } else {
      createDeck(newDeck).then((result) => {
        history.push(`/decks/${result.id}/success`);
      })
      .catch(() => {
        setAlertType("danger");
      });
    }
  };

  return (
    <>
      <h2>{formType} Deck</h2>
      <form onSubmit={handleSubmit}>
        {/*name input*/}
        <div className="form-group">
          <label htmlFor="name">Name</label>
          <input
            className="form-control"
            id="name"
            type="text"
            name="name"
            onChange={(event) => {
              setName(event.target.value);
            }}
            value={name}
            maxLength="50"
          />
        </div>
        {/*description input*/}
        <div className="form-group">
          <label htmlFor="description">Description</label>
          <textarea
            className="form-control"
            id="description"
            rows="3"
            name="description"
            onChange={(event) => {
              setDescription(event.target.value);
            }}
            value={description}
          ></textarea>
        </div>
        {/*cancel button*/}
        <Link className="btn btn-secondary mx-1 mb-3" to="/">
          <span className="oi oi-circle-x mr-2" />
          Cancel
        </Link>
        {/*submit button*/}
        <button className="btn btn-primary mx-1 mb-3" type="submit">
          <span className="oi oi-circle-check mr-2" />
          Submit
        </button>
        <SubmitAlert form="deck" alertType={alertType} />
      </form>
    </>
  );
}
<file_sep>/src/Layout/Common/Loading.js
import React from "react";

//spinning wheel and "Loading..." displays on pages while waiting for api fetch
export default function Loading() {
  return (
    <div>
      <span className="spinner-border float-left mt-1 mr-3"></span>{" "}
      <h2 className="mt-1">Loading...</h2>
    </div>
  );
}
<file_sep>/README.md
![Flashcard-o-matic](https://raw.githubusercontent.com/micah-patrick/flashcards/main/src/readme/readme-header.png "Flashcard-o-matic")

#
# Flashcard-o-matic

Thinkful student project built using React and Bootstrap. Users can create decks and cards, then study them as flashcards. Both decks and cards can be edited or deleted. Additional cards can be added to a deck.

#
## SKILLS USED
* React 
* Javascript
* HTML / JSX 
* CSS
* Bootstrap
* React Router
* React Hooks (useState, useEffect)

#
## FEATURES BEYOND THE SCOPE OF THE PROJECT

I went beyond the scope of the project and added the following features

* Fully responsive
* Flashcards shuffle when studying
    * shuffle algorithm uses a runtime of O(n)
* Error handling that gives feedback to user with alerts for
    * Saving...
    * Loading...
    * Your `[deck, card]` is saved!
    * Unable to save your `[deck, card]`.
* Status bar showing percentage of cards a user has studied
* Truncation for long names and descriptions

#
## FUTURE FEATURES

Some features that I would like to impliment in the future

* Get server up and running
* Grading. Track right/ wrong answers while studying
* Add different types of decks / cards (multiple choice, etc)
* Double sided deck option
* Update favicon
#
## SCREENSHOTS

### Home Page:
The home page displays a card for each deck as well as a button to create a new deck.

`path = '/'`
![Home Page](https://raw.githubusercontent.com/micah-patrick/flashcards/main/src/readme/scrn-sht-home.png)


### Study:
Study a deck. Flip to reveal the back of a card.

`path = '/decks/:deckId/study'`
![Study Page](https://raw.githubusercontent.com/micah-patrick/flashcards/main/src/readme/scrn-sht-study.png)


### Deck View:
Viewing a deck reveals the deck's full description, all it's associated cards and options to study, edit, or delete the deck, and add to, edit or delete the cards.

`path = '/decks/:deckId'`
![Deck View](https://raw.githubusercontent.com/micah-patrick/flashcards/main/src/readme/scrn-sht-deck.png)<file_sep>/src/Layout/Decks/DeckCard.js
import React from "react";
import { Link, useHistory } from "react-router-dom";
import { deleteDeck } from "../../utils/api";

//individual deck card as seen on home page and '/decks/:deckId' route (deck view)
//deck prop passes the deck object. the deckView prop (boolean) tells if this component is being used in the deck view route or not ('/decks/:deckId')
export default function DeckCard({ deck, deckView, deckListUpdated }) {
  const history = useHistory();

  //trunkate the deck's description if being displayed on home screen and if the description is long
  let trunkatedDescription = deck.description;
  if (!deckView & (deck.description.length > 115)) {
    trunkatedDescription = deck.description.slice(0, 115) + "...";
  }

  // some buttons should only be displayed in some cases
  const deckViewButton = deckView ? (
    ""
  ) : (
    <Link to={`/decks/${deck.id}`} className="btn btn-sm mb-1 btn-secondary mx-1">
      <span className="oi oi-eye mr-2" />
      View
    </Link>
  );

  const deckEditButton = deckView ? (
    <Link
      to={`/decks/${deck.id}/edit`}
      className="btn btn-sm mb-1 btn-secondary mx-1 float-right"
    >
      <span className="oi oi-pencil mr-2" />
      Edit
    </Link>
  ) : (
    ""
  );

  //delete deck, go to root directory and refresh
  const deleteHandler = () => {
    const message =
      "Delete this deck? \n \n You will not be able to recover it.";
    if (window.confirm(message)) {
      deleteDeck(deck.id).then(() => {
        history.push("/");
        !deckView && deckListUpdated();
      });
    }
  };

  //card count should not be plural if there is only one card
  const cardCount =
    deck.cards.length === 1 ? `1 Card` : `${deck.cards.length} cards`;

  return (
    <>
      <div className="card mb-3">
        <div className="card-header bg-dark text-white pb-0">
          {/* number of cards in the deck */}
          <span className="float-right badge bg-success text-white ml-3">
            {cardCount}
          </span>
          {/* name of the deck */}
          <h5 className="card-title text-truncate pb-2 my-0">{deck.name}</h5>
        </div>
        <div className="card-body row">



          <div className="col col-12">
            {/* deck description */}
            <p className="" style={{ minHeight: "5rem" }}>
              {trunkatedDescription}{" "}
            </p>
          </div>                    


          <div className="col">
            {/* view button */}
            {deckViewButton}
            {/* study button */}
            <Link to={`/decks/${deck.id}/study`} className="btn btn-sm mb-1 btn-primary mx-1">
              <span className="oi oi-book  mr-2" />
              Study
            </Link>

            {/* delete button */}
            <button
              className="btn btn-sm mb-1 btn-danger mx-1 float-right"
              onClick={deleteHandler}
              >
              <span className="oi oi-trash ml-1" />
            </button>
            {/* edit button */}
            {deckEditButton}
          </div>                      



        </div>
      </div>
    </>
  );
}
<file_sep>/src/Layout/Decks/Deck.js
import React from "react";
import DeckCard from "./DeckCard";
import CardList from "../Cards/CardList";

// 'decks/:deckId' route. display the deck details and a list of cards.
export default function Deck({ deck, cards, deckUpdated }) {
  return (
    <>
      <DeckCard deck={deck} deckView={true} />
      <CardList deck={deck} cards={cards} deckUpdated={deckUpdated} />
    </>
  );
}
<file_sep>/src/Layout/Cards/NotEnoughCards.js
import React from "react";
import { Link } from "react-router-dom";
import CardList from "./CardList";

//component displays when trying to study a deck with less than 3 cards
export default function NotEnoughCards({ deck }) {
  const notEnoughMessage =
    deck.cards.length === 1 ? "is 1 card" : `are ${deck.cards.length} cards`;

  return (
    <>
      <div className="alert alert-warning" role="alert">
        Not enough cards. You need at least 3 cards to study. There{" "}
        {notEnoughMessage} in this deck.
      </div>
      <h2>Study: {deck.name}</h2>

      <br />

      <Link
        className="btn btn-primary mb-3 btn"
        to={`/decks/${deck.id}/cards/new`}
      >
        <span className="oi oi-plus pr-2" />
        Add Cards
      </Link>
      <CardList deck={deck} cards={deck.cards} />
    </>
  );
}
<file_sep>/src/Layout/Cards/CardList.js
import React, { useEffect, useState } from "react";
import { Link, useLocation, useParams } from "react-router-dom";
import CardCard from "./CardCard";

//list all given cards. used on '/decks/:deckId' route
export default function CardList({ deck, cards, deckUpdated }) {

  const deckId = useParams().deckId;
  const addCardsBtn = (
    <Link 
      to={`/decks/${deckId}/cards/new`} 
      className="btn btn-sm mb-3 btn-success mx-1 float-right"
    >
      <span className="oi oi-plus  mr-2" />
      Add Cards
    </Link>
  );

  const [listAllCards, setListAllCards] = useState("");
  const [cardHeadDisplay, setCardHeadDisplay] = useState("");
  const [addCardsBtnDisplay, setAddCardBtnDisplay] = useState(addCardsBtn);
  const location = useLocation().pathname;
  const currentPage = location.substring(location.lastIndexOf('/') + 1)

  useEffect(() => {
    if (currentPage === "new" || currentPage === "edit" || currentPage === "study"){
      setAddCardBtnDisplay("");
    }
  },[currentPage])



  //map cards into list of Card components
  useEffect(() => {
    setListAllCards(
      cards.map((card, index) => {
        return <CardCard key={index} card={card} deckUpdated={deckUpdated} />;
      })
    );
    //only display "Cards" headline if there is at least one card in the deck
    cards.length > 0 && setCardHeadDisplay(<h3 className="mb-3">Cards</h3>);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [cards]);

  return (
    <>
     
      {addCardsBtnDisplay}
      {cardHeadDisplay}
      <div className="row">{listAllCards}</div>
    </>
  );
}
<file_sep>/src/Layout/index.js
import React from "react";
import { Route, Switch } from "react-router";
import Header from "./Header";
import Home from "./Home/Home";
import Decks from "./Decks/Decks";
import NotFound from "./NotFound";
import BreadCrumbs from "./Common/BreadCrumbs";
import DeckForm from "./Decks/DeckForm";

function Layout() {
  //bread crumbs for for new deck bread crumbs
  const newCrumbList = [{ name: "Create Deck", path: `/decks/new` }];



  return (
    <div>
      <Header />
      <div className="container pb-3">
        <Switch>
          <Route exact={true} path="/">
            {" "}
            {/* route for root directory */}
            <Home />
          </Route>
          <Route exact={true} path="/decks/new">
            {" "}
            {/* route for new deck */}
            <BreadCrumbs crumbs={newCrumbList} />
            <DeckForm formType={"Create"} />
          </Route>
          <Route path="/decks/:deckId">
            {" "}
            {/* route for any page that contains deckId */}
            <Decks />
          </Route>
          <Route>
            {" "}
            {/* route for unknown pages */}
            <NotFound />
          </Route>
        </Switch>
      </div>
    </div>
  );
}

export default Layout;
