matarial-init 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/bin/cli.js +7 -0
- package/bin/copy-src.js +86 -0
- package/bin/postinstall.js +6 -0
- package/index.js +9 -0
- package/package.json +49 -0
- package/src/App.css +24 -0
- package/src/App.jsx +51 -0
- package/src/components/Button.jsx +21 -0
- package/src/components/Header.jsx +29 -0
- package/src/components/Input.jsx +33 -0
- package/src/components/Modal.jsx +43 -0
- package/src/components/Navbar.jsx +19 -0
- package/src/components/Sidebar.jsx +30 -0
- package/src/components/Table.jsx +56 -0
- package/src/constants/permissions.js +27 -0
- package/src/constants/storageKeys.js +151 -0
- package/src/context/DataContext.jsx +19 -0
- package/src/context/dataContext.js +22 -0
- package/src/context/useData.jsx +98 -0
- package/src/hooks/usePermission.jsx +14 -0
- package/src/index.css +0 -0
- package/src/layout/DashboardLayout.jsx +31 -0
- package/src/layout/PublicLayout.jsx +13 -0
- package/src/main.jsx +10 -0
- package/src/pages/Books.jsx +121 -0
- package/src/pages/DashBoard.jsx +414 -0
- package/src/pages/Home.jsx +9 -0
- package/src/pages/Members.jsx +80 -0
- package/src/pages/MyBooks.jsx +79 -0
- package/src/pages/Penalty.jsx +9 -0
- package/src/pages/SignUp.jsx +69 -0
- package/src/pages/Signin.jsx +73 -0
- package/src/routes/ProtectedRoute.jsx +11 -0
- package/src/routes/PublicRoute.jsx +11 -0
- package/src/uttils/persistence.js +49 -0
- package/src/validation/Validation.jsx +41 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
export const STORAGE_KEYS = {
|
|
2
|
+
USERS: "users",
|
|
3
|
+
CURRENT_USER: "currentUser",
|
|
4
|
+
BOOKS: "books"
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const SIDEBAR_MENU = [
|
|
8
|
+
{
|
|
9
|
+
id: 1,
|
|
10
|
+
title: "Dashboard",
|
|
11
|
+
path: "/dashboard",
|
|
12
|
+
permission: "VIEW_DASHBOARD"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 2,
|
|
16
|
+
title: "Books",
|
|
17
|
+
path: "/books",
|
|
18
|
+
permission: "VIEW_BOOKS"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 3,
|
|
22
|
+
title: "MyBooks",
|
|
23
|
+
path: "/mybooks",
|
|
24
|
+
permission: "VIEW_MyBOOKS"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 4,
|
|
28
|
+
title: "Members",
|
|
29
|
+
path: "/members",
|
|
30
|
+
permission: "VIEW_MEMBER"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 5,
|
|
34
|
+
title: "Penalty",
|
|
35
|
+
path: "/penalty",
|
|
36
|
+
permission: "VIEW_PANALTY"
|
|
37
|
+
}
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export const signUpFields = [
|
|
41
|
+
{
|
|
42
|
+
name: "username",
|
|
43
|
+
type: "text",
|
|
44
|
+
placeholder: "Username"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "email",
|
|
48
|
+
type: "email",
|
|
49
|
+
placeholder: "Email"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "password",
|
|
53
|
+
type: "password",
|
|
54
|
+
placeholder: "Password"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "confirmPassword",
|
|
58
|
+
type: "password",
|
|
59
|
+
placeholder: "Confirm Password"
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
export const memberFields = [
|
|
64
|
+
{
|
|
65
|
+
name: "username",
|
|
66
|
+
type: "text",
|
|
67
|
+
placeholder: "Username"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "email",
|
|
71
|
+
type: "email",
|
|
72
|
+
placeholder: "Email"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "password",
|
|
76
|
+
type: "password",
|
|
77
|
+
placeholder: "Password"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "confirmPassword",
|
|
81
|
+
type: "password",
|
|
82
|
+
placeholder: "Confirm Password"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "role",
|
|
86
|
+
type: "select",
|
|
87
|
+
placeholder: "Role",
|
|
88
|
+
options: [
|
|
89
|
+
{ value: "member", label: "Member" },
|
|
90
|
+
{ value: "librarian", label: "Librarian" },
|
|
91
|
+
{ value: "admin", label: "Admin" },
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
export const booksFields = [
|
|
97
|
+
{
|
|
98
|
+
name: "title",
|
|
99
|
+
type: "text",
|
|
100
|
+
placeholder: "Book Name"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "author",
|
|
104
|
+
type: "text",
|
|
105
|
+
placeholder: "Author Name"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "isbn",
|
|
109
|
+
type: "text",
|
|
110
|
+
placeholder: "ISBN Number"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "category",
|
|
114
|
+
type: "text",
|
|
115
|
+
placeholder: "category Name"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "quantity",
|
|
119
|
+
type: "number",
|
|
120
|
+
placeholder: "NBooks Quantity"
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
export const mybooksFields = [
|
|
125
|
+
{
|
|
126
|
+
name: "returndate",
|
|
127
|
+
type: "date",
|
|
128
|
+
placeholder: "Return date"
|
|
129
|
+
},
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
export const mamberColumn = [
|
|
133
|
+
{ key: "username", title: "UserName" },
|
|
134
|
+
{ key: "email", title: "Email" },
|
|
135
|
+
{ key: "role", title: "Role" }
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
export const bookColumn = [
|
|
139
|
+
{ key: "title", title: "Title" },
|
|
140
|
+
{ key: "author", title: "Author" },
|
|
141
|
+
{ key: "isbn", title: "ISBN" },
|
|
142
|
+
{ key: "category", title: "Category" },
|
|
143
|
+
{ key: "quantity", title: "Quantity" }
|
|
144
|
+
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
export const Role = {
|
|
148
|
+
ADMIN: "admin",
|
|
149
|
+
Member: "member",
|
|
150
|
+
LIBRARIAN: "librarian"
|
|
151
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useMemo } from "react"
|
|
2
|
+
import { DataContext, defaultSource } from "./dataContext"
|
|
3
|
+
|
|
4
|
+
export const DataProvider = ({
|
|
5
|
+
children,
|
|
6
|
+
source = defaultSource,
|
|
7
|
+
refreshInterval = null
|
|
8
|
+
}) => {
|
|
9
|
+
const value = useMemo(() => ({
|
|
10
|
+
source,
|
|
11
|
+
refreshInterval
|
|
12
|
+
}), [source, refreshInterval])
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<DataContext.Provider value={value}>
|
|
16
|
+
{children}
|
|
17
|
+
</DataContext.Provider>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createContext } from "react"
|
|
2
|
+
|
|
3
|
+
const PUBLIC_API_BASE = "https://jsonplaceholder.typicode.com"
|
|
4
|
+
|
|
5
|
+
// Default source handler that fetches from the public placeholder API (JSONPlaceholder)
|
|
6
|
+
export const defaultSource = {
|
|
7
|
+
get: async (key) => {
|
|
8
|
+
const url = key.startsWith("http://") || key.startsWith("https://")
|
|
9
|
+
? key
|
|
10
|
+
: `${PUBLIC_API_BASE}/${key.replace(/^\//, "")}`
|
|
11
|
+
|
|
12
|
+
const response = await fetch(url)
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
throw new Error(`Failed to fetch from ${url} (${response.status} ${response.statusText})`)
|
|
15
|
+
}
|
|
16
|
+
return await response.json()
|
|
17
|
+
},
|
|
18
|
+
set: async (key, nextData) => nextData,
|
|
19
|
+
remove: async () => null,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const DataContext = createContext(null)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useCallback, useContext, useEffect, useState } from "react"
|
|
2
|
+
import { DataContext } from "./dataContext"
|
|
3
|
+
|
|
4
|
+
export const useData = (key) => {
|
|
5
|
+
const context = useContext(DataContext)
|
|
6
|
+
const [data, setData] = useState(null)
|
|
7
|
+
const [loading, setLoading] = useState(true)
|
|
8
|
+
const [error, setError] = useState(null)
|
|
9
|
+
|
|
10
|
+
if (!context) {
|
|
11
|
+
throw new Error("useData must be used inside DataProvider")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { source, refreshInterval } = context
|
|
15
|
+
|
|
16
|
+
const refresh = useCallback(async () => {
|
|
17
|
+
setLoading(true)
|
|
18
|
+
try {
|
|
19
|
+
const nextData = await source.get(key)
|
|
20
|
+
setData(nextData)
|
|
21
|
+
setError(null)
|
|
22
|
+
return nextData
|
|
23
|
+
} catch (nextError) {
|
|
24
|
+
setError(nextError)
|
|
25
|
+
return null
|
|
26
|
+
} finally {
|
|
27
|
+
setLoading(false)
|
|
28
|
+
}
|
|
29
|
+
}, [key, source])
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
let isMounted = true
|
|
33
|
+
const timeoutId = setTimeout(() => {
|
|
34
|
+
if (isMounted) {
|
|
35
|
+
refresh().catch(() => {})
|
|
36
|
+
}
|
|
37
|
+
}, 0)
|
|
38
|
+
return () => {
|
|
39
|
+
isMounted = false
|
|
40
|
+
clearTimeout(timeoutId)
|
|
41
|
+
}
|
|
42
|
+
}, [refresh])
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!refreshInterval) return undefined
|
|
46
|
+
const intervalId = setInterval(() => {
|
|
47
|
+
refresh().catch(() => {})
|
|
48
|
+
}, refreshInterval)
|
|
49
|
+
return () => clearInterval(intervalId)
|
|
50
|
+
}, [refresh, refreshInterval])
|
|
51
|
+
|
|
52
|
+
const save = async (nextData) => {
|
|
53
|
+
setLoading(true)
|
|
54
|
+
try {
|
|
55
|
+
const savedData = await source.set(key, nextData)
|
|
56
|
+
setData(savedData)
|
|
57
|
+
setError(null)
|
|
58
|
+
return savedData
|
|
59
|
+
} catch (nextError) {
|
|
60
|
+
setError(nextError)
|
|
61
|
+
throw nextError
|
|
62
|
+
} finally {
|
|
63
|
+
setLoading(false)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const collection = Array.isArray(data) ? data : []
|
|
68
|
+
|
|
69
|
+
const runMutation = (mutation) => save(mutation(collection))
|
|
70
|
+
|
|
71
|
+
const clear = async () => {
|
|
72
|
+
setLoading(true)
|
|
73
|
+
try {
|
|
74
|
+
const nextData = await source.remove(key)
|
|
75
|
+
setData(nextData)
|
|
76
|
+
return nextData
|
|
77
|
+
} finally {
|
|
78
|
+
setLoading(false)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
data,
|
|
84
|
+
collection,
|
|
85
|
+
loading,
|
|
86
|
+
error,
|
|
87
|
+
refresh,
|
|
88
|
+
getData: () => data,
|
|
89
|
+
findData: (callback) => collection.find(callback),
|
|
90
|
+
filterData: (callback) => collection.filter(callback),
|
|
91
|
+
setData: save,
|
|
92
|
+
addData: (item) => runMutation((items) => [item, ...items]),
|
|
93
|
+
updateData: (callback) => runMutation((items) => items.map(callback)),
|
|
94
|
+
deleteData: (callback) => runMutation((items) => items.filter(callback)),
|
|
95
|
+
clearData: clear,
|
|
96
|
+
existData: () => data !== null,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PERMISSIONS } from "../constants/permissions"
|
|
2
|
+
import { STORAGE_KEYS } from "../constants/storageKeys"
|
|
3
|
+
import { getCollection } from "../uttils/persistence"
|
|
4
|
+
|
|
5
|
+
export const usePermission = () => {
|
|
6
|
+
const currentUser = getCollection(STORAGE_KEYS.CURRENT_USER)
|
|
7
|
+
const hashPermission = (permission) => {
|
|
8
|
+
if (!currentUser) {
|
|
9
|
+
return false
|
|
10
|
+
}
|
|
11
|
+
return PERMISSIONS[permission]?.includes(currentUser?.role)
|
|
12
|
+
}
|
|
13
|
+
return { currentUser, hashPermission }
|
|
14
|
+
}
|
package/src/index.css
ADDED
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Outlet } from "react-router-dom"
|
|
2
|
+
import { Sidebar } from "../components/Sidebar"
|
|
3
|
+
import { Header } from "../components/Header"
|
|
4
|
+
|
|
5
|
+
export const DashboardLayout = () => {
|
|
6
|
+
return (
|
|
7
|
+
<div className="flex h-screen">
|
|
8
|
+
|
|
9
|
+
{/* Sidebar */}
|
|
10
|
+
<aside className="w-64 border-r">
|
|
11
|
+
<Sidebar />
|
|
12
|
+
</aside>
|
|
13
|
+
|
|
14
|
+
{/* Right Section */}
|
|
15
|
+
<div className="flex-1 flex flex-col">
|
|
16
|
+
|
|
17
|
+
{/* Header */}
|
|
18
|
+
<header className="h-16 border-b flex items-center px-4 bg-gray-600 text-white font-bold">
|
|
19
|
+
<Header />
|
|
20
|
+
</header>
|
|
21
|
+
|
|
22
|
+
{/* Main Content */}
|
|
23
|
+
<main className="flex-1 overflow-auto p-5">
|
|
24
|
+
<Outlet />
|
|
25
|
+
</main>
|
|
26
|
+
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
package/src/main.jsx
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { use, useState } from "react";
|
|
2
|
+
import Button from "../components/Button";
|
|
3
|
+
import { Table } from "../components/Table";
|
|
4
|
+
import { bookColumn, booksFields, STORAGE_KEYS } from "../constants/storageKeys";
|
|
5
|
+
import { addData, deleteData, findData, getCollection, updateData } from "../uttils/persistence";
|
|
6
|
+
import { Modal } from "../components/Modal";
|
|
7
|
+
import Input from "../components/Input";
|
|
8
|
+
import { useFormik } from "formik";
|
|
9
|
+
import { toast } from "react-toastify";
|
|
10
|
+
import { addBookSchema } from "../validation/Validation";
|
|
11
|
+
|
|
12
|
+
const Books = () => {
|
|
13
|
+
const [open, setOpen] = useState(false)
|
|
14
|
+
const [editBook, setEditBook] = useState(null)
|
|
15
|
+
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
|
|
16
|
+
const [deleteBook, setDeleteBook] = useState(null)
|
|
17
|
+
const books = getCollection(STORAGE_KEYS.BOOKS) || [];
|
|
18
|
+
|
|
19
|
+
const handleSignUp = (values, { setFieldError, resetForm }) => {
|
|
20
|
+
if (editBook) {
|
|
21
|
+
updateData(STORAGE_KEYS.BOOKS, (book) =>
|
|
22
|
+
book.id === editBook.id
|
|
23
|
+
? { ...book, ...values }
|
|
24
|
+
: book
|
|
25
|
+
);
|
|
26
|
+
toast.success("Book updated successfully")
|
|
27
|
+
resetForm()
|
|
28
|
+
setEditBook(null)
|
|
29
|
+
setOpen(false)
|
|
30
|
+
return
|
|
31
|
+
} else {
|
|
32
|
+
const existingBook = findData(STORAGE_KEYS.BOOKS, (book) => book.isbn === values.isbn)
|
|
33
|
+
if (existingBook) {
|
|
34
|
+
setFieldError("isbn", "isbn already exists")
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
addData(STORAGE_KEYS.BOOKS, {
|
|
38
|
+
id: crypto.randomUUID(),
|
|
39
|
+
...values
|
|
40
|
+
});
|
|
41
|
+
toast.success("Book added successfull")
|
|
42
|
+
resetForm()
|
|
43
|
+
setOpen(false);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const handleEdit = (book) => {
|
|
48
|
+
setOpen(true)
|
|
49
|
+
setEditBook(book)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const handleDelete = (book) => {
|
|
53
|
+
setDeleteBook(book)
|
|
54
|
+
setDeleteModalOpen(true)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const confirmDelete = () => {
|
|
58
|
+
deleteData(STORAGE_KEYS.BOOKS, ((item) => item.id !== deleteBook.id))
|
|
59
|
+
toast.success("Book Deleted Successfully")
|
|
60
|
+
setDeleteBook(null);
|
|
61
|
+
setDeleteModalOpen(false);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const formik = useFormik({
|
|
65
|
+
enableReinitialize: true,
|
|
66
|
+
initialValues: {
|
|
67
|
+
title: editBook?.title || "",
|
|
68
|
+
author: editBook?.author || "",
|
|
69
|
+
isbn: editBook?.isbn || "",
|
|
70
|
+
category: editBook?.category || "",
|
|
71
|
+
quantity: editBook?.quantity || 0
|
|
72
|
+
},
|
|
73
|
+
validationSchema: addBookSchema,
|
|
74
|
+
onSubmit: handleSignUp
|
|
75
|
+
});
|
|
76
|
+
return (
|
|
77
|
+
<div>
|
|
78
|
+
<div className="flex justify-between items-center mb-4">
|
|
79
|
+
<h1 className="text-2xl font-bold">Books</h1>
|
|
80
|
+
<Button className="bg-gray-400 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded" text="Add Books" onClick={() => { setOpen(true) }} />
|
|
81
|
+
</div>
|
|
82
|
+
<Table data={books} column={bookColumn} action={[{ label: "Edit", onClick: handleEdit, className: "bg-green-700 hover:bg-green-900 text-white" }, { label: "Delete", onClick: handleDelete, className: "bg-red-700 hover:bg-red-900 text-white" }]} />
|
|
83
|
+
<Modal isOpen={open} onClose={() => setOpen(false)} title="Create new user" submit={formik.handleSubmit}>
|
|
84
|
+
<form onSubmit={formik.handleSubmit} className="flex flex-col gap-4 border p-4 mb-4 rounded-md shadow-md">
|
|
85
|
+
<h1 className="border-b-1 p-2 font-bold">Create new Book Collection</h1>
|
|
86
|
+
{booksFields.map((field, index) =>
|
|
87
|
+
<div key={field.name}>
|
|
88
|
+
<Input
|
|
89
|
+
type={field.type}
|
|
90
|
+
name={field.name}
|
|
91
|
+
value={formik.values[field.name]}
|
|
92
|
+
onChange={formik.handleChange}
|
|
93
|
+
onBlur={formik.handleBlur}
|
|
94
|
+
placeholder={field.placeholder}
|
|
95
|
+
options={field?.options}
|
|
96
|
+
/>
|
|
97
|
+
{formik.touched[field.name] && formik.errors[field.name] && (
|
|
98
|
+
<div>
|
|
99
|
+
<p className="text-red-400">{formik.errors[field.name]}</p>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</form>
|
|
105
|
+
</Modal>
|
|
106
|
+
<Modal
|
|
107
|
+
isOpen={deleteModalOpen}
|
|
108
|
+
onClose={() => {
|
|
109
|
+
setDeleteModalOpen(false);
|
|
110
|
+
setDeleteBook(null);
|
|
111
|
+
}}
|
|
112
|
+
title="Delete Book"
|
|
113
|
+
submit={confirmDelete}
|
|
114
|
+
>
|
|
115
|
+
<p className="p-2 mb-2">{`Are you sure you want to delete ${deleteBook?.title}?`}</p>
|
|
116
|
+
</Modal>
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export default Books
|