first commit

This commit is contained in:
Dino 2023-07-02 16:09:56 +02:00
commit 38e3083a85
31 changed files with 40292 additions and 0 deletions

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
}
]
}

26
README.md Normal file
View File

@ -0,0 +1,26 @@
# To Run
Run `npm start` in the project root and the app will be available on port 3000.
# State
The app's state is totally normalized, with slices for topics, quizzes, and cards.
# Routes
- `/new-topic`  form to create a new topic
- `/topics`  index of all topics
- `/topics/:topicId`  page for an individual topic
- `/new-quiz`  form to create a new quiz
- `/quizzes`  index of all quizzes
- `/quizzes/:quizId`  page for an individual quiz
# To Test
1. Create topics
2. Create quizzes
3. Visit the page for an individual quiz and flip the cards over
# Questions
Is this appropriately scoped? Does it have too many features? Too few?

29462
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

45
package.json Normal file
View File

@ -0,0 +1,45 @@
{
"name": "quiz-app",
"version": "0.1.0",
"private": true,
"dependencies": {
"@reduxjs/toolkit": "^1.5.0",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-scripts": "^5.0.1",
"uuid": "^8.3.2",
"web-vitals": "^1.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"prettier": "2.2.1"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

218
public/index.css Normal file
View File

@ -0,0 +1,218 @@
* {
font-family: "Open Sans", sans-serif;
box-sizing: border-box;
}
html,
body {
margin: 0;
}
nav {
background: #b736ff;
display: flex;
padding: 24px 36px;
}
nav ul {
display: flex;
justify-content: space-around;
width: 100%;
padding: 0;
margin: 0;
list-style: none;
}
nav ul li {
margin-bottom: 0 !important;
}
nav a {
color: white;
font-size: 16px;
text-decoration: none;
}
nav a.active {
font-weight: bold;
}
h1 {
font-weight: bold;
font-size: 20px;
color: #000000;
margin: 0;
}
h2 {
font-weight: bold;
font-size: 16px;
color: #000000;
margin: 0;
}
button,
.button {
background: #ffffff;
border: 1px solid #03a8d8;
border-radius: 20px;
text-align: center;
font-weight: bold;
font-size: 14px;
color: #03a8d8;
padding: 10px 21px;
text-decoration: none;
}
.create-new-topic-button {
margin: 0 auto;
display: block;
width: 182px;
}
section {
padding: 20px 16px;
}
input,
select {
background: #ffffff;
border: 1px solid #979797;
border-radius: 3px;
padding: 8px 10px;
width: 100%;
margin-bottom: 16px;
}
select:invalid,
input::placeholder {
color: #979797;
opacity: 1;
}
.form-section {
padding-top: 20px;
padding-bottom: 30px;
}
.center {
display: block;
margin: 0 auto;
text-align: center;
}
section > h1 {
margin: 0;
margin-bottom: 20px;
text-align: center;
}
.actions-container {
padding-top: 16px;
display: flex;
justify-content: space-between;
}
.card-front-back {
padding-top: 25px;
}
.remove-card-button {
color: #03a8d8;
text-align: right;
font-weight: bold;
font-size: 12px;
margin-left: auto;
display: block;
border: none;
padding: unset;
margin-top: -8px;
}
option {
text-transform: capitalize;
}
.topics-list {
list-style: none;
padding: 0;
}
.topic {
background: #ffffff;
border: 1px solid #bcbcbc;
border-radius: 3px;
padding: 26px 20px;
}
.topic-container {
display: flex;
}
.topic img {
width: 40px;
margin-right: 14px;
}
.text-content > * {
margin: 0;
}
.topic-link {
text-decoration: none;
color: #000000;
}
.text-content > p {
font-size: 14px;
}
li:not(:last-child) {
margin-bottom: 20px;
}
.topic-icon {
width: 40px;
display: block;
margin: 0 auto;
}
.quizzes-list {
list-style: none;
padding: 0;
display: flex;
gap: 12px;
}
.quiz {
font-weight: bold;
font-size: 14px;
color: #000000;
padding: 25px 6px;
background: #ffffff;
border: 1px solid #bcbcbc;
border-radius: 3px;
width: 88px;
text-align: center;
margin: 0 !important;
text-decoration: none;
}
.quiz > a {
text-decoration: none;
color: #000000;
}
.cards-list {
list-style: none;
padding: 0;
}
.card {
background: #b736ff;
border-radius: 4px;
border: none;
color: white;
padding: 73px 44px;
width: 100%;
}

49
public/index.html Normal file
View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="stylesheet" href="%PUBLIC_URL%/index.css" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap"
rel="stylesheet"
/>
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

25
public/manifest.json Normal file
View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
public/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

8
src/App.test.js Normal file
View File

@ -0,0 +1,8 @@
import { render, screen } from "@testing-library/react";
import App from "./App";
test("renders learn react link", () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

90
src/app/App.js Normal file
View File

@ -0,0 +1,90 @@
import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
NavLink,
useRouteMatch,
} from "react-router-dom";
import NewQuizForm from "../components/NewQuizForm";
import NewTopicForm from "../components/NewTopicForm";
import Topics from "../features/topics/Topics";
import Topic from "../features/topics/Topic";
import Quiz from "../features/quizzes/Quiz";
import Quizzes from "../features/quizzes/Quizzes";
import ROUTES from "./routes";
export default function App() {
return (
<Router>
<nav>
<ul>
<li>
<NavLink to={ROUTES.topicsRoute()} activeClassName="active">
Topics
</NavLink>
</li>
<li>
<NavLink to={ROUTES.quizzesRoute()} activeClassName="active">
Quizzes
</NavLink>
</li>
<li>
<NavLink to={ROUTES.newQuizRoute()} activeClassName="active">
New Quiz
</NavLink>
</li>
</ul>
</nav>
<Switch>
<Route path="/topics">
<TopicsRoutes />
</Route>
<Route path="/quizzes">
<QuizRoutes />
</Route>
</Switch>
</Router>
);
}
function TopicsRoutes() {
let match = useRouteMatch();
return (
<>
<Switch>
<Route path={`${match.path}/new`}>
<NewTopicForm />
</Route>
<Route path={`${match.path}/:topicId`}>
<Topic />
</Route>
<Route path={`${match.path}`}>
<Topics />
</Route>
</Switch>
</>
);
}
function QuizRoutes() {
let match = useRouteMatch();
return (
<>
<Switch>
<Route path={`${match.path}/new`}>
<NewQuizForm />
</Route>
<Route path={`${match.path}/:quizId`}>
<Quiz />
</Route>
<Route path={`${match.path}`}>
<Quizzes />
</Route>
</Switch>
</>
);
}

10
src/app/routes.js Normal file
View File

@ -0,0 +1,10 @@
const ROUTES = {
newQuizRoute: () => "/quizzes/new",
quizRoute: (id) => `/quizzes/${id}`,
quizzesRoute: () => "/quizzes",
newTopicRoute: () => "/topics/new",
topicRoute: (id) => `/topics/${id}`,
topicsRoute: () => "/topics",
};
export default ROUTES;

7
src/app/store.js Normal file
View File

@ -0,0 +1,7 @@
import { configureStore, combineReducers } from "@reduxjs/toolkit";
import topics from "../features/topics/topicsSlice";
import quizzes from "../features/quizzes/quizzesSlice";
export default configureStore({
reducer: { topics, quizzes },
});

View File

@ -0,0 +1,113 @@
import React, { useState } from "react";
import { useHistory } from "react-router-dom";
import { v4 as uuidv4 } from "uuid";
import ROUTES from "../app/routes";
import { selectTopics } from "../features/topics/topicsSlice";
import { useDispatch, useSelector } from "react-redux";
import { addQuizThunk } from "../features/quizzes/quizzesSlice";
export default function NewQuizForm() {
const [name, setName] = useState("");
const [cards, setCards] = useState([]);
const [topicId, setTopicId] = useState("");
const history = useHistory();
const topics = useSelector(selectTopics);
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
if (name.length === 0) {
return;
}
const cardIds = [];
// create the new cards here and add each card's id to cardIds
let uniqueId = uuidv4();
dispatch(
addQuizThunk({
id: uniqueId,
name,
topicId,
cardIds,
})
);
// create the new quiz here
history.push(ROUTES.quizzesRoute());
};
const addCardInputs = (e) => {
e.preventDefault();
setCards(cards.concat({ front: "", back: "" }));
};
const removeCard = (e, index) => {
e.preventDefault();
setCards(cards.filter((card, i) => index !== i));
};
const updateCardState = (index, side, value) => {
const newCards = cards.slice();
newCards[index][side] = value;
setCards(newCards);
};
return (
<section>
<h1>Create a new quiz</h1>
<form onSubmit={handleSubmit}>
<input
id="quiz-name"
value={name}
onChange={(e) => setName(e.currentTarget.value)}
placeholder="Quiz Title"
/>
<select
id="quiz-topic"
onChange={(e) => setTopicId(e.currentTarget.value)}
placeholder="Topic"
>
<option value="">Topic</option>
{Object.values(topics).map((topic) => (
<option key={topic.id} value={topic.id}>
{topic.name}
</option>
))}
</select>
{cards.map((card, index) => (
<div key={index} className="card-front-back">
<input
id={`card-front-${index}`}
value={cards[index].front}
onChange={(e) =>
updateCardState(index, "front", e.currentTarget.value)
}
placeholder="Front"
/>
<input
id={`card-back-${index}`}
value={cards[index].back}
onChange={(e) =>
updateCardState(index, "back", e.currentTarget.value)
}
placeholder="Back"
/>
<button
onClick={(e) => removeCard(e, index)}
className="remove-card-button"
>
Remove Card
</button>
</div>
))}
<div className="actions-container">
<button onClick={addCardInputs}>Add a Card</button>
<button>Create Quiz</button>
</div>
</form>
</section>
);
}

View File

@ -0,0 +1,64 @@
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { useHistory } from "react-router-dom";
import { v4 as uuidv4 } from "uuid";
import ROUTES from "../app/routes";
import { ALL_ICONS } from "../data/icons";
import { addTopic } from "../features/topics/topicsSlice";
export default function NewTopicForm() {
const dispatch = useDispatch();
const [name, setName] = useState("");
const [icon, setIcon] = useState("");
const history = useHistory();
const handleSubmit = (e) => {
e.preventDefault();
if (name.length === 0) {
return;
}
// dispatch your add topic action her
let uniqueId = uuidv4();
dispatch(
addTopic({
id: uniqueId,
name: name,
icon: icon,
})
);
history.push(ROUTES.topicsRoute());
};
return (
<section>
<form onSubmit={handleSubmit}>
<h1 className="center">Create a new topic</h1>
<div className="form-section">
<input
id="topic-name"
type="text"
value={name}
onChange={(e) => setName(e.currentTarget.value)}
placeholder="Topic Name"
/>
<select
onChange={(e) => setIcon(e.currentTarget.value)}
required
defaultValue="default"
>
<option value="default" disabled hidden>
Choose an icon
</option>
{ALL_ICONS.map(({ name, url }) => (
<option key={url} value={url}>
{name}
</option>
))}
</select>
</div>
<button className="center">Add Topic</button>
</form>
</section>
);
}

129
src/data/icons.js Normal file
View File

@ -0,0 +1,129 @@
export const BOOK_ICON =
"https://static-assets.codecademy.com/skillpaths/react-redux/redux-quiz-app/book.svg";
export const BALLOON_ICON =
"https://static-assets.codecademy.com/skillpaths/react-redux/redux-quiz-app/balloon.svg";
export const BIRD_ICON =
"https://static-assets.codecademy.com/skillpaths/react-redux/redux-quiz-app/bird.svg";
export const CALENDAR_ICON =
"https://static-assets.codecademy.com/skillpaths/react-redux/redux-quiz-app/calendar.svg";
export const CLOVER_ICON =
"https://static-assets.codecademy.com/skillpaths/react-redux/redux-quiz-app/clover.svg";
export const CRAYONS_ICON =
"https://static-assets.codecademy.com/skillpaths/react-redux/redux-quiz-app/crayons.svg";
export const DATA_FLOW_ICON =
"https://static-assets.codecademy.com/skillpaths/react-redux/redux-quiz-app/data-flow.svg";
export const FENCE_ICON =
"https://static-assets.codecademy.com/skillpaths/react-redux/redux-quiz-app/fence.svg";
export const GRILL_ICON =
"https://static-assets.codecademy.com/skillpaths/react-redux/redux-quiz-app/grill.svg";
export const HAND_DRILL_ICON =
"https://static-assets.codecademy.com/skillpaths/react-redux/redux-quiz-app/hand-drill.svg";
export const HAT_ICON =
"https://static-assets.codecademy.com/skillpaths/react-redux/redux-quiz-app/hat.svg";
export const INTERNET_ICON =
"https://static-assets.codecademy.com/skillpaths/react-redux/redux-quiz-app/internet.svg";
export const LADYBUG_ICON =
"https://static-assets.codecademy.com/skillpaths/react-redux/redux-quiz-app/ladybug.svg";
export const LEAVES_ICON =
"https://static-assets.codecademy.com/skillpaths/react-redux/redux-quiz-app/leaves.svg";
export const MEDICINE_ICON =
"https://static-assets.codecademy.com/skillpaths/react-redux/redux-quiz-app/medicine.svg";
export const NEST_ICON =
"https://static-assets.codecademy.com/skillpaths/react-redux/redux-quiz-app/nest.svg";
export const SHUTTLECOCK_ICON =
"https://static-assets.codecademy.com/skillpaths/react-redux/redux-quiz-app/shuttlecock.svg";
export const SPADE_ICON =
"https://static-assets.codecademy.com/skillpaths/react-redux/redux-quiz-app/spade.svg";
export const STATISTICS_ICON =
"https://static-assets.codecademy.com/skillpaths/react-redux/redux-quiz-app/statistics.svg";
export const SUN_ICON =
"https://static-assets.codecademy.com/skillpaths/react-redux/redux-quiz-app/sun.svg";
export const TREE_ICON =
"https://static-assets.codecademy.com/skillpaths/react-redux/redux-quiz-app/tree.svg";
export const ALL_ICONS = [
{
url: BOOK_ICON,
name: "Book",
},
{
url: BALLOON_ICON,
name: "Balloon",
},
{
url: BIRD_ICON,
name: "Bird",
},
{
url: CALENDAR_ICON,
name: "Calendar",
},
{
url: CLOVER_ICON,
name: "Clover",
},
{
url: CRAYONS_ICON,
name: "Crayons",
},
{
url: DATA_FLOW_ICON,
name: "Data",
},
{
url: FENCE_ICON,
name: "Fence",
},
{
url: GRILL_ICON,
name: "Grill",
},
{
url: HAND_DRILL_ICON,
name: "Hand",
},
{
url: HAT_ICON,
name: "Hat",
},
{
url: INTERNET_ICON,
name: "Internet",
},
{
url: LADYBUG_ICON,
name: "Ladybug",
},
{
url: LEAVES_ICON,
name: "Leaves",
},
{
url: MEDICINE_ICON,
name: "Medicine",
},
{
url: NEST_ICON,
name: "Nest",
},
{
url: SHUTTLECOCK_ICON,
name: "Shuttlecock",
},
{
url: SPADE_ICON,
name: "Spade",
},
{
url: STATISTICS_ICON,
name: "Statistics",
},
{
url: SUN_ICON,
name: "Sun",
},
{
url: TREE_ICON,
name: "Tree",
},
];

4
src/features/.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"printWidth": 80,
"trailingComma": "none"
}

