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,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> &amp; <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 &amp; 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,9 @@
1
+ const Home = () => {
2
+ return (
3
+ <div>
4
+ <h1>Welcome to the Home Page</h1>
5
+ </div>
6
+ )
7
+ }
8
+
9
+ export default Home
@@ -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,9 @@
1
+ const Penalty = () => {
2
+ return (
3
+ <div>
4
+ <h1>Welcome to the Penalty Page.</h1>
5
+ </div>
6
+ )
7
+ }
8
+
9
+ export default Penalty
@@ -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