Skip to content

Commit

Permalink
Made into a very basic redux app
Browse files Browse the repository at this point in the history
  • Loading branch information
jonniebigodes committed Dec 21, 2023
1 parent 429780a commit c70ec15
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 56 deletions.
4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -14,8 +14,10 @@
"license": "MIT",
"private": true,
"dependencies": {
"@reduxjs/toolkit": "^2.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-redux": "^9.0.4"
},
"scripts": {
"dev": "vite",
Expand Down
61 changes: 35 additions & 26 deletions src/components/TaskList.jsx
@@ -1,11 +1,32 @@
import React from "react";
import PropTypes from "prop-types";
import Task from "./Task";
import { useDispatch, useSelector } from "react-redux";
import { updateTaskState } from "../lib/store";

export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
const events = {
onPinTask,
onArchiveTask,
export default function TaskList() {
// We're retrieving our state from the store
const tasks = useSelector((state) => {
const tasksInOrder = [
...state.taskbox.tasks.filter((t) => t.state === "TASK_PINNED"),
...state.taskbox.tasks.filter((t) => t.state !== "TASK_PINNED"),
];
const filteredTasks = tasksInOrder.filter(
(t) => t.state === "TASK_INBOX" || t.state === "TASK_PINNED"
);
return filteredTasks;
});

const { status } = useSelector((state) => state.taskbox);

const dispatch = useDispatch();

const pinTask = (value) => {
// We're dispatching the Pinned event back to our store
dispatch(updateTaskState({ id: value, newTaskState: "TASK_PINNED" }));
};
const archiveTask = (value) => {
// We're dispatching the Archive event back to our store
dispatch(updateTaskState({ id: value, newTaskState: "TASK_ARCHIVED" }));
};
const LoadingRow = (
<div className="loading-item">
Expand All @@ -15,7 +36,7 @@ export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
</span>
</div>
);
if (loading) {
if (status === "loading") {
return (
<div className="list-items" data-testid="loading" key={"loading"}>
{LoadingRow}
Expand All @@ -39,28 +60,16 @@ export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
);
}

const tasksInOrder = [
...tasks.filter((t) => t.state === "TASK_PINNED"),
...tasks.filter((t) => t.state !== "TASK_PINNED"),
];
return (
<div className="list-items">
{tasksInOrder.map((task) => (
<Task key={task.id} task={task} {...events} />
<div className="list-items" data-testid="success" key={"success"}>
{tasks.map((task) => (
<Task
key={task.id}
task={task}
onPinTask={(task) => pinTask(task)}
onArchiveTask={(task) => archiveTask(task)}
/>
))}
</div>
);
}
TaskList.propTypes = {
/** Checks if it's in loading state */
loading: PropTypes.bool,
/** The list of tasks */
tasks: PropTypes.arrayOf(Task.propTypes.task).isRequired,
/** Event to change the task to pinned */
onPinTask: PropTypes.func,
/** Event to change the task to archived */
onArchiveTask: PropTypes.func,
};
TaskList.defaultProps = {
loading: false,
};
120 changes: 91 additions & 29 deletions src/components/TaskList.stories.jsx
@@ -1,49 +1,111 @@
import TaskList from "./TaskList";
import * as TaskStories from "./Task.stories";

import { Provider } from "react-redux";

import { configureStore, createSlice } from "@reduxjs/toolkit";

// A super-simple mock of the state of the store
export const MockedState = {
tasks: [
{ ...TaskStories.Default.args.task, id: "1", title: "Task 1" },
{ ...TaskStories.Default.args.task, id: "2", title: "Task 2" },
{ ...TaskStories.Default.args.task, id: "3", title: "Task 3" },
{ ...TaskStories.Default.args.task, id: "4", title: "Task 4" },
{ ...TaskStories.Default.args.task, id: "5", title: "Task 5" },
{ ...TaskStories.Default.args.task, id: "6", title: "Task 6" },
],
status: "idle",
error: null,
};

// A super-simple mock of a redux store
const Mockstore = ({ taskboxState, children }) => (
<Provider
store={configureStore({
reducer: {
taskbox: createSlice({
name: "taskbox",
initialState: taskboxState,
reducers: {
updateTaskState: (state, action) => {
const { id, newTaskState } = action.payload;
const task = state.tasks.findIndex((task) => task.id === id);
if (task >= 0) {
state.tasks[task].state = newTaskState;
}
},
},
}).reducer,
},
})}
>
{children}
</Provider>
);

export default {
component: TaskList,
title: "TaskList",
decorators: [(story) => <div style={{ padding: "3rem" }}>{story()}</div>],
tags: ["autodocs"],
decorators: [(story) => <div style={{ padding: "3rem" }}>{story()}</div>],
excludeStories: /.*MockedState$/,
};

export const Default = {
args: {
// Shaping the stories through args composition.
// The data was inherited from the Default story in Task.stories.js.
tasks: [
{ ...TaskStories.Default.args.task, id: "1", title: "Task 1" },
{ ...TaskStories.Default.args.task, id: "2", title: "Task 2" },
{ ...TaskStories.Default.args.task, id: "3", title: "Task 3" },
{ ...TaskStories.Default.args.task, id: "4", title: "Task 4" },
{ ...TaskStories.Default.args.task, id: "5", title: "Task 5" },
{ ...TaskStories.Default.args.task, id: "6", title: "Task 6" },
],
},
decorators: [
(story) => <Mockstore taskboxState={MockedState}>{story()}</Mockstore>,
],
};

export const WithPinnedTasks = {
args: {
tasks: [
...Default.args.tasks.slice(0, 5),
{ id: "6", title: "Task 6 (pinned)", state: "TASK_PINNED" },
],
},
decorators: [
(story) => {
const pinnedtasks = [
...MockedState.tasks.slice(0, 5),
{ id: "6", title: "Task 6 (pinned)", state: "TASK_PINNED" },
];

return (
<Mockstore
taskboxState={{
...MockedState,
tasks: pinnedtasks,
}}
>
{story()}
</Mockstore>
);
},
],
};

export const Loading = {
args: {
tasks: [],
loading: true,
},
decorators: [
(story) => (
<Mockstore
taskboxState={{
...MockedState,
status: "loading",
}}
>
{story()}
</Mockstore>
),
],
};

export const Empty = {
args: {
// Shaping the stories through args composition.
// Inherited data coming from the Loading story.
...Loading.args,
loading: false,
},
decorators: [
(story) => (
<Mockstore
taskboxState={{
...MockedState,
tasks: [],
}}
>
{story()}
</Mockstore>
),
],
};
55 changes: 55 additions & 0 deletions src/lib/store.js
@@ -0,0 +1,55 @@
/* A simple redux store/actions/reducer implementation.
* A true app would be more complex and separated into different files.
*/
import { configureStore, createSlice } from "@reduxjs/toolkit";

/*
* The initial state of our store when the app loads.
* Usually, you would fetch this from a server. Let's not worry about that now
*/
const defaultTasks = [
{ id: "1", title: "Something", state: "TASK_INBOX" },
{ id: "2", title: "Something more", state: "TASK_INBOX" },
{ id: "3", title: "Something else", state: "TASK_INBOX" },
{ id: "4", title: "Something again", state: "TASK_INBOX" },
];
const TaskBoxData = {
tasks: defaultTasks,
status: "idle",
error: null,
};

/*
* The store is created here.
* You can read more about Redux Toolkit's slices in the docs:
* https://redux-toolkit.js.org/api/createSlice
*/
const TasksSlice = createSlice({
name: "taskbox",
initialState: TaskBoxData,
reducers: {
updateTaskState: (state, action) => {
const { id, newTaskState } = action.payload;
const task = state.tasks.findIndex((task) => task.id === id);
if (task >= 0) {
state.tasks[task].state = newTaskState;
}
},
},
});

// The actions contained in the slice are exported for usage in our components
export const { updateTaskState } = TasksSlice.actions;

/*
* Our app's store configuration goes here.
* Read more about Redux's configureStore in the docs:
* https://redux-toolkit.js.org/api/configureStore
*/
const store = configureStore({
reducer: {
taskbox: TasksSlice.reducer,
},
});

export default store;
48 changes: 48 additions & 0 deletions yarn.lock
Expand Up @@ -1675,6 +1675,16 @@
dependencies:
"@babel/runtime" "^7.13.10"

"@reduxjs/toolkit@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-2.0.1.tgz#0a5233c1e35c1941b03aece39cceade3467a1062"
integrity sha512-fxIjrR9934cmS8YXIGd9e7s1XRsEU++aFc9DVNMFMRTM5Vtsg2DCRMj21eslGtDt43IUf9bJL3h5bwUlZleibA==
dependencies:
immer "^10.0.3"
redux "^5.0.0"
redux-thunk "^3.1.0"
reselect "^5.0.1"

"@rollup/pluginutils@^5.0.2":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.0.tgz#7e53eddc8c7f483a4ad0b94afb1f7f5fd3c771e0"
Expand Down Expand Up @@ -2692,6 +2702,11 @@
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc"
integrity sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==

"@types/use-sync-external-store@^0.0.3":
version "0.0.3"
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43"
integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==

"@types/uuid@^9.0.1":
version "9.0.7"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.7.tgz#b14cebc75455eeeb160d5fe23c2fcc0c64f724d8"
Expand Down Expand Up @@ -4379,6 +4394,11 @@ ignore@^5.2.0:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78"
integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==

immer@^10.0.3:
version "10.0.3"
resolved "https://registry.yarnpkg.com/immer/-/immer-10.0.3.tgz#a8de42065e964aa3edf6afc282dfc7f7f34ae3c9"
integrity sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==

imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
Expand Down Expand Up @@ -5799,6 +5819,14 @@ react-is@^18.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==

react-redux@^9.0.4:
version "9.0.4"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.0.4.tgz#6892d465f086507a517d4b53eb589876e6bc8344"
integrity sha512-9J1xh8sWO0vYq2sCxK2My/QO7MzUMRi3rpiILP/+tDr8krBHixC6JMM17fMK88+Oh3e4Ae6/sHIhNBgkUivwFA==
dependencies:
"@types/use-sync-external-store" "^0.0.3"
use-sync-external-store "^1.0.0"

react-refresh@^0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e"
Expand Down Expand Up @@ -5906,6 +5934,16 @@ redent@^3.0.0:
indent-string "^4.0.0"
strip-indent "^3.0.0"

redux-thunk@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3"
integrity sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==

redux@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.0.tgz#29572e29a439e094ff8fec46883fc45053f6736d"
integrity sha512-blLIYmYetpZMET6Q6uCY7Jtl/Im5OBldy+vNPauA8vvsdqyt66oep4EUpAMWNHauTC6xa9JuRPhRB72rY82QGA==

regenerate-unicode-properties@^10.1.0:
version "10.1.1"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480"
Expand Down Expand Up @@ -5983,6 +6021,11 @@ require-directory@^2.1.1:
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==

reselect@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.0.1.tgz#587cdaaeb4e0e8927cff80ebe2bbef05f74b1648"
integrity sha512-D72j2ubjgHpvuCiORWkOUxndHJrxDaSolheiz5CO+roz8ka97/4msh2E8F5qay4GawR5vzBt5MkbDHT+Rdy/Wg==

resolve-from@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
Expand Down Expand Up @@ -6698,6 +6741,11 @@ use-sidecar@^1.1.2:
detect-node-es "^1.1.0"
tslib "^2.0.0"

use-sync-external-store@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==

util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
Expand Down

0 comments on commit c70ec15

Please sign in to comment.