View File

@ -0,0 +1,16 @@
import React, { useState } from "react";
import { Link, useParams } from "react-router-dom";
export default function Card({ id }) {
const cards = {}; // replace this with a call to your selector to get all the cards in state
const card = cards[id];
const [flipped, setFlipped] = useState(false);
return (
<li>
<button className="card" onClick={(e) => setFlipped(!flipped)}>
{flipped ? card.back : card.front}
</button>
</li>
);
}

View File

@ -0,0 +1,23 @@
import { Link, useParams } from "react-router-dom";
import Card from "../cards/Card";
import ROUTES from "../../app/routes";
export default function Topic() {
const quizzes = {}; // replace this with a call to your selector to get all the quizzes in state
let { quizId } = useParams();
const quiz = quizzes[quizId];
return (
<section>
<h1>{quiz.name}</h1>
<ul className="cards-list">
{quiz.cardIds.map((id) => (
<Card key={id} id={id} />
))}
</ul>
<Link to={ROUTES.newQuizRoute()} className="button center">
Create a New Quiz
</Link>
</section>
);
}

View File

@ -0,0 +1,21 @@
import { Link } from "react-router-dom";
import ROUTES from "../../app/routes";
export default function Quizzes() {
const quizzes = {}; // replace this with a call to your selector to get all the quizzes in state
return (
<section className="center">
<h1>Quizzes</h1>
<ul className="quizzes-list">
{Object.values(quizzes).map((quiz) => (
<Link key={quiz.id} to={ROUTES.quizRoute(quiz.id)}>
<li className="quiz">{quiz.name}</li>
</Link>
))}
</ul>
<Link to={ROUTES.newQuizRoute()} className="button">
Create New Quiz
</Link>
</section>
);
}

