diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index 8459229..e6065ef 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -30,6 +30,7 @@ interface FrontendRegisterForm { groups?: string[]; merchants?: string[]; creator?: string; + countryCode: string; } export async function POST(request: Request) { diff --git a/app/components/Modal/Modal.scss b/app/components/Modal/Modal.scss index 3b9200e..d145691 100644 --- a/app/components/Modal/Modal.scss +++ b/app/components/Modal/Modal.scss @@ -17,7 +17,7 @@ box-shadow: 0 2px 16px rgba(0, 0, 0, 0.2); position: relative; min-width: 320px; - max-width: 60vw; + max-width: 65vw; max-height: 90vh; overflow: auto; padding: 2rem 1.5rem 1.5rem 1.5rem; diff --git a/app/components/Spinner/Spinner.scss b/app/components/Spinner/Spinner.scss new file mode 100644 index 0000000..196e4dd --- /dev/null +++ b/app/components/Spinner/Spinner.scss @@ -0,0 +1,48 @@ +.spinner { + display: inline-block; + position: relative; + border-radius: 50%; + border: 2px solid transparent; + border-top: 2px solid #1976d2; + animation: spin 1s linear infinite; + + &--small { + width: 16px; + height: 16px; + border-width: 1px; + } + + &--medium { + width: 20px; + height: 20px; + border-width: 2px; + } + + &--large { + width: 32px; + height: 32px; + border-width: 3px; + } + + &__inner { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 60%; + height: 60%; + border-radius: 50%; + border: 1px solid transparent; + border-top: 1px solid currentColor; + animation: spin 0.8s linear infinite reverse; + } +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/app/components/Spinner/Spinner.tsx b/app/components/Spinner/Spinner.tsx new file mode 100644 index 0000000..7725f11 --- /dev/null +++ b/app/components/Spinner/Spinner.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import "./Spinner.scss"; + +interface SpinnerProps { + size?: "small" | "medium" | "large"; + color?: string; +} + +const Spinner: React.FC = ({ + size = "medium", + color = "#1976d2", +}) => { + return ( +
+
+
+ ); +}; + +export default Spinner; diff --git a/app/features/UserRoles/AddUser/AddUser.scss b/app/features/UserRoles/AddUser/AddUser.scss index 0479219..d9b507a 100644 --- a/app/features/UserRoles/AddUser/AddUser.scss +++ b/app/features/UserRoles/AddUser/AddUser.scss @@ -67,6 +67,15 @@ border-radius: 4px; width: 100px; cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + + &:disabled { + opacity: 0.7; + cursor: not-allowed; + } } button:first-child { diff --git a/app/features/UserRoles/AddUser/AddUser.tsx b/app/features/UserRoles/AddUser/AddUser.tsx index 84e6b30..8a643e3 100644 --- a/app/features/UserRoles/AddUser/AddUser.tsx +++ b/app/features/UserRoles/AddUser/AddUser.tsx @@ -2,11 +2,15 @@ import React, { useState } from "react"; import { useRouter } from "next/navigation"; +import { useDispatch, useSelector } from "react-redux"; +import { AppDispatch } from "@/app/redux/store"; import "./AddUser.scss"; -import { addUser } from "@/services/roles.services"; +import { addUser } from "@/app/redux/auth/authSlice"; import { IEditUserForm } from "../User.interfaces"; import { COUNTRY_CODES } from "../constants"; import { formatPhoneDisplay, validatePhone } from "../utils"; +import Spinner from "../../../components/Spinner/Spinner"; +import { RootState } from "@/app/redux/store"; interface AddUserFormProps { onSuccess?: () => void; @@ -14,6 +18,11 @@ interface AddUserFormProps { const AddUserForm: React.FC = ({ onSuccess }) => { const router = useRouter(); + const dispatch = useDispatch(); + const { status, error: authError } = useSelector( + (state: RootState) => state.auth + ); + const [form, setForm] = useState({ username: "", firstName: "", @@ -26,11 +35,11 @@ const AddUserForm: React.FC = ({ onSuccess }) => { jobTitle: "", }); - const [error, setError] = useState(""); - const [loading, setLoading] = useState(false); const [phoneError, setPhoneError] = useState(""); const [countryCode, setCountryCode] = useState("+1"); + const loading = status === "loading"; + const handleChange = ( e: React.ChangeEvent ) => { @@ -78,7 +87,6 @@ const AddUserForm: React.FC = ({ onSuccess }) => { // Validate phone number if provided if (form.phone && phoneError) { - setError("Please fix phone number errors before submitting."); return; } @@ -90,28 +98,22 @@ const AddUserForm: React.FC = ({ onSuccess }) => { form.groups.length === 0 || !form.jobTitle ) { - setError("Please fill in all required fields."); return; } + // Format phone number with country code before submission + const formattedForm = { + ...form, + phone: form.phone ? formatPhoneDisplay(form.phone, countryCode) : "", + }; + try { - setLoading(true); - setError(""); - - // Format phone number with country code before submission - const formattedForm = { - ...form, - phone: form.phone ? formatPhoneDisplay(form.phone, countryCode) : "", - }; - - await addUser(formattedForm); + await dispatch(addUser(formattedForm)); if (onSuccess) onSuccess(); router.refresh(); // <- refreshes the page (SSR re-runs) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (err: any) { - setError(err.message || "Something went wrong."); - } finally { - setLoading(false); + } catch (err) { + // Error is handled by Redux state + console.error("Failed to add user:", err); } }; @@ -255,7 +257,9 @@ const AddUserForm: React.FC = ({ onSuccess }) => { /> - {error && {error}} + {authError && ( + {authError} + )} = ({ onSuccess }) => {
diff --git a/app/redux/auth/authSlice.tsx b/app/redux/auth/authSlice.tsx index cc60210..5fe1e19 100644 --- a/app/redux/auth/authSlice.tsx +++ b/app/redux/auth/authSlice.tsx @@ -1,5 +1,7 @@ import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; import { ThunkSuccess, ThunkError, IUserResponse } from "../types"; +import { IEditUserForm } from "../../features/UserRoles/User.interfaces"; +import { RootState } from "../store"; import toast from "react-hot-toast"; interface TokenInfo { @@ -175,6 +177,39 @@ export const validateAuth = createAsyncThunk< // TODO - Creaye a new thunk to update the user stuff +// ---------------- Add User ---------------- +export const addUser = createAsyncThunk< + ThunkSuccess<{ user: IUserResponse }>, + IEditUserForm, + { rejectValue: ThunkError; state: RootState } +>("auth/addUser", async (userData, { rejectWithValue, getState }) => { + try { + const state = getState(); + const currentUserId = state.auth.user?.id; + console.log("[DEBUG] [ADD-USER] [currentUserId]: ", currentUserId); + const res = await fetch("/api/auth/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ...userData, creator: currentUserId || "" }), + }); + + const data = await res.json(); + + if (!res.ok) { + return rejectWithValue(data.message || "Failed to create user"); + } + + return { + message: data.message || "User created successfully", + user: data.user, + }; + } catch (err: unknown) { + return rejectWithValue( + (err as Error).message || "Network error during user creation" + ); + } +}); + // ---------------- Update User Details ---------------- export const updateUserDetails = createAsyncThunk< ThunkSuccess<{ user: IUserResponse }>, @@ -297,6 +332,20 @@ const authSlice = createSlice({ state.tokenInfo = null; state.error = action.payload as string; }) + // Add User + .addCase(addUser.pending, state => { + state.status = "loading"; + state.authMessage = "Creating user..."; + }) + .addCase(addUser.fulfilled, (state, action) => { + state.status = "succeeded"; + state.authMessage = action.payload.message; + }) + .addCase(addUser.rejected, (state, action) => { + state.status = "failed"; + state.error = action.payload as string; + state.authMessage = action.payload as string; + }) // Update User Details .addCase(updateUserDetails.pending, state => { state.status = "loading"; diff --git a/services/roles.services.ts b/services/roles.services.ts index f6cf334..093d831 100644 --- a/services/roles.services.ts +++ b/services/roles.services.ts @@ -1,18 +1,18 @@ import { IEditUserForm } from "@/app/features/UserRoles/User.interfaces"; -export async function addUser(data: IEditUserForm) { - const res = await fetch("/api/auth/register", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(data), - }); +// export async function addUser(data: IEditUserForm) { +// const res = await fetch("/api/auth/register", { +// method: "POST", +// headers: { "Content-Type": "application/json" }, +// body: JSON.stringify(data), +// }); - if (!res.ok) { - throw new Error("Failed to create role"); - } +// if (!res.ok) { +// throw new Error("Failed to create role"); +// } - return res.json(); // or return type depending on your backend -} +// return res.json(); // or return type depending on your backend +// } export async function editUser(id: string, data: IEditUserForm) { console.log("[editUser] - id", id, data); const res = await fetch(`/api/dashboard/admin/users/${id}`, {