diff --git a/package.json b/package.json index 57b0849..2193c17 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,12 @@ "test": "vitest" }, "dependencies": { + "@tanstack/react-form": "^0.14.0", "@tanstack/react-query": "^5.18.0", "@tanstack/react-query-devtools": "^5.18.0", "@types/js-cookie": "^3.0.6", "@uidotdev/usehooks": "^2.4.1", + "@tanstack/zod-form-adapter": "^0.14.0", "date-fns": "^3.3.1", "js-cookie": "^3.0.5", "localforage": "^1.10.0", @@ -23,7 +25,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.22.3", - "sort-by": "^1.2.0" + "sort-by": "^1.2.0", + "zod": "^3.22.4" }, "devDependencies": { "@testing-library/jest-dom": "^6.4.2", diff --git a/public/-icon-home.svg b/public/-icon-home.svg new file mode 100644 index 0000000..9712a9e --- /dev/null +++ b/public/-icon-home.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/-icon-mail.svg b/public/-icon-mail.svg new file mode 100644 index 0000000..8992a51 --- /dev/null +++ b/public/-icon-mail.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/-icon-phone.svg b/public/-icon-phone.svg new file mode 100644 index 0000000..f2c0439 --- /dev/null +++ b/public/-icon-phone.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/-icon-user.svg b/public/-icon-user.svg new file mode 100644 index 0000000..564c8e1 --- /dev/null +++ b/public/-icon-user.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/back-button@2x.png b/public/back-button@2x.png new file mode 100644 index 0000000..d702847 Binary files /dev/null and b/public/back-button@2x.png differ diff --git a/public/dropdown-filter-select.svg b/public/dropdown-filter-select.svg new file mode 100644 index 0000000..80b9d20 --- /dev/null +++ b/public/dropdown-filter-select.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/edit-profile-pic.svg b/public/edit-profile-pic.svg new file mode 100644 index 0000000..24f3fe2 --- /dev/null +++ b/public/edit-profile-pic.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/email.svg b/public/email.svg new file mode 100644 index 0000000..7ef405e --- /dev/null +++ b/public/email.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/public/fintechsoclogo-6@2x.png b/public/fintechsoclogo-6@2x.png new file mode 100644 index 0000000..2df70d3 Binary files /dev/null and b/public/fintechsoclogo-6@2x.png differ diff --git a/public/home.svg b/public/home.svg new file mode 100644 index 0000000..bceb07f --- /dev/null +++ b/public/home.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/public/image-5@2x.png b/public/image-5@2x.png new file mode 100644 index 0000000..2e497c9 Binary files /dev/null and b/public/image-5@2x.png differ diff --git a/public/phone.svg b/public/phone.svg new file mode 100644 index 0000000..a874110 --- /dev/null +++ b/public/phone.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/profile-button@2x.png b/public/profile-button@2x.png new file mode 100644 index 0000000..7343c31 Binary files /dev/null and b/public/profile-button@2x.png differ diff --git a/public/role.svg b/public/role.svg new file mode 100644 index 0000000..d37f361 --- /dev/null +++ b/public/role.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/assets/css/Navbar.module.css b/src/assets/css/Navbar.module.css new file mode 100644 index 0000000..34fdbfe --- /dev/null +++ b/src/assets/css/Navbar.module.css @@ -0,0 +1,127 @@ +.fintechsocLogo6Icon { + height: 4.063rem; + width: 9.125rem; + position: relative; + object-fit: cover; + } + .announcements { + align-self: stretch; + position: relative; + } + .announcementsFrame { + width: 10.938rem; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + padding: var(--padding-2xl) 0 0; + box-sizing: border-box; + } + .members { + align-self: stretch; + position: relative; + } + .attendanceFrame { + width: 8.75rem; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + padding: var(--padding-mid) var(--padding-9xs) 0 0; + box-sizing: border-box; + color: var(--brand-yellow); + } + .events { + align-self: stretch; + position: relative; + } + .attendanceFrame1 { + width: 6.938rem; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + padding: var(--padding-mid) 0 0; + box-sizing: border-box; + } + .tasks { + align-self: stretch; + position: relative; + } + .attendanceFrame2 { + width: 7.313rem; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + padding: var(--padding-base) 0.688rem 0 0; + box-sizing: border-box; + } + .attendance { + align-self: stretch; + position: relative; + } + .attendanceFrame3 { + width: 9.188rem; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + padding: var(--padding-base) 0.5rem 0 0; + box-sizing: border-box; + } + .recruitment { + align-self: stretch; + position: relative; + } + .attendanceFrame4 { + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + padding: var(--padding-base) 2.188rem 0 0; + } + .image5Icon { + height: 3.438rem; + width: 3.431rem; + position: relative; + object-fit: cover; + } + .desktopNavBarUpdatedAtt, + .eventsFrame, + .tasksFrame { + display: flex; + align-items: flex-start; + justify-content: flex-start; + } + .tasksFrame { + align-self: stretch; + flex-direction: row; + gap: 0 1.188rem; + } + .desktopNavBarUpdatedAtt, + .eventsFrame { + box-sizing: border-box; + max-width: 100%; + } + .eventsFrame { + flex: 1; + flex-direction: column; + padding: var(--padding-8xs) 0 0; + } + .desktopNavBarUpdatedAtt { + align-self: stretch; + background-color: var(--brand-blue); + overflow: hidden; + flex-direction: row; + padding: var(--padding-mid) 2.75rem var(--padding-base) var(--padding-xl); + gap: 0 1.188rem; + top: 0; + z-index: 99; + position: sticky; + text-align: center; + font-size: var(--font-size-xl); + color: var(--color-white); + font-family: var(--font-dm-sans); + } diff --git a/src/assets/css/ProfilePage.module.css b/src/assets/css/ProfilePage.module.css new file mode 100644 index 0000000..5a719dc --- /dev/null +++ b/src/assets/css/ProfilePage.module.css @@ -0,0 +1,584 @@ +.editProfilePic { + width: 4.688rem; + height: 4.688rem; + position: absolute; + margin: 0 !important; + bottom: 20.813rem; + left: 17.25rem; + z-index: 1; + } + .profile { + margin: 0; + align-self: stretch; + height: 4.063rem; + position: relative; + font-size: inherit; + font-weight: 500; + font-family: inherit; + display: flex; + align-items: center; + } + .profileName { + width: 17.563rem; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + padding: 0 0 var(--padding-2xl); + box-sizing: border-box; + } + .admin { + margin: 0; + align-self: stretch; + height: 2.125rem; + position: relative; + font-size: inherit; + font-weight: 500; + font-family: inherit; + display: flex; + align-items: center; + flex-shrink: 0; + } + .departmentText { + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + padding: 0.438rem 0 0; + } + .roleFrame { + height: 3rem; + width: 3rem; + position: relative; + border-radius: 12.63px; + background-color: var(--color-gainsboro); + } + .adminText, + .homeIconFrame { + display: flex; + flex-direction: row; + } + .homeIconFrame { + width: 10.188rem; + align-items: flex-start; + justify-content: flex-start; + font-size: var(--font-size-15xl); + color: var(--color-black); + } + .adminText { + width: 67.5rem; + align-items: flex-end; + justify-content: space-between; + gap: var(--gap-xl); + max-width: 100%; + } + .profileButton1 { + height: 15.625rem; + width: 15.625rem; + position: relative; + object-fit: cover; + } + .profileButton { + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: flex-start; + padding: 0 var(--padding-xl); + } + .name { + margin: 0; + align-self: stretch; + position: relative; + font-size: inherit; + font-weight: 500; + font-family: inherit; + } + .contactFrame { + width: 18.125rem; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + gap: 1.75rem 0; + min-width: 18.125rem; + } + .departmentChild { + height: 6.375rem; + width: 47.375rem; + position: relative; + border-radius: var(--br-3xs); + background-color: var(--brand-cream); + border: 1px solid var(--color-black); + box-sizing: border-box; + display: none; + max-width: 100%; + } + .department1, + .iconHome { + position: relative; + z-index: 1; + } + .iconHome { + height: 2.775rem; + width: 3.125rem; + } + .department1 { + height: 3rem; + flex: 1; + font-size: var(--font-size-15xl); + font-weight: 500; + font-family: var(--font-dm-sans); + color: var(--brand-dark-grey); + text-align: left; + display: flex; + align-items: center; + min-width: 9.625rem; + max-width: 100%; + } + .eventsFrame1 { + width: 30rem; + display: flex; + flex-direction: row; + align-items: flex-end; + justify-content: flex-start; + gap: 0 1.75rem; + max-width: 100%; + } + .dropdownFilterSelect { + align-self: stretch; + height: 1.181rem; + position: relative; + max-width: 100%; + overflow: hidden; + flex-shrink: 0; + z-index: 1; + } + .recruitmentFrame { + width: 3.438rem; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + padding: 0 0 0.906rem; + box-sizing: border-box; + } + .department, + .membersEventsTasksFrame { + display: flex; + flex-direction: row; + box-sizing: border-box; + max-width: 100%; + } + .department { + flex: 1; + border-radius: var(--br-3xs); + background-color: var(--brand-cream); + border: 1px solid var(--color-black); + align-items: flex-end; + justify-content: space-between; + padding: var(--padding-8xl) 1.813rem 1.662rem var(--padding-8xl); + gap: var(--gap-xl); + } + .membersEventsTasksFrame { + align-self: stretch; + align-items: flex-start; + justify-content: flex-start; + padding: 0 0 0 var(--padding-9xs); + } + .roleChild { + height: 5.5rem; + width: 47.375rem; + position: relative; + border-radius: var(--br-3xs); + background-color: var(--color-gainsboro); + border: 1px solid var(--color-black); + box-sizing: border-box; + display: none; + max-width: 100%; + } + .iconUser { + width: 2.625rem; + height: 3rem; + position: relative; + z-index: 1; + } + .departmentLabel { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + padding: 0.188rem 0 0; + } + .role1 { + height: 3rem; + width: 10.75rem; + position: relative; + font-size: var(--font-size-15xl); + font-weight: 500; + font-family: var(--font-dm-sans); + color: var(--color-black); + text-align: left; + display: flex; + align-items: center; + flex-shrink: 0; + z-index: 1; + } + .backButtonFrame, + .role { + border-radius: var(--br-3xs); + background-color: var(--color-gainsboro); + border: 1px solid var(--color-black); + box-sizing: border-box; + } + .role { + align-self: stretch; + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: flex-start; + padding: var(--padding-xl) 2.25rem var(--padding-mid); + gap: 1.938rem; + max-width: 100%; + } + .backButtonFrame { + height: 100%; + width: 100%; + position: absolute; + margin: 0 !important; + top: 0; + right: 0; + bottom: 0; + left: 0; + } + .iconMail { + width: 2.813rem; + height: 1.969rem; + position: relative; + z-index: 1; + } + .iconMailWrapper { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + padding: 0 0 0.469rem; + } + .email, + .email1 { + display: flex; + position: relative; + } + .email1 { + margin: 0; + height: 3rem; + width: 80%; + font-size: var(--font-size-15xl); + font-weight: 500; + font-family: var(--font-dm-sans); + color: var(--color-black); + text-align: left; + align-items: center; + flex-shrink: 0; + z-index: 1; + overflow-x: auto; + overflow-y: hidden; + } + .email { + flex: 0.8157; + flex-direction: row; + align-items: flex-end; + justify-content: flex-start; + padding: var(--padding-xl) 2.125rem; + box-sizing: border-box; + gap: 0 1.875rem; + min-width: 15rem; + max-width: 100%; + } + .componentInstance { + height: 100%; + width: 100%; + position: absolute; + margin: 0 !important; + top: 0; + right: 0; + bottom: 0; + left: 0; + border-radius: var(--br-3xs); + background-color: var(--color-gainsboro); + border: 1px solid var(--color-black); + box-sizing: border-box; + } + .iconPhone { + height: 2.481rem; + width: 2.5rem; + position: relative; + z-index: 1; + } + .contact, + .telegram { + display: flex; + position: relative; + } + .telegram { + height: 3rem; + flex: 1; + font-size: var(--font-size-15xl); + font-weight: 500; + font-family: var(--font-dm-sans); + color: var(--color-black); + text-align: left; + align-items: center; + min-width: 7.188rem; + z-index: 1; + } + .contact { + cursor: pointer; + border: 0; + padding: 1.125rem 3.25rem var(--padding-3xl) 1.875rem; + background-color: transparent; + align-self: stretch; + flex-direction: row; + align-items: flex-end; + justify-content: flex-start; + gap: 0 2.5rem; + } + .componentInstance1 { + height: 2.625rem; + flex: 1; + position: relative; + border-radius: var(--br-3xs); + background-color: var(--brand-blue); + box-shadow: 0 4px 4px rgba(0, 0, 0, 0.08); + } + .button { + width: 4.188rem; + position: relative; + font-size: var(--font-size-6xl); + font-weight: 500; + font-family: var(--font-dm-sans); + color: var(--color-white); + text-align: center; + display: none; + } + .component3 { + position: absolute; + height: 100%; + width: 100%; + top: 0; + right: -0.07%; + bottom: 0; + left: 0.07%; + box-shadow: 0 4px 4px rgba(0, 0, 0, 0.08); + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: flex-start; + } + .backButton1 { + position: absolute; + height: 67.38%; + width: 23.39%; + top: 17.38%; + right: 35.55%; + bottom: 15.24%; + left: 41.06%; + max-width: 100%; + overflow: hidden; + max-height: 100%; + object-fit: contain; + z-index: 1; + } + .backButton { + height: 2.625rem; + flex: 1; + position: relative; + cursor: pointer; + } + .backButtonWrapper, + .telegramIconFrame { + display: flex; + justify-content: flex-start; + } + .backButtonWrapper { + width: 9.231rem; + flex-direction: row; + align-items: flex-start; + padding: 0 var(--padding-8xs); + box-sizing: border-box; + } + .telegramIconFrame { + align-self: stretch; + flex-direction: column; + align-items: flex-end; + gap: 2.625rem 0; + } + .contactFrameContent, + .emailInputFrame { + display: flex; + align-items: flex-start; + justify-content: flex-start; + max-width: 100%; + } + .contactFrameContent { + flex: 1; + flex-direction: column; + padding: 0.125rem 0 0; + box-sizing: border-box; + min-width: 15rem; + } + .emailInputFrame { + align-self: stretch; + flex-direction: row; + gap: 0 1.25rem; + } + .departmentIconFrame { + margin: 0; + flex: 1; + flex-direction: column; + align-items: flex-start; + gap: 1.281rem 0; + min-width: 30.938rem; + } + .departmentIconFrame, + .emailInput, + .profileDetailsFrame { + display: flex; + justify-content: flex-start; + max-width: 100%; + } + .emailInput { + align-self: stretch; + flex-direction: row; + align-items: flex-start; + gap: 0 3rem; + text-align: center; + font-size: 2.625rem; + color: var(--brand-black); + } + .profileDetailsFrame { + width: 68.75rem; + flex-direction: column; + align-items: flex-end; + gap: 1rem 0; + } + .headerFrame, + .profilePage { + display: flex; + align-items: flex-start; + box-sizing: border-box; + } + .headerFrame { + width: 78.875rem; + flex-direction: row; + justify-content: center; + padding: 0 var(--padding-xl); + max-width: 100%; + text-align: left; + font-size: 3.125rem; + color: var(--brand-blue); + font-family: var(--font-dm-sans); + } + .profilePage { + width: 100%; + position: relative; + background-color: var(--color-white); + overflow: hidden; + flex-direction: column; + justify-content: flex-start; + padding: 0 0 11.875rem; + gap: 2.313rem 0; + letter-spacing: normal; + } + @media screen and (max-width: 1200px) { + .tasksFrame { + display: none; + } + } + @media screen and (max-width: 1050px) { + .profile { + font-size: 2.5rem; + } + .admin { + font-size: var(--font-size-8xl); + } + .name { + font-size: var(--font-size-15xl); + } + .contactFrame { + flex: 1; + } + .department1, + .email1, + .role1, + .telegram { + font-size: var(--font-size-8xl); + } + .emailInput { + flex-wrap: wrap; + } + } + @media screen and (max-width: 750px) { + .desktopNavBarUpdatedAtt { + padding-right: var(--padding-3xl); + box-sizing: border-box; + } + .adminText, + .department, + .emailInputFrame, + .eventsFrame1 { + flex-wrap: wrap; + } + .departmentIconFrame { + min-width: 100%; + } + .emailInput { + gap: 0 1.5rem; + } + .profilePage { + gap: 1.125rem 0; + } + } + @media screen and (max-width: 450px) { + .profile { + font-size: 1.875rem; + } + .admin { + font-size: var(--font-size-xl); + } + .name { + font-size: var(--font-size-6xl); + } + .department1, + .role1 { + font-size: var(--font-size-xl); + } + .role { + flex-wrap: wrap; + gap: 0.938rem; + } + .email1 { + font-size: var(--font-size-xl); + } + .email { + flex-wrap: wrap; + gap: 0 0.938rem; + flex: 1; + } + .telegram { + font-size: var(--font-size-xl); + } + .contact { + flex-wrap: wrap; + gap: 0 1.25rem; + padding-right: var(--padding-xl); + box-sizing: border-box; + } + .button { + font-size: var(--font-size-xl); + } + .telegramIconFrame { + gap: 1.313rem 0; + } + } + \ No newline at end of file diff --git a/src/assets/css/index.css b/src/assets/css/index.css new file mode 100644 index 0000000..d0366fa --- /dev/null +++ b/src/assets/css/index.css @@ -0,0 +1,119 @@ +@import url("https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500&display=swap"); + +:root { + line-height: 1.5; + font-weight: 400; + + --font-dm-sans: "DM Sans"; + + /* font sizes */ + --font-size-6xl: 1.563rem; + --font-size-xl: 1.25rem; + --font-size-15xl: 2.125rem; + --font-size-8xl: 1.688rem; + + /* Colors */ + --color-white: #fff; + --brand-blue: #0c1747; + --color-black: #000; + --color-gainsboro: #d9d9d9; + --brand-cream: #fbf6ef; + --brand-dark-grey: #5a5a5a; + --brand-black: #151515; + --brand-yellow: #f9a72b; + + /* Gaps */ + --gap-0: 0rem; + --gap-xl: 1.25rem; + + /* Paddings */ + --padding-xl: 1.25rem; + --padding-8xs: 0.313rem; + --padding-3xl: 1.375rem; + --padding-mid: 1.063rem; + --padding-9xs: 0.25rem; + --padding-8xl: 1.688rem; + --padding-2xl: 1.313rem; + --padding-base: 1rem; + + /* Border radiuses */ + --br-3xs: 10px; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + line-height: normal; + place-items: center; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} + +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +/* Track */ +::-webkit-scrollbar-track { + background: var(--lightestgrey); +} + +/* Handle */ +::-webkit-scrollbar-thumb { + background: #888; + border-radius: 5px; +} + +/* Handle on hover */ +::-webkit-scrollbar-thumb:hover { + background: #555; +} diff --git a/src/components/Input.tsx b/src/components/Input.tsx new file mode 100644 index 0000000..59f4d15 --- /dev/null +++ b/src/components/Input.tsx @@ -0,0 +1,13 @@ +const Input = (field: any) => { + return ( + field.handleChange(e.target.value)} + /> + ); +}; + +export default Input; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx new file mode 100644 index 0000000..6d334f8 --- /dev/null +++ b/src/components/Navbar.tsx @@ -0,0 +1,44 @@ +import styles from "css/Navbar.module.css"; + +const Navbar = () => { + return ( + + + + Announcements + + + + + Members + + + Events + + + Tasks + + + Attendance + + + Recruitment + + + + + + ); +}; + +export default Navbar; diff --git a/src/main.tsx b/src/main.tsx index c378af5..ed40257 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,31 +1,37 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import React from "react"; +import ReactDOM from "react-dom/client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { RouterProvider } from "react-router-dom"; +import Navbar from "components/Navbar.tsx"; import router from "routes/router.tsx"; -import 'css/reset.css' +import "css/index.css"; +import "css/reset.css"; -const queryClient = new QueryClient() +const queryClient = new QueryClient(); async function enableMocking() { - if (!import.meta.env.DEV || import.meta.env.VITE_IGNORE_MSW.toLowerCase() === "true") { - return + if ( + !import.meta.env.DEV || + import.meta.env.VITE_IGNORE_MSW.toLowerCase() === "true" + ) { + return; } - const { worker } = await import('./mocks/worker') + const { worker } = await import("./mocks/worker"); - return worker.start({ onUnhandledRequest: "bypass" }) + return worker.start({ onUnhandledRequest: "bypass" }); } enableMocking().then(() => { - ReactDOM.createRoot(document.getElementById('root')!).render( + ReactDOM.createRoot(document.getElementById("root")!).render( - - + + + - , - ) -}) \ No newline at end of file + + ); +}); diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index 342fd44..53286e3 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -1,21 +1,61 @@ //src/mocks/handlers.ts -import { http, HttpResponse } from "msw" +import { HttpResponseResolver, delay, http, HttpResponse } from "msw"; import resolveURL from "../api/fetch.ts"; +import {auth_handlers} from "@/mocks/authentication/auth_handlers.ts"; -import { auth_handlers } from "./authentication/auth_handlers.ts"; +const API_CALL_DELAY = 0; +function withDelay( + durationMs: number, + resolver: HttpResponseResolver +): HttpResponseResolver { + return async (...args) => { + await delay(durationMs); + return resolver(...args); + }; +} -const default_handlers = [ - http.get(resolveURL('/resource'), () => { - return HttpResponse.json({ - result: "Hello World!" - }) +export const handlers = [ + http.get(resolveURL("/resource"), () => { + return HttpResponse.json({ + result: "Hello World!", + }); + }), + + http.get( + resolveURL("/profile/:id"), + withDelay(API_CALL_DELAY, ({ params }) => { + const { id } = params; + return HttpResponse.json({ + id, + name: "Rick Astley", + department: "Software", + role: "Singer", + email: "nevergonnagiveyouup@gmail.com", + telegram: "@rickroll", + }); }) -] + ), + http.patch( + resolveURL("/profile/:id"), + withDelay(API_CALL_DELAY, ({ params }) => { + const { id } = params; + const { name, department, role, email, telegram } = params as Record< + string, + string + >; + return HttpResponse.json({ + id, + name, + department, + role, + email, + telegram, + }); + }) + ), -export const handlers = [ - ...default_handlers, ...auth_handlers -] +]; diff --git a/src/routes/profiles/ProfilePage.tsx b/src/routes/profiles/ProfilePage.tsx new file mode 100644 index 0000000..17a61b6 --- /dev/null +++ b/src/routes/profiles/ProfilePage.tsx @@ -0,0 +1,273 @@ +import { useQuery, useMutation } from "@tanstack/react-query"; +import { useForm } from "@tanstack/react-form"; +import { zodValidator } from "@tanstack/zod-form-adapter"; +import { z } from "zod"; +import resolveURL from "../../api/fetch"; +import styles from "css/ProfilePage.module.css"; +import { useParams } from "react-router-dom"; +import { FocusEventHandler } from "react"; + +// TODO: Integrate with React Router +const ProfilePage = () => { + const { id } = useParams(); + + const profileQuery = useQuery({ + queryKey: ["profile", id], + queryFn: () => + fetch(resolveURL(`/profile/${id}`)).then((res) => res.json()), + }); + + const data = profileQuery.data; + + const profileMutation = useMutation({ + mutationFn: (data: object) => { + return fetch(resolveURL(`/profile/${id}`), { + method: "PATCH", + body: JSON.stringify(data), + }); + }, + }); + + const form = useForm({ + defaultValues: { + department: data?.department, + role: data?.role, + email: data?.email, + telegram: data?.telegram, + }, + validatorAdapter: zodValidator, + onSubmit: async ({ value }) => { + profileMutation.mutate(value); + // TODO: Add a global success toast component, render it here + }, + }); + + const handleSubmit: FocusEventHandler = (e) => { + e.preventDefault(); + e.stopPropagation(); + void form.handleSubmit(); + }; + // id, + // name: "Rick Astley", + // department: "Software", + // role: "Singer", + // email: "nevergonnagiveyouup@gmail.com", + // telegram: "@rickroll", + + const onBackButtonClick = () => {}; + + if (profileQuery.error) { + return Error: {profileQuery.error.message}; // TODO: Add a global error component + } + if (profileMutation.isError) { + return Error: {profileMutation.error.message}; + } + + if (profileQuery.isPending) { + return Loading...; // TODO: Add a global spinner component + } + + if (profileMutation.isPending) { + return Saving...; + } + + return ( + + + + + + {`Profile`} + + + + Admin + + + + + + + + + + + {data.name} + + { + e.preventDefault(); + e.stopPropagation(); + void form.handleSubmit(); + }} + > + + + + + + + + {(field) => + + field.handleChange(e.target.value) + } + /> + } + + + + + + + + + + + + + + + + + {(field) => + field.handleChange(e.target.value)} + />} + + + + + + + + + + + + {(field) => + field.handleChange(e.target.value)} + /> + } + + + + + + + + + + + {(field) => + + field.handleChange(e.target.value) + } + /> + } + + + + + + + + Close + + + + + + + + + + + + + ); +}; + +export default ProfilePage; diff --git a/src/routes/router.tsx b/src/routes/router.tsx index 0b6325e..c7b0be7 100644 --- a/src/routes/router.tsx +++ b/src/routes/router.tsx @@ -1,13 +1,13 @@ import { createBrowserRouter } from "react-router-dom"; import TestPage from "./shared/TestPage.tsx"; -import App from "./App.tsx" +import App from "./App.tsx"; +import ProfilePage from "./profiles/ProfilePage.tsx"; import EventPage from "./events/EventPage.tsx"; - const router = createBrowserRouter([ { path: "/", - Component: App + Component: App, }, { path: "/test", @@ -16,7 +16,11 @@ const router = createBrowserRouter([ { path: "/events", Component: EventPage, - } -]) + }, + { + path: "/profiles/:id", + Component: ProfilePage, + }, +]); -export default router \ No newline at end of file +export default router; diff --git a/src/routes/shared/TestPage.tsx b/src/routes/shared/TestPage.tsx index 37fdf92..b153aa4 100644 --- a/src/routes/shared/TestPage.tsx +++ b/src/routes/shared/TestPage.tsx @@ -5,17 +5,15 @@ import viteLogo from "/vite.svg"; import reactLogo from "/react.svg"; import resolveURL from "@/api/fetch.ts"; - const getResourceOptions = { - queryKey: ['resourceData'], - queryFn: () => fetch(resolveURL('/resource')).then((res) => res.json()) -} - + queryKey: ["resourceData"], + queryFn: () => fetch(resolveURL("/resource")).then((res) => res.json()), +}; function TestPage() { - const [count, setCount] = useState(0) + const [count, setCount] = useState(0); - const { isPending, data } = useQuery(getResourceOptions) + const { isPending, data } = useQuery(getResourceOptions); return ( <> @@ -37,7 +35,7 @@ function TestPage() { Click on the Vite and React logos to learn more > - ) + ); } -export default TestPage \ No newline at end of file +export default TestPage; diff --git a/yarn.lock b/yarn.lock index 4c5e991..1bc0634 100644 --- a/yarn.lock +++ b/yarn.lock @@ -575,6 +575,13 @@ resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== +"@tanstack/form-core@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@tanstack/form-core/-/form-core-0.14.0.tgz#702c54f80c5a56cb444b7e1c94acb98c54e2f30a" + integrity sha512-VlDKrWCr+47FopXcDsthj/hYFqIzyueAHEZvVOE8CYNWPMvrtK6+0H+IL39TFEv0n8J8Hf8jojPP/q1y7Ft9Uw== + dependencies: + "@tanstack/store" "^0.3.1" + "@tanstack/query-core@5.18.0": version "5.18.0" resolved "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.18.0.tgz" @@ -585,6 +592,16 @@ resolved "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.18.0.tgz" integrity sha512-h0KuF8CboNB7z6+Y6psTwK56JwHD1pNB28gDbc1PaaQ9Q5uD837egYH3qOEEireO/1P3j2s3jVPsbB0+bqdthw== +"@tanstack/react-form@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-form/-/react-form-0.14.0.tgz#cfe44c1389bb3bd02a93869fa8bf3463d218bcb7" + integrity sha512-Y5DoYYs3iwUWGxC+HUVx5C0ZF29vPY23a6BBEVBnI6fxFw3sF1tuBXK+FKPA6ZT2XAcBeHRxI7jd1pArWoy1vA== + dependencies: + "@tanstack/form-core" "0.14.0" + "@tanstack/react-store" "^0.3.1" + decode-formdata "^0.4.0" + rehackt "^0.0.3" + "@tanstack/react-query-devtools@^5.18.0": version "5.18.0" resolved "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.18.0.tgz" @@ -599,6 +616,26 @@ dependencies: "@tanstack/query-core" "5.18.0" +"@tanstack/react-store@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@tanstack/react-store/-/react-store-0.3.1.tgz#f93896da40f91b4385aca9448d3ccf914b314b50" + integrity sha512-PfV271d345It6FdcX4c9gd+llKGddtvau8iJnybTAWmYVyDeFWfIIkiAJ5iNITJmI02AzqgtcV3QLNBBlpBUjA== + dependencies: + "@tanstack/store" "0.3.1" + use-sync-external-store "^1.2.0" + +"@tanstack/store@0.3.1", "@tanstack/store@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@tanstack/store/-/store-0.3.1.tgz#317bfb7eedd8cc4ee32c4e52740abbf1bd311d36" + integrity sha512-A49KN8SpLMWaNmZGPa9K982RQ81W+m7W6iStcQVeKeVS70JZRqkF0fDwKByREPq6qz9/kS0aQFOPQ0W6wIeU5g== + +"@tanstack/zod-form-adapter@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@tanstack/zod-form-adapter/-/zod-form-adapter-0.14.0.tgz#64d0663278cd6ba0b7a861875fcb54275d39d704" + integrity sha512-PJ3GRckc/fjLk8Ez/4nslv5XJSF8gvr5wmCx2pFjWQ1Iqok5d4qNzkRGLytCGSXbKFwEz3GW1nl+GfzgwpF21Q== + dependencies: + "@tanstack/form-core" "0.14.0" + "@testing-library/dom@^9.0.0": version "9.3.4" resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz" @@ -1347,6 +1384,11 @@ decimal.js@^10.4.3: resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz" integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== +decode-formdata@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/decode-formdata/-/decode-formdata-0.4.0.tgz#485715a37417868435a92d2144b39646fe654aae" + integrity sha512-/OMUlsRLrSgHPOWCwembsFFTT4DY7Ts9GGlwK8v9yeLOyYZSPKIfn/1oOuV9UmpQ9CZi5JeyT8edunRoBOOl5g== + deep-eql@^4.1.3: version "4.1.3" resolved "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz" @@ -3155,6 +3197,11 @@ regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: define-properties "^1.2.0" set-function-name "^2.0.0" +rehackt@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/rehackt/-/rehackt-0.0.3.tgz#1ea454620d4641db8342e2db44595cf0e7ac6aa0" + integrity sha512-aBRHudKhOWwsTvCbSoinzq+Lej/7R8e8UoPvLZo5HirZIIBLGAgdG7SL9QpdcBoQ7+3QYPi3lRLknAzXBlhZ7g== + remove-accents@0.5.0: version "0.5.0" resolved "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz" @@ -3725,6 +3772,11 @@ url-parse@^1.5.3: querystringify "^2.1.1" requires-port "^1.0.0" +use-sync-external-store@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" @@ -3972,3 +4024,8 @@ yocto-queue@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz" integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== + +zod@^3.22.4: + version "3.22.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" + integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==