View File

@ -0,0 +1,32 @@
import { createSlice } from "@reduxjs/toolkit";
import { addQuizId } from "../topics/topicsSlice.js";
export const quizzesSlice = createSlice({
name: "quizzes",
initialState: {
quizzes: {}
},
reducers: {
addQuiz(state, action) {
const { id, name, topicId, cardIds } = action.payload;
state.quizzes[id] = {
id,
name,
topicId,
cardIds
};
}
}
});
export const addQuizThunk = (payload) => {
return (dispatch) => {
dispatch(addQuiz(payload));
dispatch(addQuizId({ topicId: payload.topicId, id: payload.id }));
};
};
export const selectQuizzes = (state) => state.quizzes.quizzes;
export const { addQuiz } = quizzesSlice.actions;
export default quizzesSlice.reducer;

View File

@ -0,0 +1,28 @@
import NewTopicForm from "../../components/NewTopicForm";
import { Link, useParams } from "react-router-dom";
import ROUTES from "../../app/routes";
export default function Topic() {
const topics = {}; // replace this with a call to your selector to select all the topics in state
const quizzes = {}; // replace this with a call to your selector to select all the quizzes in state
let { topicId } = useParams();
const topic = topics[topicId];
const quizzesForTopic = topic.quizIds.map((quizId) => quizzes[quizId]);
return (
<section>
<img src={topic.icon} alt="" className="topic-icon" />
<h1>Topic: {topic.name}</h1>
<ul className="quizzes-list">
{quizzesForTopic.map((quiz) => (
<li className="quiz" key={quiz.id}>
<Link to={ROUTES.quizRoute(quiz.id)}>{quiz.name}</Link>
</li>
))}
</ul>
<Link to="/quizzes/new" className="button center">
Create a New Quiz
</Link>
</section>
);
}

