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.
@@ -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
+ }
@@ -0,0 +1,13 @@
1
+ import { Outlet } from "react-router-dom"
2
+ import Navbar from "../components/Navbar"
3
+
4
+ export const PublicLayout = () => {
5
+ return (
6
+ <div>
7
+ <Navbar />
8
+ <main>
9
+ <Outlet />
10
+ </main>
11
+ </div>
12
+ )
13
+ }
package/src/main.jsx ADDED
@@ -0,0 +1,10 @@
1
+ import { BrowserRouter } from 'react-router-dom'
2
+ import App from './App.jsx'
3
+ import './App.css'
4
+ import { createRoot } from 'react-dom/client'
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <BrowserRouter>
8
+ <App />
9
+ </BrowserRouter>,
10
+ )
@@ -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