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,414 @@
|
|
|
1
|
+
import { useState, useMemo } from "react"
|
|
2
|
+
import { useData } from "../context/useData"
|
|
3
|
+
import Button from "../components/Button"
|
|
4
|
+
import { Table } from "../components/Table"
|
|
5
|
+
import { Modal } from "../components/Modal"
|
|
6
|
+
import Input from "../components/Input"
|
|
7
|
+
import { toast } from "react-toastify"
|
|
8
|
+
|
|
9
|
+
const RESOURCE_OPTIONS = [
|
|
10
|
+
{ key: "posts", label: "Posts", description: "Articles & Announcements" },
|
|
11
|
+
{ key: "todos", label: "Todos", description: "Tasks & Workflows" },
|
|
12
|
+
{ key: "users", label: "Users", description: "Team & Member Directory" },
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
const DashBoard = () => {
|
|
16
|
+
const [selectedResource, setSelectedResource] = useState("posts")
|
|
17
|
+
const [searchTerm, setSearchTerm] = useState("")
|
|
18
|
+
const [currentPage, setCurrentPage] = useState(1)
|
|
19
|
+
const pageSize = 8
|
|
20
|
+
|
|
21
|
+
// Modal states
|
|
22
|
+
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
|
|
23
|
+
const [isViewModalOpen, setIsViewModalOpen] = useState(false)
|
|
24
|
+
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
|
|
25
|
+
const [activeItem, setActiveItem] = useState(null)
|
|
26
|
+
const [newTitle, setNewTitle] = useState("")
|
|
27
|
+
const [newBody, setNewBody] = useState("")
|
|
28
|
+
|
|
29
|
+
// Dynamic workflow via DataContext
|
|
30
|
+
const {
|
|
31
|
+
collection,
|
|
32
|
+
loading,
|
|
33
|
+
error,
|
|
34
|
+
refresh,
|
|
35
|
+
addData,
|
|
36
|
+
deleteData
|
|
37
|
+
} = useData(selectedResource)
|
|
38
|
+
|
|
39
|
+
// Reset pagination & search when resource switches
|
|
40
|
+
const handleResourceChange = (key) => {
|
|
41
|
+
setSelectedResource(key)
|
|
42
|
+
setCurrentPage(1)
|
|
43
|
+
setSearchTerm("")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Filter items based on search term
|
|
47
|
+
const filteredItems = useMemo(() => {
|
|
48
|
+
if (!collection) return []
|
|
49
|
+
if (!searchTerm.trim()) return collection
|
|
50
|
+
|
|
51
|
+
const query = searchTerm.toLowerCase()
|
|
52
|
+
return collection.filter((item) => {
|
|
53
|
+
const titleMatch = item.title ? String(item.title).toLowerCase().includes(query) : false
|
|
54
|
+
const nameMatch = item.name ? String(item.name).toLowerCase().includes(query) : false
|
|
55
|
+
const bodyMatch = item.body ? String(item.body).toLowerCase().includes(query) : false
|
|
56
|
+
const emailMatch = item.email ? String(item.email).toLowerCase().includes(query) : false
|
|
57
|
+
return titleMatch || nameMatch || bodyMatch || emailMatch
|
|
58
|
+
})
|
|
59
|
+
}, [collection, searchTerm])
|
|
60
|
+
|
|
61
|
+
// Pagination calculation
|
|
62
|
+
const totalPages = Math.max(1, Math.ceil(filteredItems.length / pageSize))
|
|
63
|
+
const paginatedItems = useMemo(() => {
|
|
64
|
+
const start = (currentPage - 1) * pageSize
|
|
65
|
+
return filteredItems.slice(start, start + pageSize)
|
|
66
|
+
}, [filteredItems, currentPage, pageSize])
|
|
67
|
+
|
|
68
|
+
// Dynamically configure table columns based on active resource
|
|
69
|
+
const columns = useMemo(() => {
|
|
70
|
+
if (selectedResource === "posts") {
|
|
71
|
+
return [
|
|
72
|
+
{ key: "id", title: "ID" },
|
|
73
|
+
{ key: "title", title: "Title" },
|
|
74
|
+
{ key: "preview", title: "Description" },
|
|
75
|
+
{ key: "userId", title: "User ID" },
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
if (selectedResource === "todos") {
|
|
79
|
+
return [
|
|
80
|
+
{ key: "id", title: "ID" },
|
|
81
|
+
{ key: "title", title: "Task Title" },
|
|
82
|
+
{ key: "statusText", title: "Status" },
|
|
83
|
+
{ key: "userId", title: "Assignee ID" },
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
return [
|
|
87
|
+
{ key: "id", title: "ID" },
|
|
88
|
+
{ key: "name", title: "Full Name" },
|
|
89
|
+
{ key: "email", title: "Email Address" },
|
|
90
|
+
{ key: "companyName", title: "Company" },
|
|
91
|
+
]
|
|
92
|
+
}, [selectedResource])
|
|
93
|
+
|
|
94
|
+
// Format data for table display
|
|
95
|
+
const tableData = useMemo(() => {
|
|
96
|
+
return paginatedItems.map((item) => ({
|
|
97
|
+
...item,
|
|
98
|
+
preview: item.body ? (item.body.length > 75 ? `${item.body.slice(0, 75)}...` : item.body) : "-",
|
|
99
|
+
statusText: item.completed !== undefined ? (item.completed ? "✓ Completed" : "⏳ Pending") : "-",
|
|
100
|
+
companyName: item.company?.name || "-",
|
|
101
|
+
}))
|
|
102
|
+
}, [paginatedItems])
|
|
103
|
+
|
|
104
|
+
// Action handlers
|
|
105
|
+
const handleView = (item) => {
|
|
106
|
+
setActiveItem(item)
|
|
107
|
+
setIsViewModalOpen(true)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const handleDeletePrompt = (item) => {
|
|
111
|
+
setActiveItem(item)
|
|
112
|
+
setIsDeleteModalOpen(true)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const confirmDelete = () => {
|
|
116
|
+
if (!activeItem) return
|
|
117
|
+
deleteData((item) => item.id !== activeItem.id)
|
|
118
|
+
toast.success("Item removed from dynamic context collection!")
|
|
119
|
+
setIsDeleteModalOpen(false)
|
|
120
|
+
setActiveItem(null)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const handleCreateItem = (e) => {
|
|
124
|
+
if (e) e.preventDefault()
|
|
125
|
+
if (!newTitle.trim()) {
|
|
126
|
+
toast.error("Please enter a title")
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const newItem = {
|
|
131
|
+
id: Date.now(),
|
|
132
|
+
title: newTitle.trim(),
|
|
133
|
+
body: newBody.trim() || "Dynamic context record added dynamically.",
|
|
134
|
+
userId: 1,
|
|
135
|
+
completed: false,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
addData(newItem)
|
|
139
|
+
toast.success("New record dynamically added to context!")
|
|
140
|
+
setNewTitle("")
|
|
141
|
+
setNewBody("")
|
|
142
|
+
setIsAddModalOpen(false)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const handleRefresh = async () => {
|
|
146
|
+
try {
|
|
147
|
+
await refresh()
|
|
148
|
+
toast.info(`Refreshed ${selectedResource} from placeholder API!`)
|
|
149
|
+
} catch {
|
|
150
|
+
toast.error("Failed to refresh data")
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const tableActions = [
|
|
155
|
+
{
|
|
156
|
+
label: "View",
|
|
157
|
+
onClick: handleView,
|
|
158
|
+
className: "bg-blue-600 hover:bg-blue-700 text-white font-medium py-1 px-3 rounded text-sm",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
label: "Delete",
|
|
162
|
+
onClick: handleDeletePrompt,
|
|
163
|
+
className: "bg-red-600 hover:bg-red-700 text-white font-medium py-1 px-3 rounded text-sm",
|
|
164
|
+
},
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div className="space-y-6">
|
|
169
|
+
{/* Header Banner */}
|
|
170
|
+
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200 flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
|
171
|
+
<div>
|
|
172
|
+
<div className="flex items-center gap-3">
|
|
173
|
+
<h1 className="text-2xl font-bold text-gray-800">Dynamic Workflow Dashboard</h1>
|
|
174
|
+
<span className="bg-green-100 text-green-800 text-xs font-semibold px-2.5 py-0.5 rounded-full border border-green-300">
|
|
175
|
+
Live Context Connected
|
|
176
|
+
</span>
|
|
177
|
+
</div>
|
|
178
|
+
<p className="text-sm text-gray-500 mt-1">
|
|
179
|
+
Dynamic data fetched and managed via <code className="bg-gray-100 px-1 py-0.5 rounded text-gray-700 font-mono text-xs">DataContext</code> & <code className="bg-gray-100 px-1 py-0.5 rounded text-gray-700 font-mono text-xs">useData</code> from public placeholder API (<code className="text-blue-600">jsonplaceholder.typicode.com/{selectedResource}</code>).
|
|
180
|
+
</p>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div className="flex items-center gap-2">
|
|
184
|
+
<Button
|
|
185
|
+
className="bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium py-2 px-4 rounded border border-gray-300 flex items-center gap-2"
|
|
186
|
+
text={loading ? "Fetching..." : "↻ Refresh"}
|
|
187
|
+
onClick={handleRefresh}
|
|
188
|
+
disabled={loading}
|
|
189
|
+
/>
|
|
190
|
+
<Button
|
|
191
|
+
className="bg-gray-700 hover:bg-gray-800 text-white font-medium py-2 px-4 rounded"
|
|
192
|
+
text="+ Add Record"
|
|
193
|
+
onClick={() => setIsAddModalOpen(true)}
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
{/* Metrics Cards */}
|
|
199
|
+
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
200
|
+
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-200">
|
|
201
|
+
<p className="text-xs font-medium text-gray-500 uppercase tracking-wider">Total Records</p>
|
|
202
|
+
<p className="text-2xl font-bold text-gray-800 mt-1">{collection?.length || 0}</p>
|
|
203
|
+
<span className="text-xs text-gray-400">Public API items in context</span>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-200">
|
|
207
|
+
<p className="text-xs font-medium text-gray-500 uppercase tracking-wider">Filtered Results</p>
|
|
208
|
+
<p className="text-2xl font-bold text-blue-600 mt-1">{filteredItems.length}</p>
|
|
209
|
+
<span className="text-xs text-gray-400">Matching current search</span>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-200">
|
|
213
|
+
<p className="text-xs font-medium text-gray-500 uppercase tracking-wider">Active Endpoint</p>
|
|
214
|
+
<p className="text-lg font-bold text-gray-700 mt-1 truncate">/{selectedResource}</p>
|
|
215
|
+
<span className="text-xs text-gray-400">JSONPlaceholder REST API</span>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-200">
|
|
219
|
+
<p className="text-xs font-medium text-gray-500 uppercase tracking-wider">Workflow Mode</p>
|
|
220
|
+
<p className="text-lg font-bold text-green-600 mt-1">Dynamic State</p>
|
|
221
|
+
<span className="text-xs text-gray-400">Mutations & live sync active</span>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
{/* Dynamic Resource Switcher & Controls */}
|
|
226
|
+
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-200 space-y-4">
|
|
227
|
+
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
|
228
|
+
{/* Resource Selector Tabs */}
|
|
229
|
+
<div className="flex flex-wrap gap-2">
|
|
230
|
+
{RESOURCE_OPTIONS.map((item) => (
|
|
231
|
+
<button
|
|
232
|
+
key={item.key}
|
|
233
|
+
type="button"
|
|
234
|
+
onClick={() => handleResourceChange(item.key)}
|
|
235
|
+
className={`px-4 py-2 rounded-md font-medium text-sm transition cursor-pointer ${
|
|
236
|
+
selectedResource === item.key
|
|
237
|
+
? "bg-gray-800 text-white shadow-sm"
|
|
238
|
+
: "bg-gray-100 hover:bg-gray-200 text-gray-700"
|
|
239
|
+
}`}
|
|
240
|
+
>
|
|
241
|
+
{item.label}
|
|
242
|
+
</button>
|
|
243
|
+
))}
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
{/* Search Input */}
|
|
247
|
+
<div className="w-full md:w-72">
|
|
248
|
+
<Input
|
|
249
|
+
type="text"
|
|
250
|
+
name="search"
|
|
251
|
+
value={searchTerm}
|
|
252
|
+
onChange={(e) => {
|
|
253
|
+
setSearchTerm(e.target.value)
|
|
254
|
+
setCurrentPage(1)
|
|
255
|
+
}}
|
|
256
|
+
placeholder={`Search ${selectedResource}...`}
|
|
257
|
+
/>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
{/* Data Container with Loading / Error / Content */}
|
|
262
|
+
{loading ? (
|
|
263
|
+
<div className="flex flex-col items-center justify-center py-16 text-gray-500 space-y-3">
|
|
264
|
+
<div className="animate-spin rounded-full h-10 w-10 border-4 border-gray-300 border-t-gray-800"></div>
|
|
265
|
+
<p className="text-sm font-medium">Fetching dynamic data from public placeholder API...</p>
|
|
266
|
+
</div>
|
|
267
|
+
) : error ? (
|
|
268
|
+
<div className="bg-red-50 border border-red-200 rounded-md p-4 text-center">
|
|
269
|
+
<p className="text-red-700 font-medium">Failed to fetch data from placeholder API.</p>
|
|
270
|
+
<p className="text-xs text-red-500 mt-1">{error.message || String(error)}</p>
|
|
271
|
+
<Button
|
|
272
|
+
className="mt-3 bg-red-600 hover:bg-red-700 text-white font-medium py-1.5 px-4 rounded text-sm"
|
|
273
|
+
text="Retry Connection"
|
|
274
|
+
onClick={handleRefresh}
|
|
275
|
+
/>
|
|
276
|
+
</div>
|
|
277
|
+
) : (
|
|
278
|
+
<div className="space-y-4">
|
|
279
|
+
<div className="overflow-x-auto">
|
|
280
|
+
<Table
|
|
281
|
+
data={tableData}
|
|
282
|
+
column={columns}
|
|
283
|
+
action={tableActions}
|
|
284
|
+
/>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
{/* Pagination Controls */}
|
|
288
|
+
{filteredItems.length > pageSize && (
|
|
289
|
+
<div className="flex justify-between items-center pt-3 border-t text-sm text-gray-600">
|
|
290
|
+
<span>
|
|
291
|
+
Showing {(currentPage - 1) * pageSize + 1} to{" "}
|
|
292
|
+
{Math.min(currentPage * pageSize, filteredItems.length)} of {filteredItems.length} records
|
|
293
|
+
</span>
|
|
294
|
+
<div className="flex items-center gap-2">
|
|
295
|
+
<Button
|
|
296
|
+
className="bg-gray-200 hover:bg-gray-300 text-gray-800 py-1 px-3 rounded text-sm"
|
|
297
|
+
text="← Prev"
|
|
298
|
+
disabled={currentPage === 1}
|
|
299
|
+
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
|
300
|
+
/>
|
|
301
|
+
<span className="font-semibold text-gray-700">
|
|
302
|
+
{currentPage} / {totalPages}
|
|
303
|
+
</span>
|
|
304
|
+
<Button
|
|
305
|
+
className="bg-gray-200 hover:bg-gray-300 text-gray-800 py-1 px-3 rounded text-sm"
|
|
306
|
+
text="Next →"
|
|
307
|
+
disabled={currentPage >= totalPages}
|
|
308
|
+
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
|
|
309
|
+
/>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
</div>
|
|
314
|
+
)}
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
{/* Add New Item Modal */}
|
|
318
|
+
<Modal
|
|
319
|
+
isOpen={isAddModalOpen}
|
|
320
|
+
onClose={() => setIsAddModalOpen(false)}
|
|
321
|
+
title={`Add Dynamic Record (${selectedResource.toUpperCase()})`}
|
|
322
|
+
submit={handleCreateItem}
|
|
323
|
+
>
|
|
324
|
+
<div className="space-y-4 py-2">
|
|
325
|
+
<div>
|
|
326
|
+
<label className="block text-sm font-semibold text-gray-700 mb-1">
|
|
327
|
+
Title / Name
|
|
328
|
+
</label>
|
|
329
|
+
<Input
|
|
330
|
+
type="text"
|
|
331
|
+
name="title"
|
|
332
|
+
value={newTitle}
|
|
333
|
+
onChange={(e) => setNewTitle(e.target.value)}
|
|
334
|
+
placeholder={`Enter new ${selectedResource} title...`}
|
|
335
|
+
/>
|
|
336
|
+
</div>
|
|
337
|
+
<div>
|
|
338
|
+
<label className="block text-sm font-semibold text-gray-700 mb-1">
|
|
339
|
+
Description / Content
|
|
340
|
+
</label>
|
|
341
|
+
<textarea
|
|
342
|
+
className="input w-full min-h-[90px]"
|
|
343
|
+
name="body"
|
|
344
|
+
value={newBody}
|
|
345
|
+
onChange={(e) => setNewBody(e.target.value)}
|
|
346
|
+
placeholder="Enter details..."
|
|
347
|
+
/>
|
|
348
|
+
</div>
|
|
349
|
+
<p className="text-xs text-gray-400 italic">
|
|
350
|
+
This item will be dynamically prepended to the context collection using <code className="font-mono">addData()</code>.
|
|
351
|
+
</p>
|
|
352
|
+
</div>
|
|
353
|
+
</Modal>
|
|
354
|
+
|
|
355
|
+
{/* View Item Details Modal */}
|
|
356
|
+
<Modal
|
|
357
|
+
isOpen={isViewModalOpen}
|
|
358
|
+
onClose={() => {
|
|
359
|
+
setIsViewModalOpen(false)
|
|
360
|
+
setActiveItem(null)
|
|
361
|
+
}}
|
|
362
|
+
title="Record Details"
|
|
363
|
+
submit={() => {
|
|
364
|
+
setIsViewModalOpen(false)
|
|
365
|
+
setActiveItem(null)
|
|
366
|
+
}}
|
|
367
|
+
>
|
|
368
|
+
{activeItem && (
|
|
369
|
+
<div className="space-y-3 py-2 text-sm text-gray-700">
|
|
370
|
+
<div className="bg-gray-50 p-3 rounded border">
|
|
371
|
+
<p className="font-bold text-gray-900 text-base">{activeItem.title || activeItem.name}</p>
|
|
372
|
+
{activeItem.email && <p className="text-xs text-blue-600 font-mono mt-0.5">{activeItem.email}</p>}
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
{activeItem.body && (
|
|
376
|
+
<div>
|
|
377
|
+
<p className="text-xs font-semibold text-gray-500 uppercase">Body</p>
|
|
378
|
+
<p className="mt-1 bg-white p-2 border rounded text-gray-800">{activeItem.body}</p>
|
|
379
|
+
</div>
|
|
380
|
+
)}
|
|
381
|
+
|
|
382
|
+
<div className="grid grid-cols-2 gap-2 text-xs text-gray-500">
|
|
383
|
+
<p><span className="font-semibold text-gray-700">Record ID:</span> {activeItem.id}</p>
|
|
384
|
+
{activeItem.userId && <p><span className="font-semibold text-gray-700">User ID:</span> {activeItem.userId}</p>}
|
|
385
|
+
{activeItem.completed !== undefined && (
|
|
386
|
+
<p>
|
|
387
|
+
<span className="font-semibold text-gray-700">Status:</span>{" "}
|
|
388
|
+
{activeItem.completed ? "Completed" : "Pending"}
|
|
389
|
+
</p>
|
|
390
|
+
)}
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
)}
|
|
394
|
+
</Modal>
|
|
395
|
+
|
|
396
|
+
{/* Confirm Delete Modal */}
|
|
397
|
+
<Modal
|
|
398
|
+
isOpen={isDeleteModalOpen}
|
|
399
|
+
onClose={() => {
|
|
400
|
+
setIsDeleteModalOpen(false)
|
|
401
|
+
setActiveItem(null)
|
|
402
|
+
}}
|
|
403
|
+
title="Confirm Delete"
|
|
404
|
+
submit={confirmDelete}
|
|
405
|
+
>
|
|
406
|
+
<p className="py-2 text-gray-700">
|
|
407
|
+
Are you sure you want to remove item #{activeItem?.id} (<strong>{activeItem?.title || activeItem?.name}</strong>) from the dynamic context collection?
|
|
408
|
+
</p>
|
|
409
|
+
</Modal>
|
|
410
|
+
</div>
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export default DashBoard
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import Button from "../components/Button";
|
|
3
|
+
import { Table } from "../components/Table";
|
|
4
|
+
import { mamberColumn, memberFields, STORAGE_KEYS } from "../constants/storageKeys";
|
|
5
|
+
import { addData, findData, getCollection } from "../uttils/persistence";
|
|
6
|
+
import { Modal } from "../components/Modal";
|
|
7
|
+
import Input from "../components/Input";
|
|
8
|
+
import { addMemberSchema } from "../validation/Validation";
|
|
9
|
+
import { useFormik } from "formik";
|
|
10
|
+
import { toast } from "react-toastify";
|
|
11
|
+
|
|
12
|
+
const Members = () => {
|
|
13
|
+
const [open, setOpen] = useState(false)
|
|
14
|
+
const users = getCollection(STORAGE_KEYS.USERS) || [];
|
|
15
|
+
|
|
16
|
+
const handleSignUp = (values, { setFieldError, resetForm }) => {
|
|
17
|
+
const existingUser = findData(STORAGE_KEYS.USER, (user) => user.email === values.email)
|
|
18
|
+
|
|
19
|
+
if (existingUser) {
|
|
20
|
+
setFieldError("email", "Email already exists")
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { confirmPassword, ...user } = values
|
|
25
|
+
const userData = {
|
|
26
|
+
id: crypto.randomUUID(),
|
|
27
|
+
...user
|
|
28
|
+
}
|
|
29
|
+
addData(STORAGE_KEYS.USERS, userData)
|
|
30
|
+
toast.success("User added successfull")
|
|
31
|
+
resetForm()
|
|
32
|
+
setOpen(false);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const formik = useFormik({
|
|
36
|
+
initialValues: {
|
|
37
|
+
username: "",
|
|
38
|
+
email: "",
|
|
39
|
+
password: "",
|
|
40
|
+
confirmPassword: "",
|
|
41
|
+
role: "member"
|
|
42
|
+
},
|
|
43
|
+
validationSchema: addMemberSchema,
|
|
44
|
+
onSubmit: handleSignUp
|
|
45
|
+
});
|
|
46
|
+
return (
|
|
47
|
+
<div>
|
|
48
|
+
<div className="flex justify-between items-center mb-4">
|
|
49
|
+
<h1 className="text-2xl font-bold">Members</h1>
|
|
50
|
+
<Button className="bg-gray-400 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded" text="Add Member" onClick={() => { setOpen(true) }} />
|
|
51
|
+
</div>
|
|
52
|
+
<Table data={users} column={mamberColumn}/>
|
|
53
|
+
<Modal isOpen={open} onClose={() => setOpen(false)} title="Create new user" submit={formik.handleSubmit}>
|
|
54
|
+
<form onSubmit={formik.handleSubmit} className="flex flex-col gap-4 border p-4 mb-4 rounded-md shadow-md">
|
|
55
|
+
<h1 className="border-b-1 p-2 font-bold">Please Sign Up</h1>
|
|
56
|
+
{memberFields.map((field, index) =>
|
|
57
|
+
<div key={field.name}>
|
|
58
|
+
<Input
|
|
59
|
+
type={field.type}
|
|
60
|
+
name={field.name}
|
|
61
|
+
value={formik.values[field.name]}
|
|
62
|
+
onChange={formik.handleChange}
|
|
63
|
+
onBlur={formik.handleBlur}
|
|
64
|
+
placeholder={field.placeholder}
|
|
65
|
+
options={field?.options}
|
|
66
|
+
/>
|
|
67
|
+
{formik.touched[field.name] && formik.errors[field.name] && (
|
|
68
|
+
<div>
|
|
69
|
+
<p className="text-red-400">{formik.errors[field.name]}</p>
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
</form>
|
|
75
|
+
</Modal>
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default Members
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { use, useState } from "react";
|
|
2
|
+
import Button from "../components/Button";
|
|
3
|
+
import { Table } from "../components/Table";
|
|
4
|
+
import { bookColumn, mybooksFields, 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 { myBookSchema } from "../validation/Validation";
|
|
11
|
+
|
|
12
|
+
const MyBooks = () => {
|
|
13
|
+
const [open, setOpen] = useState(false)
|
|
14
|
+
const [selectbook, setSelectBook] = useState(null)
|
|
15
|
+
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
|
|
16
|
+
const [deleteBook, setDeleteBook] = useState(null)
|
|
17
|
+
const books = getCollection(STORAGE_KEYS.BOOKS) || [];
|
|
18
|
+
const today = new Date();
|
|
19
|
+
const maxDate = new Date();
|
|
20
|
+
maxDate.setDate(maxDate.getDate() + 7);
|
|
21
|
+
const handleBorrowBook = (values, { setFieldError, resetForm }) => {
|
|
22
|
+
const user = getCollection(STORAGE_KEYS.CURRENT_USER)
|
|
23
|
+
console.log("user and book is here :::>>><<<", user, selectedBook)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const handleBorrow = (book) => {
|
|
27
|
+
setOpen(true)
|
|
28
|
+
setSelectBook(book)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const handleReturn = (book) => {
|
|
32
|
+
console.log("book", book)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
const formik = useFormik({
|
|
37
|
+
enableReinitialize: true,
|
|
38
|
+
initialValues: {
|
|
39
|
+
return_date: selectbook?.return_date || "",
|
|
40
|
+
},
|
|
41
|
+
validationSchema: myBookSchema(today),
|
|
42
|
+
onSubmit: handleBorrowBook
|
|
43
|
+
});
|
|
44
|
+
return (
|
|
45
|
+
<div>
|
|
46
|
+
<div className="flex justify-between items-center mb-4">
|
|
47
|
+
<h1 className="text-2xl font-bold">MyBooks</h1>
|
|
48
|
+
</div>
|
|
49
|
+
<Table data={books} column={bookColumn} action={[{ label: "Borrow Book", onClick: handleBorrow, className: "bg-green-700 hover:bg-green-900 text-white" }, { label: "Return Book", onClick: handleReturn, className: "bg-red-700 hover:bg-red-900 text-white" }]} />
|
|
50
|
+
<Modal isOpen={open} onClose={() => setOpen(false)} title="Borrow Book" submit={formik.handleSubmit}>
|
|
51
|
+
<form onSubmit={formik.handleSubmit} className="flex flex-col gap-4 border p-4 mb-4 rounded-md shadow-md">
|
|
52
|
+
<h5>Please select book return date</h5>
|
|
53
|
+
{mybooksFields.map((field, index) =>
|
|
54
|
+
<div key={field.name}>
|
|
55
|
+
<Input
|
|
56
|
+
type={field.type}
|
|
57
|
+
name={field.name}
|
|
58
|
+
value={formik.values[field.name]}
|
|
59
|
+
onChange={formik.handleChange}
|
|
60
|
+
onBlur={formik.handleBlur}
|
|
61
|
+
placeholder={field.placeholder}
|
|
62
|
+
options={field?.options}
|
|
63
|
+
min={today.toISOString().split("T")[0]}
|
|
64
|
+
max={maxDate.toISOString().split("T")[0]}
|
|
65
|
+
/>
|
|
66
|
+
{formik.touched[field.name] && formik.errors[field.name] && (
|
|
67
|
+
<div>
|
|
68
|
+
<p className="text-red-400">{formik.errors[field.name]}</p>
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
</form>
|
|
74
|
+
</Modal>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default MyBooks
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useId, useState } from "react";
|
|
2
|
+
import Button from "../components/Button";
|
|
3
|
+
import Input from "../components/Input";
|
|
4
|
+
import { useFormik } from "formik";
|
|
5
|
+
import { signUpSchema } from "../validation/Validation";
|
|
6
|
+
import { addData, findData } from "../uttils/persistence";
|
|
7
|
+
import { signUpFields, STORAGE_KEYS } from "../constants/storageKeys";
|
|
8
|
+
import { toast } from "react-toastify";
|
|
9
|
+
import { useNavigate } from "react-router-dom";
|
|
10
|
+
|
|
11
|
+
const SignUp = () => {
|
|
12
|
+
const navigate = useNavigate()
|
|
13
|
+
const id = useId();
|
|
14
|
+
const handleSignUp = (values, { setFieldError, resetForm }) => {
|
|
15
|
+
const ExistingUser = findData(STORAGE_KEYS.USERS, (user) => user.email === values.email)
|
|
16
|
+
if (ExistingUser) {
|
|
17
|
+
setFieldError("email", "Email already exists.");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const { confirmPassword, ...user } = values
|
|
21
|
+
const userData = {
|
|
22
|
+
id: crypto.randomUUID(),
|
|
23
|
+
role: "member",
|
|
24
|
+
...user
|
|
25
|
+
}
|
|
26
|
+
addData(STORAGE_KEYS.USERS, userData)
|
|
27
|
+
toast.success("Sign Up successful! Please Sign In.")
|
|
28
|
+
resetForm()
|
|
29
|
+
navigate("/signin")
|
|
30
|
+
}
|
|
31
|
+
const formik = useFormik({
|
|
32
|
+
initialValues: {
|
|
33
|
+
username: "",
|
|
34
|
+
email: "",
|
|
35
|
+
password: "",
|
|
36
|
+
confirmPassword: ""
|
|
37
|
+
},
|
|
38
|
+
validationSchema: signUpSchema,
|
|
39
|
+
onSubmit: handleSignUp
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="flex flex-col gap-4 items-center justify-center h-screen">
|
|
44
|
+
<form onSubmit={formik.handleSubmit} className="w-[20%] flex flex-col gap-4 border p-4 rounded-md shadow-md">
|
|
45
|
+
<h1 className="border-b-1 p-2 font-bold">Please Sign Up</h1>
|
|
46
|
+
{signUpFields.map((field, index) =>
|
|
47
|
+
<div key={field.name}>
|
|
48
|
+
<Input
|
|
49
|
+
type={field.type}
|
|
50
|
+
name={field.name}
|
|
51
|
+
value={formik.values[field.name]}
|
|
52
|
+
onChange={formik.handleChange}
|
|
53
|
+
onBlur={formik.handleBlur}
|
|
54
|
+
placeholder={field.placeholder}
|
|
55
|
+
/>
|
|
56
|
+
{formik.touched[field.name] && formik.errors[field.name] && (
|
|
57
|
+
<div>
|
|
58
|
+
<p className="text-red-400">{formik.errors[field.name]}</p>
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
)}
|
|
63
|
+
<Button text="Sign Up" type="submit" />
|
|
64
|
+
</form>
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default SignUp
|