View File

@ -0,0 +1,36 @@
import NewTopicForm from "../../components/NewTopicForm";
import { Link } from "react-router-dom";
import ROUTES from "../../app/routes";
import { selectTopics } from "./topicsSlice";
import { useSelector } from "react-redux";
export default function Topics() {
const topics = useSelector(selectTopics); // replace this with a call to your selector to select all the topics in state
return (
<section className="center">
<h1>Topics</h1>
<ul className="topics-list">
{Object.values(topics).map((topic) => (
<li className="topic" key={topic.id}>
<Link to={ROUTES.topicRoute(topic.id)} className="topic-link">
<div className="topic-container">
<img src={topic.icon} alt="" />
<div className="text-content">
<h2>{topic.name}</h2>
<p>{topic.quizIds.length} Quizzes</p>
</div>
</div>
</Link>
</li>
))}
</ul>
<Link
to={ROUTES.newTopicRoute()}
className="button create-new-topic-button"
>
Create New Topic
</Link>
</section>
);
}

View File

@ -0,0 +1,30 @@
import { createSlice } from "@reduxjs/toolkit";
export const topicsSlice = createSlice({
name: "topics",
initialState: {
topics: {}
},
reducers: {
addTopic(state, action) {
const { id, name, icon } = action.payload;
state.topics[id] = {
id,
name,
icon,
quizIds: []
};
return state;
},
addQuizId(state, action) {
const { topicId, quizId } = action.payload;
state.topics[topicId].quizIds.push(quizId);
}
}
});
export const selectTopics = (state) => state.topics.topics;
export const { addTopic, addQuizId } = topicsSlice.actions;
export default topicsSlice.reducer;

45
src/index.css Normal file
View File

@ -0,0 +1,45 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
ul {
list-style: none;
}
.topics-list,
.quizzes-list {
display: flex;
flex-wrap: wrap;
}
.topic,
.quiz {
width: 200px;
height: 200px;
background: gray;
display: flex;
justify-content: center;
align-items: center;
margin-right: 20px;
margin-top: 20px;
}
.card {
width: 500px;
height: 250px;
background: gray;
margin: 20px auto;
display: flex;
justify-content: center;
align-items: center;
}

14
src/index.js Normal file
View File

@ -0,0 +1,14 @@
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import App from "./app/App";
import store from "./app/store";
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById("root")
);

5
src/setupTests.js Normal file
View File

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom";

9751
yarn.lock Normal file

File diff suppressed because it is too large Load Diff