├── public
└── _redirects
├── .env.example
├── src
├── components
│ ├── views
│ │ ├── user-detail
│ │ │ ├── user-detail.module.css
│ │ │ └── UserDetail.jsx
│ │ ├── thing-detail
│ │ │ ├── thing-detail.module.css
│ │ │ └── ThingDetail.jsx
│ │ ├── home
│ │ │ ├── home.module.css
│ │ │ └── Home.jsx
│ │ ├── login
│ │ │ ├── login.module.css
│ │ │ └── Login.jsx
│ │ ├── register
│ │ │ ├── register.module.css
│ │ │ └── Register.jsx
│ │ ├── add-thing
│ │ │ ├── add-thing.module.css
│ │ │ └── AddThing.jsx
│ │ ├── edit-thing
│ │ │ ├── edit-thing.module.css
│ │ │ └── EditThing.jsx
│ │ ├── delete-thing
│ │ │ ├── delete-thing.module.css
│ │ │ └── DeleteThing.jsx
│ │ ├── things-list
│ │ │ ├── things-list.module.css
│ │ │ └── ThingsList.jsx
│ │ ├── users-list
│ │ │ ├── users-list.module.css
│ │ │ └── UsersList.jsx
│ │ └── my-things
│ │ │ ├── my-things.module.css
│ │ │ └── MyThings.jsx
│ └── ui
│ │ ├── main-content
│ │ ├── main-content.module.css
│ │ └── MainContent.jsx
│ │ ├── site-footer
│ │ ├── site-footer.module.css
│ │ └── SiteFooter.jsx
│ │ ├── root
│ │ ├── root.module.css
│ │ └── Root.jsx
│ │ ├── toast
│ │ ├── toast.module.css
│ │ └── Toast.jsx
│ │ ├── breadcrumb
│ │ ├── breadcrumb.module.css
│ │ └── Breadcrumb.jsx
│ │ ├── site-nav
│ │ ├── site-nav.module.css
│ │ └── SiteNav.jsx
│ │ ├── site-header
│ │ ├── site-header.module.css
│ │ └── SiteHeader.jsx
│ │ └── protected-route
│ │ └── ProtectedRoute.jsx
├── slices
│ ├── breadcrumbSlice.js
│ ├── toastSlice.js
│ ├── authSlice.js
│ ├── usersSlice.js
│ └── thingsSlice.js
├── main.jsx
├── store.js
├── assets
│ └── css
│ │ └── global.css
├── services
│ ├── authService.js
│ └── apiService.js
└── routes.jsx
├── vite.config.js
├── index.html
├── .gitignore
├── .vscode
└── settings.json
├── .eslintrc.cjs
├── package.json
└── README.md
/public/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | VITE_API_URL="http://localhost:3001/api"
--------------------------------------------------------------------------------
/src/components/views/user-detail/user-detail.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | background: #efefef;
3 | width: 100%;
4 | padding: 1rem;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/views/thing-detail/thing-detail.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | background: #efefef;
3 | width: 100%;
4 | padding: 1rem;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/views/home/home.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | background: #efefef;
3 | width: 100%;
4 | align-items: center;
5 | justify-content: center;
6 | display: flex;
7 | }
8 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/src/components/ui/main-content/main-content.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: flex;
3 | align-items: stretch;
4 | justify-content: center;
5 | width: 100%;
6 | height: 100%;
7 | overflow: auto;
8 | flex: 1;
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/ui/site-footer/site-footer.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | padding: 1rem;
3 | display: flex;
4 | align-items: center;
5 | justify-content: flex-end;
6 | color: white;
7 | background: grey;
8 | font-size: 14px;
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/views/login/login.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | background: #efefef;
3 | width: 100%;
4 | padding: 1rem;
5 | }
6 |
7 | .inputContainer {
8 | display: block;
9 | }
10 |
11 | .input {
12 | padding: 10px;
13 | border: 1px solid #ccc;
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/views/register/register.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | background: #efefef;
3 | width: 100%;
4 | padding: 1rem;
5 | }
6 |
7 | .inputContainer {
8 | display: block;
9 | }
10 |
11 | .input {
12 | padding: 10px;
13 | border: 1px solid #ccc;
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/ui/site-footer/SiteFooter.jsx:
--------------------------------------------------------------------------------
1 | import styles from "./site-footer.module.css";
2 |
3 | function SiteFooter() {
4 | return (
5 |
© {new Date().getFullYear()}
6 | );
7 | }
8 |
9 | export default SiteFooter;
10 |
--------------------------------------------------------------------------------
/src/components/views/add-thing/add-thing.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | background: #efefef;
3 | width: 100%;
4 | padding: 1rem;
5 | }
6 |
7 | .inputContainer {
8 | display: block;
9 | }
10 |
11 | .input {
12 | padding: 10px;
13 | border: 1px solid #ccc;
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/views/edit-thing/edit-thing.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | background: #efefef;
3 | width: 100%;
4 | padding: 1rem;
5 | }
6 |
7 | .inputContainer {
8 | display: block;
9 | }
10 |
11 | .input {
12 | padding: 10px;
13 | border: 1px solid #ccc;
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/views/delete-thing/delete-thing.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | background: #efefef;
3 | width: 100%;
4 | padding: 1rem;
5 | }
6 |
7 | .inputContainer {
8 | display: block;
9 | }
10 |
11 | .input {
12 | padding: 10px;
13 | border: 1px solid #ccc;
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/ui/root/root.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | display: flex;
3 | flex-direction: column;
4 | width: 90%;
5 | height: 80%;
6 | max-width: 600px;
7 | box-shadow: 0 0 10px rgb(0 0 0 / 40%);
8 | background: #fff;
9 | overflow: hidden;
10 | border-radius: 20px;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/ui/main-content/MainContent.jsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from "react-router-dom";
2 |
3 | import styles from "./main-content.module.css";
4 |
5 | function MainContent() {
6 | return (
7 |
8 |
9 |
10 | );
11 | }
12 |
13 | export default MainContent;
14 |
--------------------------------------------------------------------------------
/src/components/ui/toast/toast.module.css:
--------------------------------------------------------------------------------
1 | .toast {
2 | position: fixed;
3 | bottom: 20px;
4 | right: 20px;
5 | background-color: #333;
6 | color: #fff;
7 | padding: 1rem;
8 | font-size: 24px;
9 | border-radius: 5px;
10 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
11 | z-index: 1000;
12 | opacity: 0.8;
13 | }
14 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | The things!
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/ui/breadcrumb/breadcrumb.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | padding: 1rem;
3 | display: flex;
4 | align-items: center;
5 | justify-content: flex-start;
6 | background: #ddd;
7 | }
8 |
9 | .crumb {
10 | display: flex;
11 | font-size: 14px;
12 | }
13 |
14 | .divider {
15 | margin-left: 5px;
16 | margin-right: 5px;
17 | }
18 |
19 | .link {
20 | color: #333;
21 | }
22 |
--------------------------------------------------------------------------------
/src/slices/breadcrumbSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const breadcrumbSlice = createSlice({
4 | name: "breadcrumb",
5 | initialState: [],
6 | reducers: {
7 | setBreadcrumb: (state, action) => {
8 | return action.payload;
9 | },
10 | },
11 | });
12 |
13 | export const { setBreadcrumb } = breadcrumbSlice.actions;
14 | export default breadcrumbSlice.reducer;
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | !.vscode/settings.json
19 | .idea
20 | .DS_Store
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw?
26 |
27 | # env
28 | .env
--------------------------------------------------------------------------------
/src/components/ui/site-nav/site-nav.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | padding: 1rem;
3 | border: 1px solid lightgray;
4 | display: flex;
5 | align-items: center;
6 | justify-content: flex-start;
7 | background: lightgrey;
8 | }
9 |
10 | .links {
11 | display: flex;
12 | }
13 |
14 | .activeLink,
15 | .inactiveLink {
16 | font-size: 20px;
17 | margin-right: 1.5rem;
18 | }
19 |
20 | .activeLink {
21 | color: #000;
22 | }
23 |
24 | .inactiveLink {
25 | color: #666;
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/ui/site-header/site-header.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | padding: 1rem;
3 | display: flex;
4 | width: 100%;
5 | align-items: center;
6 | justify-content: space-between;
7 | color: white;
8 | background: grey;
9 | }
10 |
11 | .title {
12 | margin: 5px 0 0 0;
13 | }
14 |
15 | .links {
16 | }
17 |
18 | .activeLink,
19 | .inactiveLink {
20 | padding: 8px;
21 | }
22 |
23 | .activeLink {
24 | color: #fff;
25 | }
26 |
27 | .inactiveLink {
28 | color: #ddd;
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/ui/protected-route/ProtectedRoute.jsx:
--------------------------------------------------------------------------------
1 | import { Navigate, useLocation } from "react-router-dom";
2 | import { useSelector } from "react-redux";
3 |
4 | const ProtectedRoute = ({ element }) => {
5 | const location = useLocation();
6 | const isLoggedIn = useSelector((state) => state.auth.isLoggedIn);
7 |
8 | if (!isLoggedIn) {
9 | return ;
10 | }
11 |
12 | return element;
13 | };
14 |
15 | export default ProtectedRoute;
16 |
--------------------------------------------------------------------------------
/src/slices/toastSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const toastSlice = createSlice({
4 | name: "toast",
5 | initialState: {
6 | message: null,
7 | },
8 | reducers: {
9 | showToast: (state, action) => {
10 | state.message = action.payload;
11 | },
12 | hideToast: (state) => {
13 | state.message = null;
14 | },
15 | },
16 | });
17 |
18 | export const { showToast, hideToast } = toastSlice.actions;
19 | export default toastSlice.reducer;
20 |
--------------------------------------------------------------------------------
/src/components/views/home/Home.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useDispatch } from "react-redux";
3 | import { setBreadcrumb } from "../../../slices/breadcrumbSlice";
4 | import styles from "./home.module.css";
5 |
6 | function Home() {
7 | const dispatch = useDispatch();
8 |
9 | useEffect(() => {
10 | dispatch(setBreadcrumb([{ label: "Home" }]));
11 | }, [dispatch]);
12 |
13 | return (
14 |
15 |
Welcome to the things
16 |
17 | );
18 | }
19 |
20 | export default Home;
21 |
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import { Provider } from "react-redux";
4 | import { createBrowserRouter, RouterProvider } from "react-router-dom";
5 | import store from "./store";
6 | import routes from "./routes";
7 | import "./assets/css/global.css";
8 |
9 | const router = createBrowserRouter(routes);
10 |
11 | ReactDOM.createRoot(document.getElementById("root")).render(
12 |
13 |
14 |
15 |
16 |
17 | );
18 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 |
3 | import authReducer from "./slices/authSlice";
4 | import thingsReducer from "./slices/thingsSlice";
5 | import usersReducer from "./slices/usersSlice";
6 | import toastReducer from "./slices/toastSlice";
7 | import breadcrumbReducer from "./slices/breadcrumbSlice";
8 |
9 | const store = configureStore({
10 | reducer: {
11 | auth: authReducer,
12 | things: thingsReducer,
13 | users: usersReducer,
14 | toast: toastReducer,
15 | breadcrumb: breadcrumbReducer,
16 | },
17 | });
18 |
19 | export default store;
20 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "[javascript]": {
4 | "editor.defaultFormatter": "esbenp.prettier-vscode"
5 | },
6 | "[json]": {
7 | "editor.defaultFormatter": "esbenp.prettier-vscode"
8 | },
9 | "[html]": {
10 | "editor.defaultFormatter": "esbenp.prettier-vscode"
11 | },
12 | "[css]": {
13 | "editor.defaultFormatter": "esbenp.prettier-vscode"
14 | },
15 | "[jsonc]": {
16 | "editor.defaultFormatter": "esbenp.prettier-vscode"
17 | },
18 | "[javascriptreact]": {
19 | "editor.defaultFormatter": "esbenp.prettier-vscode"
20 | }
21 | }
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:react/recommended',
7 | 'plugin:react/jsx-runtime',
8 | 'plugin:react-hooks/recommended',
9 | ],
10 | ignorePatterns: ['dist', '.eslintrc.cjs'],
11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
12 | settings: { react: { version: '18.2' } },
13 | plugins: ['react-refresh'],
14 | rules: {
15 | 'react/jsx-no-target-blank': 'off',
16 | 'react-refresh/only-export-components': [
17 | 'warn',
18 | { allowConstantExport: true },
19 | ],
20 | },
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/ui/root/Root.jsx:
--------------------------------------------------------------------------------
1 | import styles from "./root.module.css";
2 |
3 | import SiteHeader from "../site-header/SiteHeader";
4 | import SiteNav from "../site-nav/SiteNav";
5 | import SiteFooter from "../site-footer/SiteFooter";
6 | import MainContent from "../main-content/MainContent";
7 | import Toast from "../toast/Toast";
8 | import Breadcrumb from "../breadcrumb/Breadcrumb";
9 |
10 | function Root() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | }
22 |
23 | export default Root;
24 |
--------------------------------------------------------------------------------
/src/components/views/things-list/things-list.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | background: #efefef;
3 | width: 100%;
4 | padding: 1rem;
5 | }
6 |
7 | .list {
8 | display: flex;
9 | flex-wrap: wrap;
10 | align-items: center;
11 | justify-content: flex-start;
12 | margin: 0;
13 | padding: 0;
14 | }
15 |
16 | .item {
17 | list-style: none;
18 | flex-basis: 50%;
19 | }
20 |
21 | @media (min-width: 600px) {
22 | .item {
23 | flex-basis: 33.33%;
24 | }
25 | }
26 |
27 | .link {
28 | display: block;
29 | padding: 10px;
30 | margin-right: 10px;
31 | margin-bottom: 10px;
32 | border: 1px solid #ccc;
33 | border-radius: 10px;
34 | text-align: center;
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/views/users-list/users-list.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | background: #efefef;
3 | width: 100%;
4 | padding: 1rem;
5 | }
6 |
7 | .list {
8 | display: flex;
9 | flex-wrap: wrap;
10 | align-items: center;
11 | justify-content: flex-start;
12 | margin: 0;
13 | padding: 0;
14 | }
15 |
16 | .item {
17 | list-style: none;
18 | flex-basis: 50%;
19 | }
20 |
21 | @media (min-width: 600px) {
22 | .item {
23 | flex-basis: 33.33%;
24 | }
25 | }
26 |
27 | .link {
28 | display: block;
29 | padding: 10px;
30 | margin-right: 10px;
31 | margin-bottom: 10px;
32 | border: 1px solid #ccc;
33 | border-radius: 10px;
34 | text-align: center;
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/views/my-things/my-things.module.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | background: #efefef;
3 | width: 100%;
4 | padding: 1rem;
5 | }
6 |
7 | .list {
8 | display: flex;
9 | flex-wrap: wrap;
10 | align-items: center;
11 | justify-content: flex-start;
12 | margin: 0;
13 | padding: 0;
14 | }
15 |
16 | .item {
17 | list-style: none;
18 | flex-basis: 50%;
19 | }
20 |
21 | @media (min-width: 600px) {
22 | .item {
23 | flex-basis: 33.33%;
24 | }
25 | }
26 |
27 | .itemContent {
28 | display: block;
29 | padding: 10px;
30 | margin-right: 10px;
31 | margin-bottom: 10px;
32 | border: 1px solid #ccc;
33 | border-radius: 10px;
34 | text-align: center;
35 | }
36 |
37 | .link {
38 | margin: 0 5px;
39 | color: #333;
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/ui/toast/Toast.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { hideToast } from "../../../slices/toastSlice";
4 | import styles from "./toast.module.css";
5 |
6 | const Toast = () => {
7 | const dispatch = useDispatch();
8 | const toastMessage = useSelector((state) => state.toast.message);
9 |
10 | useEffect(() => {
11 | if (toastMessage) {
12 | const timer = setTimeout(() => {
13 | dispatch(hideToast());
14 | }, 3000);
15 | return () => clearTimeout(timer);
16 | }
17 | }, [toastMessage, dispatch]);
18 |
19 | if (!toastMessage) return null;
20 |
21 | return {toastMessage}
;
22 | };
23 |
24 | export default Toast;
25 |
--------------------------------------------------------------------------------
/src/assets/css/global.css:
--------------------------------------------------------------------------------
1 | html {
2 | box-sizing: border-box;
3 | }
4 |
5 | *,
6 | *::before,
7 | *::after {
8 | box-sizing: inherit;
9 | }
10 |
11 | html,
12 | body {
13 | height: 100%;
14 | }
15 |
16 | body {
17 | margin: 0;
18 | padding: 0;
19 | font-family: Helvetica, sans-serif;
20 | }
21 |
22 | #root {
23 | height: 100%;
24 | display: flex;
25 | align-items: center;
26 | justify-content: center;
27 | background: linear-gradient(45deg, #ffd07b, #aaa1c8);
28 | background-size: 400% 400%;
29 | animation: gradient 11s ease infinite;
30 | }
31 |
32 | @keyframes gradient {
33 | 0% {
34 | background-position: 0% 50%;
35 | }
36 | 35% {
37 | background-position: 100% 50%;
38 | }
39 | 65% {
40 | background-position: 0% 50%;
41 | }
42 | 100% {
43 | background-position: 0% 50%;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "project-react-frontend",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@reduxjs/toolkit": "^2.2.5",
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0",
16 | "react-redux": "^9.1.2",
17 | "react-router-dom": "^6.23.1",
18 | "redux": "^5.0.1"
19 | },
20 | "devDependencies": {
21 | "@types/react": "^18.2.66",
22 | "@types/react-dom": "^18.2.22",
23 | "@vitejs/plugin-react": "^4.2.1",
24 | "eslint": "^8.57.0",
25 | "eslint-plugin-react": "^7.34.1",
26 | "eslint-plugin-react-hooks": "^4.6.0",
27 | "eslint-plugin-react-refresh": "^0.4.6",
28 | "vite": "^5.2.0"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/ui/breadcrumb/Breadcrumb.jsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { NavLink } from "react-router-dom";
3 | import styles from "./breadcrumb.module.css";
4 |
5 | const Breadcrumb = () => {
6 | const breadcrumb = useSelector((state) => state.breadcrumb);
7 |
8 | return (
9 |
25 | );
26 | };
27 |
28 | export default Breadcrumb;
29 |
--------------------------------------------------------------------------------
/src/components/ui/site-nav/SiteNav.jsx:
--------------------------------------------------------------------------------
1 | import { NavLink } from "react-router-dom";
2 | import { useSelector } from "react-redux";
3 | import styles from "./site-nav.module.css";
4 |
5 | function SiteNav() {
6 | const isLoggedIn = useSelector((state) => state.auth.isLoggedIn);
7 |
8 | const navLinks = [
9 | { label: "Home", url: "/" },
10 | { label: "Things", url: "/things/" },
11 | ];
12 |
13 | if (isLoggedIn) {
14 | navLinks.push({ label: "Users", url: "/users/" });
15 | }
16 |
17 | return (
18 |
19 |
32 |
33 | );
34 | }
35 |
36 | export default SiteNav;
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Project - example front-end
2 |
3 | This is an example front-end application for the React project.
4 |
5 | It allows users to register, log in, and manage their collections of things.
6 |
7 | The app is built using React and Redux.
8 |
9 |
10 | ## Installation
11 |
12 | ### Prerequisites
13 |
14 | Ensure you have the following installed:
15 |
16 | - Node.js (version 18 or higher)
17 | - npm (version 6 or higher)
18 |
19 | ### Install dependencies
20 |
21 | ```
22 | npm install
23 | ```
24 |
25 | ### Environment Variables
26 |
27 | Create a `.env` file in the root of the project - duplicate the `.env.example` file and replace values with your real values:
28 |
29 | ```
30 | VITE_API_URL="http://localhost:3001/api"
31 | ```
32 |
33 | ## Running the app
34 |
35 | Start the app using the following command:
36 |
37 | ```
38 | npm run dev
39 | ```
40 |
41 | The app should now be running on [http://localhost:3000](http://localhost:3000)
42 |
43 |
44 | ## Available scripts
45 |
46 | In the project directory, you can run the following scripts:
47 |
48 | - `npm run dev`: Start the development server.
49 | - `npm run build`: Build the app for production.
50 | - `npm run serve`: Serve the production build of the app.
51 | - `npm run lint`: Run ESLint to lint the code.
52 |
--------------------------------------------------------------------------------
/src/slices/authSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
2 | import authService from "../services/authService";
3 |
4 | export const login = createAsyncThunk(
5 | "auth/login",
6 | async ({ email, password }) => {
7 | const user = await authService.login(email, password);
8 | return user;
9 | }
10 | );
11 |
12 | export const logout = createAsyncThunk("auth/logout", async () => {
13 | await authService.logout();
14 | });
15 |
16 | const initialState = {
17 | isLoggedIn: authService.isLoggedIn(),
18 | status: "idle",
19 | error: null,
20 | };
21 |
22 | const authSlice = createSlice({
23 | name: "auth",
24 | initialState,
25 | extraReducers: (builder) => {
26 | builder
27 | .addCase(login.pending, (state) => {
28 | state.status = "loading";
29 | })
30 | .addCase(login.fulfilled, (state) => {
31 | state.status = "succeeded";
32 | state.isLoggedIn = true;
33 | })
34 | .addCase(login.rejected, (state, action) => {
35 | state.status = "failed";
36 | state.error = action.error.message;
37 | })
38 | .addCase(logout.fulfilled, (state) => {
39 | state.isLoggedIn = false;
40 | state.status = "idle";
41 | });
42 | },
43 | });
44 |
45 | export default authSlice.reducer;
46 |
--------------------------------------------------------------------------------
/src/components/views/users-list/UsersList.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { NavLink } from "react-router-dom";
4 | import { fetchUsers } from "../../../slices/usersSlice";
5 | import { setBreadcrumb } from "../../../slices/breadcrumbSlice";
6 | import styles from "./users-list.module.css";
7 |
8 | const UsersList = () => {
9 | const dispatch = useDispatch();
10 | const users = useSelector((state) => state.users.items);
11 | const status = useSelector((state) => state.users.status);
12 | const error = useSelector((state) => state.users.error);
13 |
14 | useEffect(() => {
15 | dispatch(fetchUsers());
16 | }, [dispatch]);
17 |
18 | useEffect(() => {
19 | dispatch(setBreadcrumb([{ label: "Home", url: "/" }, { label: "Users" }]));
20 | }, [dispatch]);
21 |
22 | return (
23 |
24 | {status === "loading" &&
Loading...
}
25 | {status === "failed" &&
{error}
}
26 |
27 | {users.map((user) => (
28 | -
29 |
30 | {user.name}
31 |
32 |
33 | ))}
34 |
35 |
36 | );
37 | };
38 |
39 | export default UsersList;
40 |
--------------------------------------------------------------------------------
/src/components/views/things-list/ThingsList.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { NavLink } from "react-router-dom";
4 | import { fetchThings } from "../../../slices/thingsSlice";
5 | import { setBreadcrumb } from "../../../slices/breadcrumbSlice";
6 | import styles from "./things-list.module.css";
7 |
8 | const ThingsList = () => {
9 | const dispatch = useDispatch();
10 | const things = useSelector((state) => state.things.items);
11 | const status = useSelector((state) => state.things.status);
12 | const error = useSelector((state) => state.things.error);
13 |
14 | useEffect(() => {
15 | dispatch(fetchThings());
16 | }, [dispatch]);
17 |
18 | useEffect(() => {
19 | dispatch(setBreadcrumb([{ label: "Home", url: "/" }, { label: "Things" }]));
20 | }, [dispatch]);
21 |
22 | return (
23 |
24 | {status === "loading" &&
Loading...
}
25 | {status === "failed" &&
{error}
}
26 |
27 | {things.map((thing) => (
28 | -
29 |
30 | {thing.name}
31 |
32 |
33 | ))}
34 |
35 |
36 | );
37 | };
38 |
39 | export default ThingsList;
40 |
--------------------------------------------------------------------------------
/src/components/ui/site-header/SiteHeader.jsx:
--------------------------------------------------------------------------------
1 | import { NavLink } from "react-router-dom";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { logout } from "../../../slices/authSlice";
4 | import styles from "./site-header.module.css";
5 |
6 | function SiteHeader() {
7 | const dispatch = useDispatch();
8 | const isLoggedIn = useSelector((state) => state.auth.isLoggedIn);
9 |
10 | const handleLogout = () => {
11 | dispatch(logout());
12 | };
13 |
14 | const navLinks = [];
15 | if (isLoggedIn) {
16 | navLinks.push(
17 | { label: "My things", url: "/my-things/" },
18 | { label: "Add thing", url: "/my-things/add/" }
19 | );
20 | } else {
21 | navLinks.push(
22 | { label: "Login", url: "/login/" },
23 | { label: "Register", url: "/register/" }
24 | );
25 | }
26 |
27 | return (
28 |
29 |
The Things!
30 |
44 |
45 | );
46 | }
47 |
48 | export default SiteHeader;
49 |
--------------------------------------------------------------------------------
/src/services/authService.js:
--------------------------------------------------------------------------------
1 | import makeApiRequest from "./apiService";
2 |
3 | const register = async (name, email, password, bio) => {
4 | return makeApiRequest("auth/register", {
5 | method: "POST",
6 | body: JSON.stringify({ name, email, password, bio }),
7 | });
8 | };
9 |
10 | const login = async (email, password) => {
11 | const response = await makeApiRequest("auth/login", {
12 | method: "POST",
13 | body: JSON.stringify({ email, password }),
14 | });
15 |
16 | if (response.accessToken) {
17 | sessionStorage.setItem("accessToken", response.accessToken);
18 | }
19 |
20 | return { id: response.id, name: response.name };
21 | };
22 |
23 | const logout = async () => {
24 | await makeApiRequest("auth/logout", {
25 | method: "POST",
26 | });
27 | sessionStorage.removeItem("accessToken");
28 | };
29 |
30 | const getAccessToken = () => {
31 | return sessionStorage.getItem("accessToken");
32 | };
33 |
34 | const refreshAccessToken = async () => {
35 | const response = await makeApiRequest("auth/refresh-token", {
36 | method: "POST",
37 | });
38 |
39 | if (response.accessToken) {
40 | sessionStorage.setItem("accessToken", response.accessToken);
41 | }
42 |
43 | return response.accessToken;
44 | };
45 |
46 | const isLoggedIn = () => {
47 | return !!sessionStorage.getItem("accessToken");
48 | };
49 |
50 | export default {
51 | register,
52 | login,
53 | logout,
54 | getAccessToken,
55 | refreshAccessToken,
56 | isLoggedIn,
57 | };
58 |
--------------------------------------------------------------------------------
/src/components/views/delete-thing/DeleteThing.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useParams, useNavigate } from "react-router-dom";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { fetchThings, deleteThing } from "../../../slices/thingsSlice";
5 | import { setBreadcrumb } from "../../../slices/breadcrumbSlice";
6 | import { showToast } from "../../../slices/toastSlice";
7 | import styles from "./delete-thing.module.css";
8 |
9 | const DeleteThing = () => {
10 | const { id } = useParams();
11 | const navigate = useNavigate();
12 | const dispatch = useDispatch();
13 | const thing = useSelector((state) =>
14 | state.things.items.find((thing) => thing.id === parseInt(id))
15 | );
16 |
17 | useEffect(() => {
18 | dispatch(
19 | setBreadcrumb([
20 | { label: "Home", url: "/" },
21 | { label: "My things", url: "/my-things/" },
22 | { label: "Delete thing" },
23 | ])
24 | );
25 | }, [dispatch]);
26 |
27 | useEffect(() => {
28 | if (!thing) {
29 | dispatch(fetchThings());
30 | }
31 | }, [thing, dispatch]);
32 |
33 | const handleSubmit = async (e) => {
34 | e.preventDefault();
35 | await dispatch(deleteThing(thing.id));
36 | dispatch(showToast(`Thing deleted`));
37 | navigate("/my-things/");
38 | };
39 |
40 | return (
41 |
47 | );
48 | };
49 |
50 | export default DeleteThing;
51 |
--------------------------------------------------------------------------------
/src/services/apiService.js:
--------------------------------------------------------------------------------
1 | import authService from "./authService";
2 | import store from "../store";
3 | import { showToast } from "../slices/toastSlice";
4 |
5 | const API_URL = import.meta.env.VITE_API_URL;
6 |
7 | const makeApiRequest = async (url, options = {}) => {
8 | options.headers = options.headers || {};
9 | // Include credentials for cross-origin requests
10 | options.credentials = "include";
11 | options.headers["Content-Type"] = "application/json";
12 |
13 | let accessToken = authService.getAccessToken();
14 | if (accessToken) {
15 | options.headers["Authorization"] = `Bearer ${accessToken}`;
16 | }
17 |
18 | try {
19 | let response = await fetch(`${API_URL}/${url}`, options);
20 |
21 | if (accessToken && (response.status === 401 || response.status === 403)) {
22 | // Attempt to refresh the access token and re-request
23 | try {
24 | accessToken = await authService.refreshAccessToken();
25 | if (accessToken) {
26 | options.headers["Authorization"] = `Bearer ${accessToken}`;
27 | response = await fetch(`${API_URL}/${url}`, options);
28 | } else {
29 | throw new Error("Unauthorized");
30 | }
31 | } catch (error) {
32 | await authService.logout();
33 | throw new Error("Unauthorized");
34 | }
35 | }
36 |
37 | if (response.status >= 400) {
38 | const data = await response.json();
39 | throw new Error(data.error || "Fetch failed");
40 | }
41 |
42 | return await response.json();
43 | } catch (error) {
44 | store.dispatch(showToast(error.message));
45 | throw error;
46 | }
47 | };
48 |
49 | export default makeApiRequest;
50 |
--------------------------------------------------------------------------------
/src/slices/usersSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
2 | import apiService from "../services/apiService";
3 |
4 | export const fetchUsers = createAsyncThunk("users/fetchUsers", async () => {
5 | const response = await apiService("users", { method: "GET" });
6 | return response;
7 | });
8 |
9 | export const fetchUser = createAsyncThunk("users/fetchUser", async (id) => {
10 | const response = await apiService(`users/${id}`, { method: "GET" });
11 | return response;
12 | });
13 |
14 | const usersSlice = createSlice({
15 | name: "users",
16 | initialState: {
17 | items: [],
18 | user: null,
19 | status: "idle",
20 | userStatus: "idle",
21 | error: null,
22 | userError: null,
23 | },
24 | extraReducers: (builder) => {
25 | builder
26 | .addCase(fetchUsers.pending, (state) => {
27 | state.status = "loading";
28 | })
29 | .addCase(fetchUsers.fulfilled, (state, action) => {
30 | state.status = "succeeded";
31 | state.items = action.payload;
32 | })
33 | .addCase(fetchUsers.rejected, (state, action) => {
34 | state.status = "failed";
35 | state.error = action.error.message;
36 | })
37 | .addCase(fetchUser.pending, (state) => {
38 | state.userStatus = "loading";
39 | })
40 | .addCase(fetchUser.fulfilled, (state, action) => {
41 | state.userStatus = "succeeded";
42 | state.user = action.payload;
43 | })
44 | .addCase(fetchUser.rejected, (state, action) => {
45 | state.userStatus = "failed";
46 | state.userError = action.error.message;
47 | });
48 | },
49 | });
50 |
51 | export default usersSlice.reducer;
52 |
--------------------------------------------------------------------------------
/src/components/views/my-things/MyThings.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { NavLink, useLocation } from "react-router-dom";
4 | import { fetchMyThings } from "../../../slices/thingsSlice";
5 | import { setBreadcrumb } from "../../../slices/breadcrumbSlice";
6 | import styles from "./my-things.module.css";
7 |
8 | const MyThings = () => {
9 | const dispatch = useDispatch();
10 | const location = useLocation();
11 | const things = useSelector((state) => state.things.userThings);
12 | const status = useSelector((state) => state.things.userStatus);
13 | const error = useSelector((state) => state.things.userError);
14 |
15 | useEffect(() => {
16 | dispatch(fetchMyThings());
17 | }, [dispatch, location]);
18 |
19 | useEffect(() => {
20 | dispatch(
21 | setBreadcrumb([{ label: "Home", url: "/" }, { label: "My things" }])
22 | );
23 | }, [dispatch]);
24 |
25 | return (
26 |
27 | {status === "loading" &&
Loading...
}
28 | {status === "failed" &&
{error}
}
29 |
30 | {things.map((thing) => (
31 | -
32 |
33 |
{thing.name}
34 |
35 | Edit
36 |
37 |
38 | Delete
39 |
40 |
41 |
42 | ))}
43 |
44 |
45 | );
46 | };
47 |
48 | export default MyThings;
49 |
--------------------------------------------------------------------------------
/src/components/views/thing-detail/ThingDetail.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { NavLink, useParams } from "react-router-dom";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { fetchThing } from "../../../slices/thingsSlice";
5 | import { setBreadcrumb } from "../../../slices/breadcrumbSlice";
6 | import styles from "./thing-detail.module.css";
7 |
8 | const ThingDetail = () => {
9 | const { id } = useParams();
10 | const dispatch = useDispatch();
11 | const thing = useSelector((state) => state.things.currentThing);
12 | const status = useSelector((state) => state.things.status);
13 | const error = useSelector((state) => state.things.error);
14 | const isLoggedIn = useSelector((state) => state.auth.isLoggedIn);
15 |
16 | useEffect(() => {
17 | dispatch(fetchThing(id));
18 | }, [dispatch, id]);
19 |
20 | useEffect(() => {
21 | dispatch(
22 | setBreadcrumb([
23 | { label: "Home", url: "/" },
24 | { label: "Things", url: "/things/" },
25 | { label: thing?.name || "Thing" },
26 | ])
27 | );
28 | }, [dispatch, thing]);
29 |
30 | if (status === "loading") {
31 | return Loading...
;
32 | }
33 |
34 | if (status === "failed") {
35 | return {error}
;
36 | }
37 |
38 | if (!thing) {
39 | return Thing not found
;
40 | }
41 |
42 | return (
43 |
44 |
45 | Name: {thing.name}
46 |
47 |
48 | Description: {thing.description}
49 |
50 | {isLoggedIn && (
51 |
52 | Owner:
53 |
54 |
55 | {thing.user_name}
56 |
57 |
58 |
59 | )}
60 |
61 | );
62 | };
63 |
64 | export default ThingDetail;
65 |
--------------------------------------------------------------------------------
/src/components/views/add-thing/AddThing.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useDispatch } from "react-redux";
3 | import { useNavigate } from "react-router-dom";
4 | import { setBreadcrumb } from "../../../slices/breadcrumbSlice";
5 | import { addThing } from "../../../slices/thingsSlice";
6 | import { showToast } from "../../../slices/toastSlice";
7 | import styles from "./add-thing.module.css";
8 |
9 | const AddThing = () => {
10 | const navigate = useNavigate();
11 | const dispatch = useDispatch();
12 | const [name, setName] = useState("");
13 | const [description, setDescription] = useState("");
14 |
15 | useEffect(() => {
16 | dispatch(
17 | setBreadcrumb([{ label: "Home", url: "/" }, { label: "Add thing" }])
18 | );
19 | }, [dispatch]);
20 |
21 | const handleSubmit = async (e) => {
22 | e.preventDefault();
23 | dispatch(addThing({ name, description }));
24 | dispatch(showToast(`${name} added!`));
25 | setName("");
26 | setDescription("");
27 | navigate("/my-things/");
28 | };
29 |
30 | return (
31 |
58 | );
59 | };
60 |
61 | export default AddThing;
62 |
--------------------------------------------------------------------------------
/src/routes.jsx:
--------------------------------------------------------------------------------
1 | import Root from "./components/ui/root/Root";
2 | import Home from "./components/views/home/Home";
3 | import Login from "./components/views/login/Login";
4 | import Register from "./components/views/register/Register";
5 | import ThingsList from "./components/views/things-list/ThingsList";
6 | import ThingDetail from "./components/views/thing-detail/ThingDetail";
7 | import MyThings from "./components/views/my-things/MyThings";
8 | import AddThing from "./components/views/add-thing/AddThing";
9 | import EditThing from "./components/views/edit-thing/EditThing";
10 | import DeleteThing from "./components/views/delete-thing/DeleteThing";
11 | import UsersList from "./components/views/users-list/UsersList";
12 | import UserDetail from "./components/views/user-detail/UserDetail";
13 | import ProtectedRoute from "./components/ui/protected-route/ProtectedRoute";
14 |
15 | const routes = [
16 | {
17 | path: "/",
18 | element: ,
19 | children: [
20 | {
21 | path: "",
22 | element: ,
23 | },
24 | {
25 | path: "login/",
26 | element: ,
27 | },
28 | {
29 | path: "register/",
30 | element: ,
31 | },
32 | {
33 | path: "things/",
34 | element: ,
35 | },
36 | {
37 | path: "things/:id/",
38 | element: ,
39 | },
40 | {
41 | path: "my-things/",
42 | element: } />,
43 | },
44 | {
45 | path: "my-things/add/",
46 | element: } />,
47 | },
48 | {
49 | path: "my-things/:id/edit/",
50 | element: } />,
51 | },
52 | {
53 | path: "my-things/:id/delete/",
54 | element: } />,
55 | },
56 | {
57 | path: "users/",
58 | element: } />,
59 | },
60 | {
61 | path: "users/:id/",
62 | element: } />,
63 | },
64 | ],
65 | },
66 | ];
67 |
68 | export default routes;
69 |
--------------------------------------------------------------------------------
/src/components/views/login/Login.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { useNavigate } from "react-router-dom";
4 | import { login } from "../../../slices/authSlice";
5 | import { setBreadcrumb } from "../../../slices/breadcrumbSlice";
6 | import styles from "./login.module.css";
7 |
8 | const Login = () => {
9 | const [email, setEmail] = useState("");
10 | const [password, setPassword] = useState("");
11 | const dispatch = useDispatch();
12 | const navigate = useNavigate();
13 | const authStatus = useSelector((state) => state.auth.status);
14 | const authError = useSelector((state) => state.auth.error);
15 |
16 | useEffect(() => {
17 | dispatch(setBreadcrumb([{ label: "Home", url: "/" }, { label: "Login" }]));
18 | }, [dispatch]);
19 |
20 | const handleLogin = async (e) => {
21 | e.preventDefault();
22 | try {
23 | await dispatch(login({ email, password })).unwrap();
24 | navigate("/my-things/");
25 | } catch (error) {
26 | console.error(error);
27 | }
28 | };
29 |
30 | return (
31 |
32 |
63 |
64 | );
65 | };
66 |
67 | export default Login;
68 |
--------------------------------------------------------------------------------
/src/components/views/user-detail/UserDetail.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { NavLink, useParams } from "react-router-dom";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { fetchUser } from "../../../slices/usersSlice";
5 | import { fetchThingsByUser } from "../../../slices/thingsSlice";
6 | import { setBreadcrumb } from "../../../slices/breadcrumbSlice";
7 | import styles from "./user-detail.module.css";
8 |
9 | const UserDetail = () => {
10 | const { id } = useParams();
11 | const dispatch = useDispatch();
12 | const user = useSelector((state) => state.users.user);
13 | const userStatus = useSelector((state) => state.users.userStatus);
14 | const userError = useSelector((state) => state.users.userError);
15 | const things = useSelector((state) => state.things.userThings);
16 | const thingsStatus = useSelector((state) => state.things.userThingsStatus);
17 | const thingsError = useSelector((state) => state.things.userThingsError);
18 |
19 | useEffect(() => {
20 | dispatch(fetchUser(id));
21 | }, [dispatch, id]);
22 |
23 | useEffect(() => {
24 | dispatch(fetchThingsByUser(id));
25 | }, [dispatch, id]);
26 |
27 | useEffect(() => {
28 | dispatch(
29 | setBreadcrumb([
30 | { label: "Home", url: "/" },
31 | { label: "Users", url: "/users/" },
32 | { label: user?.name || "User" },
33 | ])
34 | );
35 | }, [dispatch, user]);
36 |
37 | if (userStatus === "loading" || thingsStatus === "loading") {
38 | return Loading...
;
39 | }
40 |
41 | if (userStatus === "failed") {
42 | return {userError}
;
43 | }
44 |
45 | if (thingsStatus === "failed") {
46 | return {thingsError}
;
47 | }
48 |
49 | if (!user) {
50 | return User not found
;
51 | }
52 |
53 | return (
54 |
55 |
56 | Name: {user.name}
57 |
58 |
59 | Bio: {user.bio}
60 |
61 |
62 | Things:
63 |
64 |
65 | {things.map((thing) => (
66 | -
67 |
68 | {thing.name}
69 |
70 |
71 | ))}
72 |
73 |
74 | );
75 | };
76 |
77 | export default UserDetail;
78 |
--------------------------------------------------------------------------------
/src/components/views/edit-thing/EditThing.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { useParams, useNavigate } from "react-router-dom";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { fetchThings, editThing } from "../../../slices/thingsSlice";
5 | import { setBreadcrumb } from "../../../slices/breadcrumbSlice";
6 | import { showToast } from "../../../slices/toastSlice";
7 | import styles from "./edit-thing.module.css";
8 |
9 | const EditThing = () => {
10 | const { id } = useParams();
11 | const navigate = useNavigate();
12 | const dispatch = useDispatch();
13 | const thing = useSelector((state) =>
14 | state.things.items.find((thing) => thing.id === parseInt(id))
15 | );
16 |
17 | const [name, setName] = useState(thing ? thing.name : "");
18 | const [description, setDescription] = useState(
19 | thing ? thing.description : ""
20 | );
21 |
22 | useEffect(() => {
23 | dispatch(
24 | setBreadcrumb([
25 | { label: "Home", url: "/" },
26 | { label: "My things", url: "/my-things/" },
27 | { label: "Edit thing" },
28 | ])
29 | );
30 | }, [dispatch]);
31 |
32 | useEffect(() => {
33 | if (!thing) {
34 | dispatch(fetchThings());
35 | }
36 | }, [thing, dispatch]);
37 |
38 | useEffect(() => {
39 | if (thing) {
40 | setName(thing.name);
41 | setDescription(thing.description);
42 | }
43 | }, [thing]);
44 |
45 | const handleSubmit = async (e) => {
46 | e.preventDefault();
47 | dispatch(editThing({ id, updatedThing: { name, description } }));
48 | dispatch(showToast(`${name} updated!`));
49 | navigate("/my-things/");
50 | };
51 |
52 | return (
53 |
54 |
80 |
81 | );
82 | };
83 |
84 | export default EditThing;
85 |
--------------------------------------------------------------------------------
/src/components/views/register/Register.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useDispatch } from "react-redux";
3 | import { useNavigate } from "react-router-dom";
4 | import authService from "../../../services/authService";
5 | import { login } from "../../../slices/authSlice";
6 | import { setBreadcrumb } from "../../../slices/breadcrumbSlice";
7 | import styles from "./register.module.css";
8 |
9 | const Register = () => {
10 | const [name, setName] = useState("");
11 | const [email, setEmail] = useState("");
12 | const [password, setPassword] = useState("");
13 | const [bio, setBio] = useState("");
14 | const dispatch = useDispatch();
15 | const navigate = useNavigate();
16 |
17 | useEffect(() => {
18 | dispatch(
19 | setBreadcrumb([{ label: "Home", url: "/" }, { label: "Register" }])
20 | );
21 | }, [dispatch]);
22 |
23 | const handleRegister = async (e) => {
24 | e.preventDefault();
25 | try {
26 | await authService.register(name, email, password, bio);
27 | await dispatch(login({ email, password })).unwrap();
28 | navigate("/my-things/add/");
29 | } catch (error) {
30 | console.error(error);
31 | }
32 | };
33 |
34 | return (
35 |
89 | );
90 | };
91 |
92 | export default Register;
93 |
--------------------------------------------------------------------------------
/src/slices/thingsSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
2 | import apiService from "../services/apiService";
3 |
4 | export const fetchThings = createAsyncThunk("things/fetchThings", async () => {
5 | const response = await apiService("things", { method: "GET" });
6 | return response;
7 | });
8 |
9 | export const fetchThingsByUser = createAsyncThunk(
10 | "things/fetchThingsByUser",
11 | async (userId) => {
12 | const response = await apiService(`users/${userId}/things`, {
13 | method: "GET",
14 | });
15 | return response;
16 | }
17 | );
18 |
19 | export const fetchMyThings = createAsyncThunk(
20 | "things/fetchMyThings",
21 | async () => {
22 | const response = await apiService(`my-things`, {
23 | method: "GET",
24 | });
25 | return response;
26 | }
27 | );
28 |
29 | export const fetchThing = createAsyncThunk("things/fetchThing", async (id) => {
30 | return await apiService(`things/${id}`, { method: "GET" });
31 | });
32 |
33 | export const addThing = createAsyncThunk(
34 | "things/addThing",
35 | async (newThing) => {
36 | const response = await apiService("my-things", {
37 | method: "POST",
38 | body: JSON.stringify(newThing),
39 | });
40 | return response;
41 | }
42 | );
43 |
44 | export const editThing = createAsyncThunk(
45 | "things/editThing",
46 | async ({ id, updatedThing }) => {
47 | const response = await apiService(`my-things/${id}`, {
48 | method: "PUT",
49 | body: JSON.stringify(updatedThing),
50 | });
51 | return response;
52 | }
53 | );
54 |
55 | export const deleteThing = createAsyncThunk(
56 | "things/deleteThing",
57 | async (id) => {
58 | await apiService(`my-things/${id}`, { method: "DELETE" });
59 | return id;
60 | }
61 | );
62 |
63 | const thingsSlice = createSlice({
64 | name: "things",
65 | initialState: {
66 | items: [],
67 | userThings: [],
68 | currentThing: null,
69 | status: "idle",
70 | userThingsStatus: "idle",
71 | currentThingStatus: "idle",
72 | error: null,
73 | userThingsError: null,
74 | currentThingError: null,
75 | },
76 | extraReducers: (builder) => {
77 | builder
78 | .addCase(fetchThings.pending, (state) => {
79 | state.status = "loading";
80 | })
81 | .addCase(fetchThings.fulfilled, (state, action) => {
82 | state.status = "succeeded";
83 | state.items = action.payload;
84 | })
85 | .addCase(fetchThings.rejected, (state, action) => {
86 | state.status = "failed";
87 | state.error = action.error.message;
88 | })
89 | .addCase(fetchThingsByUser.pending, (state) => {
90 | state.userThingsStatus = "loading";
91 | })
92 | .addCase(fetchThingsByUser.fulfilled, (state, action) => {
93 | state.userThingsStatus = "succeeded";
94 | state.userThings = action.payload;
95 | })
96 | .addCase(fetchThingsByUser.rejected, (state, action) => {
97 | state.userThingsStatus = "failed";
98 | state.userThingsError = action.error.message;
99 | })
100 | .addCase(fetchMyThings.pending, (state) => {
101 | state.userThingsStatus = "loading";
102 | })
103 | .addCase(fetchMyThings.fulfilled, (state, action) => {
104 | state.userThingsStatus = "succeeded";
105 | state.userThings = action.payload;
106 | })
107 | .addCase(fetchMyThings.rejected, (state, action) => {
108 | state.userThingsStatus = "failed";
109 | state.userThingsError = action.error.message;
110 | })
111 | .addCase(fetchThing.pending, (state) => {
112 | state.currentThingStatus = "loading";
113 | })
114 | .addCase(fetchThing.fulfilled, (state, action) => {
115 | state.currentThingStatus = "succeeded";
116 | state.currentThing = action.payload;
117 | })
118 | .addCase(fetchThing.rejected, (state, action) => {
119 | state.currentThingStatus = "failed";
120 | state.currentThingError = action.error.message;
121 | })
122 | .addCase(addThing.fulfilled, (state, action) => {
123 | state.items.push(action.payload);
124 | })
125 | .addCase(editThing.fulfilled, (state, action) => {
126 | const index = state.items.findIndex(
127 | (item) => item.id === action.payload.id
128 | );
129 | state.items[index] = action.payload;
130 | })
131 | .addCase(deleteThing.fulfilled, (state, action) => {
132 | state.items = state.items.filter((item) => item.id !== action.payload);
133 | });
134 | },
135 | });
136 |
137 | export default thingsSlice.reducer;
138 |
--------------------------------------------------------------------------------