diff --git a/hng_boilerplate_nextjs_project b/hng_boilerplate_nextjs_project deleted file mode 160000 index 8fc1b95c0..000000000 --- a/hng_boilerplate_nextjs_project +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8fc1b95c0e495b78b6b2d20e43b85695edcafec9 diff --git a/package.json b/package.json index 00d899270..78985a100 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "test:ci": "vitest --coverage", "email:dev": "email dev --dir \"./src/email/templates\"", "email:build": "email build --dir \"./src/email/templates\"", - "email:start": "email start" + "email:start": "email start", + "typecheck": "tsc --project tsconfig.json --noEmit" }, "dependencies": { "@hookform/resolvers": "^3.9.0", @@ -67,6 +68,7 @@ "react-email": "2.1.5", "react-hook-form": "^7.52.2", "react-paginate": "^8.2.0", + "react-toastify": "^10.0.5", "recharts": "^2.12.7", "sharp": "^0.33.4", "swiper": "^11.1.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d7b81ef63..a058996d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,6 +152,9 @@ importers: react-paginate: specifier: ^8.2.0 version: 8.2.0(react@18.3.1) + react-toastify: + specifier: ^10.0.5 + version: 10.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) recharts: specifier: ^2.12.7 version: 2.12.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -4484,6 +4487,12 @@ packages: '@types/react': optional: true + react-toastify@10.0.5: + resolution: {integrity: sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==} + peerDependencies: + react: '>=18' + react-dom: '>=18' + react-transition-group@4.4.5: resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: @@ -10020,6 +10029,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + react-toastify@10.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + clsx: 2.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.25.0 diff --git a/public/images/productimage.png b/public/images/productimage.png new file mode 100644 index 000000000..55cef508e Binary files /dev/null and b/public/images/productimage.png differ diff --git a/public/images/user.png b/public/images/user.png new file mode 100644 index 000000000..6f728aaa5 Binary files /dev/null and b/public/images/user.png differ diff --git a/public/messages/en.json b/public/messages/en.json index 2e703f3c0..eef7daedd 100644 --- a/public/messages/en.json +++ b/public/messages/en.json @@ -46,7 +46,7 @@ "otpExpiresIn": "OTP expires in: {timeLeft}", "resendCode": "Didn't receive the code? resend", "dataProcessing": "We would process your data as set forth in our Terms of Use, Privacy Policy and Data Processing Agreement", - "alreadyHaveAccount": "Already Have An Account? Login" + "alreadyHaveAccount": "Already Have An Account?" }, "HomePage": { "title": "Hello world!" @@ -153,6 +153,7 @@ }, "noJobs": { "noJobsTitle": "No available Jobs at the moment", + "noJobsContent": "Sign up for our newsletter to get updates on job postings", "modalTitle": "Newsletter", "button": "Sign up for newsletter", diff --git a/src/actions/notifications/getAllNotifications.ts b/src/actions/notifications/getAllNotifications.ts index c7389fb9f..456b27580 100644 --- a/src/actions/notifications/getAllNotifications.ts +++ b/src/actions/notifications/getAllNotifications.ts @@ -3,10 +3,10 @@ import axios from "axios"; import { auth } from "~/lib/auth"; -import { getApiUrl } from "../getApiUrl"; + +const apiUrl = process.env.API_URL; export const getAllNotifications = async () => { - const apiUrl = await getApiUrl(); const session = await auth(); try { const response = await axios.get(`${apiUrl}/api/v1/notifications`, { diff --git a/src/actions/organization.ts b/src/actions/organization.ts index 36f321737..ac83db272 100644 --- a/src/actions/organization.ts +++ b/src/actions/organization.ts @@ -102,8 +102,13 @@ export const getAnalytics = async () => { }, }); + const formattedData = Object.keys(response.data.data).map((key) => ({ + month: key, + revenue: response.data.data[key], + })); + return { - data: response.data.data, + data: formattedData, }; } catch (error) { return axios.isAxiosError(error) && error.response diff --git a/src/actions/product.ts b/src/actions/product.ts index b527d36c9..e9fe607f6 100644 --- a/src/actions/product.ts +++ b/src/actions/product.ts @@ -70,7 +70,6 @@ export const getAllProduct = async (org_id: string) => { ); return { products: response.data.data, - response: response, }; } catch (error) { return axios.isAxiosError(error) && error.response diff --git a/src/actions/socialAuth.ts b/src/actions/socialAuth.ts index 0673e95b5..1324573ac 100644 --- a/src/actions/socialAuth.ts +++ b/src/actions/socialAuth.ts @@ -2,14 +2,14 @@ import axios from "axios"; -import { AuthResponse, ErrorResponse, Profile } from "~/types"; +import { AuthResponse, ErrorResponse } from "~/types"; const apiUrl = process.env.API_URL; -const googleAuth = async (profile: Profile): Promise => { +const googleAuth = async (idToken: string): Promise => { try { const response = await axios.post(`${apiUrl}/api/v1/auth/google`, { - id_token: profile.id_token, + id_token: idToken, }); return { diff --git a/src/app/(auth-routes)/login/page.tsx b/src/app/(auth-routes)/login/page.tsx index 269782fdb..5251e9fee 100644 --- a/src/app/(auth-routes)/login/page.tsx +++ b/src/app/(auth-routes)/login/page.tsx @@ -10,7 +10,6 @@ import { useEffect, useState, useTransition } from "react"; import { useForm } from "react-hook-form"; import * as z from "zod"; -import { loginUser } from "~/actions/login"; import CustomButton from "~/components/common/common-button/common-button"; import { Input } from "~/components/common/input"; import LoadingSpinner from "~/components/miscellaneous/loading-spinner"; @@ -24,28 +23,22 @@ import { FormMessage, } from "~/components/ui/form"; import { useToast } from "~/components/ui/use-toast"; -import { useLocalStorage } from "~/hooks/use-local-storage"; import { cn } from "~/lib/utils"; import { LoginSchema } from "~/schemas"; -import { Organisation } from "~/types"; const Login = () => { const t = useTranslations("login"); const router = useRouter(); const { toast } = useToast(); - const { status } = useSession(); const [isLoading, startTransition] = useTransition(); const [showPassword, setShowPassword] = useState(false); - const [, setUserOrg] = useLocalStorage("user_org", []); - - const [currentOrgId, setCurrentOrgId] = useLocalStorage( - "current_orgid", - "", - ); + const { status } = useSession(); - if (status === "authenticated") { - router.push("/dashboard"); - } + useEffect(() => { + if (status === "authenticated") { + router.push("/dashboard"); + } + }, [status, router]); const form = useForm>({ resolver: zodResolver(LoginSchema), @@ -55,34 +48,37 @@ const Login = () => { rememberMe: false, }, }); - const onSubmit = async (values: z.infer) => { - startTransition(async () => { - await loginUser(values).then(async (data) => { - const { email, password } = values; + const { email, password } = values; - if (data.status === 200) { - setUserOrg(data.organisations); - if (!currentOrgId && data.organisations.length > 0) { - setCurrentOrgId(data.organisations[0].organisation_id); - } - await signIn( - "credentials", - { - email, - password, - redirect: false, - }, - { callbackUrl: "/dashboard" }, - ); + try { + startTransition(async () => { + const result = await signIn("credentials", { + email, + password, + redirect: false, + }); + + if (result?.ok) { router.push("/dashboard"); + toast({ + title: "Login success", + description: "Redirecting", + }); + } else { + toast({ + title: "An error occurred", + description: result?.error || "Unknown error", + }); } - toast({ - title: data.status === 200 ? "Login success" : "An error occurred", - description: data.status === 200 ? "Redirecting" : data.error, - }); }); - }); + } catch (error) { + toast({ + title: "Login failed", + description: + (error as Error).message || "An error occurred during login", + }); + } }; const togglePasswordVisibility = () => { @@ -91,6 +87,7 @@ const Login = () => { useEffect(() => { document.title = "Login"; }, []); + return (
@@ -106,7 +103,7 @@ const Login = () => { signIn("google", { callbackUrl: "/dashboard" })} + onClick={() => signIn("google", { callbackUrl: "/" })} icon={ { last_name: "User", email: "user@example.com", image: "path/to/image", - role: "user", }, access_token: "some-token", expires: "1", diff --git a/src/app/(landing-routes)/blog/page.tsx b/src/app/(landing-routes)/blog/page.tsx index 3ec42db7f..646d72f53 100644 --- a/src/app/(landing-routes)/blog/page.tsx +++ b/src/app/(landing-routes)/blog/page.tsx @@ -1,14 +1,62 @@ "use client"; +import axios from "axios"; +import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { getApiUrl } from "~/actions/getApiUrl"; import CustomButton from "~/components/common/common-button/common-button"; import HeroSection from "~/components/extDynamicPages/blogCollection/BlogPageHero"; import BlogCard from "~/components/layouts/BlogCards"; +import { useToast } from "~/components/ui/use-toast"; import { blogPosts } from "./data/mock"; const BlogHome = () => { const router = useRouter(); + const { toast } = useToast(); + const { data: session } = useSession(); + const [, setBlogPost] = useState(""); + + useEffect(() => { + async function fetchBlog() { + try { + const apiUrl = await getApiUrl(); + const token = session?.access_token; + const response = await axios.get(`${apiUrl}/api/v1/blogs`, { + headers: { + Authorization: `Bearer: ${token}`, + }, + }); + const data = response.data; + setBlogPost(data); + toast({ + title: + Array.isArray(data.data) && data.data.length === 0 + ? "No Blog Post Available" + : "Blog Posts Retrieved", + description: + Array.isArray(data.data) && data.data.length === 0 + ? "Blog posts are empty" + : "Blog posts retrieved successfully", + variant: + Array.isArray(data.data) && data.data.length === 0 + ? "destructive" + : "default", + }); + } catch (error) { + toast({ + title: "Error occured", + description: + error instanceof Error + ? error.message + : "An unknown error occurred", + variant: "destructive", + }); + } + } + fetchBlog(); + }, [session?.access_token, toast]); return (
diff --git a/src/app/(landing-routes)/contact-us/page.test.tsx b/src/app/(landing-routes)/contact-us/page.test.tsx index dea52ad56..5c4112890 100644 --- a/src/app/(landing-routes)/contact-us/page.test.tsx +++ b/src/app/(landing-routes)/contact-us/page.test.tsx @@ -1,11 +1,19 @@ +import { SessionProvider } from "next-auth/react"; +import { ReactNode } from "react"; + import { render } from "~/test/utils"; import Page from "./page"; describe("page tests", () => { + const renderWithSession = (component: ReactNode) => { + return render( + {component}, + ); + }; it("should render correctly", () => { expect.assertions(1); - render(); + renderWithSession(); expect(true).toBeTruthy(); }); diff --git a/src/app/(landing-routes)/help-center/page.tsx b/src/app/(landing-routes)/help-center/page.tsx index 26ff2313b..768041179 100644 --- a/src/app/(landing-routes)/help-center/page.tsx +++ b/src/app/(landing-routes)/help-center/page.tsx @@ -114,17 +114,6 @@ const HelpCenter = () => { > Frequently Asked Questions - -

- We couldn't answer your question? -

- - - Contact us -
diff --git a/src/app/dashboard/(admin)/admin/(settings)/settings/_components/layout/sidebar/index.tsx b/src/app/dashboard/(admin)/admin/(settings)/settings/_components/layout/sidebar/index.tsx index 48d8aabe8..f65b5e87f 100644 --- a/src/app/dashboard/(admin)/admin/(settings)/settings/_components/layout/sidebar/index.tsx +++ b/src/app/dashboard/(admin)/admin/(settings)/settings/_components/layout/sidebar/index.tsx @@ -12,12 +12,12 @@ import { UserRoundCog, UsersIcon, } from "lucide-react"; +import { useSession } from "next-auth/react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { FC, ForwardRefExoticComponent, RefAttributes } from "react"; import { useOrgContext } from "~/contexts/orgContext"; -import { useLocalStorage } from "~/hooks/use-local-storage"; const sideItems = [ { @@ -96,10 +96,10 @@ const SettingsSidebar: FC = ({ sideNavitems = sideItems }) => { pathname?.split("/").length == 2 ? "general" : pathname?.split("/")[3]; const organizationPath = pathname?.split("/")[4]; const { organizations } = useOrgContext(); - const [org_id] = useLocalStorage("current_orgid", ""); + const { data: session } = useSession(); const organization = organizations.find( - (org) => org.organisation_id === org_id, + (org) => org.organisation_id === session?.currentOrgId, ); return ( diff --git a/src/app/dashboard/(admin)/admin/(settings)/settings/data-and-privacy/page.tsx b/src/app/dashboard/(admin)/admin/(settings)/settings/data-and-privacy/page.tsx index dfd73041e..aa0e9bd79 100644 --- a/src/app/dashboard/(admin)/admin/(settings)/settings/data-and-privacy/page.tsx +++ b/src/app/dashboard/(admin)/admin/(settings)/settings/data-and-privacy/page.tsx @@ -2,6 +2,8 @@ import { useState } from "react"; +import CustomButton from "~/components/common/common-button/common-button"; + interface ToggleSwitchProperties { label: string; description: string; @@ -21,7 +23,7 @@ const ToggleSwitch: React.FC = ({ return (
- +
); }; diff --git a/src/app/dashboard/(admin)/admin/(settings)/settings/notification/page.tsx b/src/app/dashboard/(admin)/admin/(settings)/settings/notification/page.tsx index e1e1d263f..431eb04df 100644 --- a/src/app/dashboard/(admin)/admin/(settings)/settings/notification/page.tsx +++ b/src/app/dashboard/(admin)/admin/(settings)/settings/notification/page.tsx @@ -77,7 +77,7 @@ const NotificationPage = () => { }; return ( -
+
Notification @@ -178,7 +178,7 @@ const NotificationPage = () => { /> -
+
} diff --git a/src/app/dashboard/(admin)/admin/(settings)/settings/organization/members/page.tsx b/src/app/dashboard/(admin)/admin/(settings)/settings/organization/members/page.tsx index be3a54d02..85488d79b 100644 --- a/src/app/dashboard/(admin)/admin/(settings)/settings/organization/members/page.tsx +++ b/src/app/dashboard/(admin)/admin/(settings)/settings/organization/members/page.tsx @@ -2,12 +2,20 @@ import { AxiosResponse } from "axios"; import { EllipsisIcon } from "lucide-react"; +import Link from "next/link"; import { useState } from "react"; import CustomButton from "~/components/common/common-button/common-button"; import { Input } from "~/components/common/input"; import InviteMemberModal from "~/components/common/modals/invite-member"; +import DeleteMember from "~/components/common/modals/invite-member/DeleteMembers"; import LoadingSpinner from "~/components/miscellaneous/loading-spinner"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "~/components/ui/dropdown-menu"; import { Select, SelectContent, @@ -70,8 +78,15 @@ const activeMembers: number = memberData.length; const Members = () => { const [isModalOpen, setIsModalOpen] = useState(false); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [exporting, setExporting] = useState(false); const [text, setText] = useState("Export CSV"); + const [isVisible, setIsVisible] = useState(false); + + const toggleVisibility = () => { + setIsVisible(!isVisible); + }; const { toast } = useToast(); const handleCopy = () => { @@ -86,10 +101,18 @@ const Members = () => { setIsModalOpen(true); }; + const handleDeleteOpen = () => { + setIsDeleteModalOpen(true); + }; + const handleModalClose = () => { setIsModalOpen(false); }; + const handleDeleteClose = () => { + setIsDeleteModalOpen(false); + }; + const exportMembers = async () => { setExporting(true); setText("Exporting..."); @@ -159,32 +182,39 @@ const Members = () => { {}} + onChange={toggleVisibility} + checked={isVisible} />
-
- - https://www.figma.com/design/7hCSTNzQOJLl9aww6wEEd1/Managing-Users----Team-Learn-AI?node-i - - - Copy link - -
+ {isVisible && ( +
+ + https://www.figma.com/design/7hCSTNzQOJLl9aww6wEEd1/Managing-Users----Team-Learn-AI?node-i + + + Copy link + +
+ )}

Manage members

On the Free plan all members in a workspace are administrators. Upgrade to a paid plan to add the ability to assign or remove - administrator roles. Go to Plans + administrator roles.{" "} + + {" "} + Go to Plans +

@@ -241,7 +271,19 @@ const Members = () => {
- {}} className="flex-shrink-0" /> + + + {}} + className="flex-shrink-0" + /> + + + + Delete Member + + + ); })} @@ -271,6 +313,7 @@ const Members = () => {
+ ); }; diff --git a/src/app/dashboard/(admin)/admin/(settings)/settings/organization/roles-and-permissions/create-role/page.tsx b/src/app/dashboard/(admin)/admin/(settings)/settings/organization/roles-and-permissions/create-role/page.tsx index 0333ece7a..a55dfa546 100644 --- a/src/app/dashboard/(admin)/admin/(settings)/settings/organization/roles-and-permissions/create-role/page.tsx +++ b/src/app/dashboard/(admin)/admin/(settings)/settings/organization/roles-and-permissions/create-role/page.tsx @@ -2,6 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { ChevronLeftIcon } from "lucide-react"; +import { useSession } from "next-auth/react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; @@ -19,7 +20,6 @@ import { SelectValue, } from "~/components/ui/select"; import { toast } from "~/components/ui/use-toast"; -import { useLocalStorage } from "~/hooks/use-local-storage"; import { roleSchema } from "~/schemas"; type UseFormInputs = z.infer; @@ -123,7 +123,7 @@ const transformPermissions = (apiResponse: APIPermissions[]) => { function CreateNewRolePage() { const router = useRouter(); - const [currentOrgId] = useLocalStorage("current_orgid", ""); + const { data: session } = useSession(); const [isSaving, setIsSaving] = useState(false); const [permissions, setPermissions] = useState< PermissionOption["permissions"] | [] @@ -137,8 +137,9 @@ function CreateNewRolePage() { handleSubmit, formState: { errors }, setValue, + trigger, } = useForm({ - mode: "onBlur", + mode: "onChange", resolver: zodResolver(roleSchema), }); @@ -174,7 +175,7 @@ function CreateNewRolePage() { const onValid = async (values: UseFormInputs) => { setIsSaving(true); try { - await createRole(values, currentOrgId) + await createRole(values, session?.currentOrgId ?? "") .then((data) => { if (!data.error) { toast({ @@ -207,6 +208,10 @@ function CreateNewRolePage() { } }; + const handleInputChange = (field: keyof UseFormInputs) => { + trigger(field); + }; + return (
@@ -237,17 +242,28 @@ function CreateNewRolePage() { handleInputChange("name"), + })} + className={`!w-full rounded-md border ${ + errors.name ? "border-red-500" : "border-border" + } bg-transparent px-3 py-2 shadow-sm outline-none focus:border-primary focus:ring-ring md:w-56`} /> + {errors.name && ( +

{errors.name.message}

+ )}
+
{errors.permissions && ( -

Please select valid permissions.

+

+ {errors.permissions.message} +

)}
@@ -279,9 +297,18 @@ function CreateNewRolePage() { +
+ + +
+

Stock

+

Add and remove products

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SizeStockPrice
Small + + + setSmallPrice(event.target.value)} + /> +
+
+
Standard + + + + setStandardPrice(event.target.value) + } + /> +
+
+
Large + + + setLargePrice(event.target.value)} + /> +
+ + +
+ + +
+
+

Media

+
+

+ Upload media for your product. +

+
+
+ {image ? ( + Uploaded + ) : ( +

Image here

+ )} +
+ +
+
+ +
+
+

Status

+

Availability

+
+ +
+
+

Archive

+

Archive a product.

+
+ +
+
+
+ + + +
+

Comments

+ {comments.map((comment, index) => ( +
+
+ User Avatar +
+

{comment.name}

+

{`${comment.date} ${comment.time}`}

+
+
+

{comment.comment}

+
+ ))} + +
+ +
+
+ + ); +}; + +export default ProductDetail; diff --git a/src/app/dashboard/(user-dashboard)/products/productcard/page.tsx b/src/app/dashboard/(user-dashboard)/products/productcard/page.tsx new file mode 100644 index 000000000..baca39210 --- /dev/null +++ b/src/app/dashboard/(user-dashboard)/products/productcard/page.tsx @@ -0,0 +1,19 @@ +import productimage from "/public/images/productimage.png"; + +import ProductCard from "../_components/productcadrcomponent"; + +const Home: React.FC = () => { + return ( +
+ +
+ ); +}; + +export default Home; diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 5c5cff5e4..84430821d 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -1,9 +1,41 @@ +import { getAllNotifications } from "~/actions/notifications/getAllNotifications"; +import { getAnalytics, getStatistics } from "~/actions/organization"; +import { getAllProduct } from "~/actions/product"; import OrgContextProvider from "~/contexts/orgContext"; +import { auth } from "~/lib/auth"; -export default function GeneralLayout({ - children, -}: { +interface GeneralLayoutProperties { children: React.ReactNode; -}) { - return {children}; +} + +export default async function GeneralLayout( + properties: GeneralLayoutProperties, +) { + const session = await auth(); + + if (!session || !session.currentOrgId) { + return; + } + + const { children } = properties; + + const [notifications, statistics, analytics, products] = await Promise.all([ + getAllNotifications(), + getStatistics(), + getAnalytics(), + getAllProduct(session?.currentOrgId), + ]); + + return ( + + {children} + + ); } diff --git a/src/app/invite/page.tsx b/src/app/invite/page.tsx index 61e1c1984..e03c03903 100644 --- a/src/app/invite/page.tsx +++ b/src/app/invite/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useRouter } from "next/navigation"; // Import from next/navigation -import { useEffect } from "react"; +import { useCallback, useEffect } from "react"; import { acceptInviteRequest } from "~/actions/inviteMembers"; @@ -12,12 +12,12 @@ const extractToken = () => { ? queryString : queryString.slice(0, Math.max(0, ampersandIndex)); }; + const AcceptInvitePage = () => { const router = useRouter(); - // Function to extract token from the query string - - const handleAcceptInvite = async () => { + // Memoized function to handle invite acceptance + const handleAcceptInvite = useCallback(async () => { // Extract token using the function const token = extractToken(); @@ -56,12 +56,12 @@ const AcceptInvitePage = () => { // Handle unexpected errors router.push("/error?message=An unexpected error occurred"); } - }; + }, [router]); // Process the invite on page load useEffect(() => { handleAcceptInvite(); - }, []); + }, [handleAcceptInvite]); return (
diff --git a/src/components/authproviders/AuthProvider.tsx b/src/components/authproviders/AuthProvider.tsx deleted file mode 100644 index dcaa74598..000000000 --- a/src/components/authproviders/AuthProvider.tsx +++ /dev/null @@ -1,11 +0,0 @@ -function AuthProvider({ title }: { title: string }) { - return ( - <> - - - ); -} - -export default AuthProvider; diff --git a/src/components/card/user-card.tsx b/src/components/card/user-card.tsx index 8f37be0ac..ed6fac9af 100644 --- a/src/components/card/user-card.tsx +++ b/src/components/card/user-card.tsx @@ -1,10 +1,7 @@ -import axios from "axios"; import { ChevronDown } from "lucide-react"; import { signOut, useSession } from "next-auth/react"; import Link from "next/link"; -import { useCallback, useEffect, useState } from "react"; -import { getApiUrl } from "~/actions/getApiUrl"; import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar"; import { DropdownMenu, @@ -16,7 +13,6 @@ import { DropdownMenuShortcut, DropdownMenuTrigger, } from "~/components/ui/dropdown-menu"; -import { toast } from "~/components/ui/use-toast"; import { cn } from "~/lib/utils"; const handleLogout = async () => { @@ -27,55 +23,6 @@ const handleLogout = async () => { const UserCard = () => { const { data: session, status } = useSession(); - const { user } = session ?? {}; - const [profilePicUrl, setProfilePicUrl] = useState(""); - - const fetchProfileData = useCallback(async () => { - if (status === "authenticated" && user?.id) { - try { - const baseUrl = await getApiUrl(); - const API_URL = `${baseUrl}/api/v1/profile/${user.id}`; - const response = await axios.get(API_URL, { - headers: { - Authorization: `Bearer ${session?.access_token}`, - }, - }); - if (response.data?.data) { - const { avatar_url, profile_pic_url } = response.data.data; - setProfilePicUrl(avatar_url || profile_pic_url); - } - } catch { - toast({ - title: "Error", - description: "Failed to fetch profile data.", - variant: "destructive", - }); - } - } - }, [status, user?.id, session?.access_token]); - - useEffect(() => { - fetchProfileData(); - const handleProfileUpdate = (event: CustomEvent) => { - if (event.detail && event.detail.profilePicUrl) { - setProfilePicUrl(event.detail.profilePicUrl); - } else { - fetchProfileData(); - } - }; - - window.addEventListener( - "userProfileUpdate", - handleProfileUpdate as EventListener, - ); - - return () => { - window.removeEventListener( - "userProfileUpdate", - handleProfileUpdate as EventListener, - ); - }; - }, [fetchProfileData]); return ( @@ -91,11 +38,11 @@ const UserCard = () => { {status === "authenticated" && ( - {user?.first_name?.charAt(0)} + {session.user?.first_name?.charAt(0)} )} @@ -110,10 +57,10 @@ const UserCard = () => { - {user?.first_name} {user?.last_name} + {session?.user?.first_name} {session?.user?.last_name} - {user?.email ?? "Signed In"} + {session?.user?.email ?? "Signed In"} diff --git a/src/components/common/contact-us-form/index.tsx b/src/components/common/contact-us-form/index.tsx index 7609b15a6..1d8f692bc 100644 --- a/src/components/common/contact-us-form/index.tsx +++ b/src/components/common/contact-us-form/index.tsx @@ -1,13 +1,13 @@ "use client"; import { Mail } from "lucide-react"; +import { useSession } from "next-auth/react"; import { useEffect, useState } from "react"; import { z, ZodError } from "zod"; import { getApiUrl } from "~/actions/getApiUrl"; import { Toaster } from "~/components/ui/toaster"; import { useToast } from "~/components/ui/use-toast"; -import { useLocalStorage } from "~/hooks/use-local-storage"; import CustomButton from "../common-button/common-button"; import InputField from "./inputfield"; @@ -44,7 +44,7 @@ const ContactForm: React.FC = () => { const [status, setStatus] = useState(); const [message, setMessage] = useState(); const [loading, setLoading] = useState(false); - const [org_id] = useLocalStorage("current_orgid", ""); + const { data: session } = useSession(); const { toast } = useToast(); useEffect(() => { @@ -72,22 +72,6 @@ const ContactForm: React.FC = () => { } }; - function transformFormData( - formData: FormData, - orgId: string, - ): TransformedData { - // Create a new object with the required structure - const transformedData = { - full_name: formData.name, - email: formData.email, - phone_number: formData.phone, - message: formData.message, - org_id: orgId, // Pass orgId as a parameter or obtain it from your application context - }; - - return transformedData; - } - const handleChange = ( event: React.ChangeEvent, ) => { @@ -104,7 +88,13 @@ const ContactForm: React.FC = () => { } try { const baseUrl = await getApiUrl(); - const apiData = transformFormData(formData, org_id); + const apiData: TransformedData = { + full_name: formData.name, + email: formData.email, + phone_number: formData.phone, + message: formData.message, + org_id: session?.currentOrgId ?? "", + }; setLoading(true); const response = await fetch(`${baseUrl}/api/v1/contact`, { method: "POST", diff --git a/src/components/common/modals/invite-member/DeleteMembers/index.tsx b/src/components/common/modals/invite-member/DeleteMembers/index.tsx new file mode 100644 index 000000000..eb5c7e6ec --- /dev/null +++ b/src/components/common/modals/invite-member/DeleteMembers/index.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { Button } from "~/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "~/components/ui/dialog"; + +interface ModalProperties { + show: boolean; + onClose: () => void; +} + +const DeleteMember: React.FC = ({ show, onClose }) => { + return ( + + + + + + {" "} +

+ Delete Member +

+
+
+

+ Are you sure you want to delete Chad Bosewick ? All data will be + permanently removed. This action cannot be undone. +

+ +
+
+ + + +
+
+
+
+
+
+
+ ); +}; + +export default DeleteMember; diff --git a/src/components/common/modals/invite-member/index.tsx b/src/components/common/modals/invite-member/index.tsx index ce0f294af..ee93cd2b4 100644 --- a/src/components/common/modals/invite-member/index.tsx +++ b/src/components/common/modals/invite-member/index.tsx @@ -154,14 +154,14 @@ const InviteMemberModal: React.FC = ({ show, onClose }) => {
( "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50", className, )} + value={value} ref={reference} {...properties} /> diff --git a/src/components/waitList/WaitListForm.tsx b/src/components/waitList/WaitListForm.tsx index 549d53c73..d3950373b 100644 --- a/src/components/waitList/WaitListForm.tsx +++ b/src/components/waitList/WaitListForm.tsx @@ -131,19 +131,19 @@ const WaitlistForm: React.FC = () => { src="/images/WaitList/circle 1.svg" alt="Circle with Tick" /> -

+

Deployment made easy

-

+

You can level up your SaaS production today

-

+

Join our waitlist and get early access to our boilerplates

-
+
{isSubmitted ? (
{
diff --git a/src/components/waitList/WaitlistDialog.tsx b/src/components/waitList/WaitlistDialog.tsx index 39dd00a89..ce4c98955 100644 --- a/src/components/waitList/WaitlistDialog.tsx +++ b/src/components/waitList/WaitlistDialog.tsx @@ -4,8 +4,8 @@ import React from "react"; const WaitlistDialog: React.FC = () => { return (
-
-
+
+
{ height="25" />
-

+

Easy Customization

@@ -22,7 +22,7 @@ const WaitlistDialog: React.FC = () => { maximum results

-
+
{ height="30" />
-

+

Scalable Foundation

@@ -39,7 +39,7 @@ const WaitlistDialog: React.FC = () => { product

-
+
{ height="36" />
-

+

Pre-built Sections

@@ -65,8 +65,12 @@ const WaitlistDialog: React.FC = () => {

-

Resource variety

-

150+ Team support

+

+ Resource variety +

+

+ 150+ Team support +

diff --git a/src/components/waitList/WaitlistJoin.tsx b/src/components/waitList/WaitlistJoin.tsx index bbcdc9239..c2a7f1c06 100644 --- a/src/components/waitList/WaitlistJoin.tsx +++ b/src/components/waitList/WaitlistJoin.tsx @@ -14,21 +14,21 @@ const scrollToForm = () => { const WaitlistJoin: React.FC = () => { return (
-
+
waitlist
-
-

+
+

Join the waitlist and get early{" "} access!

-

+

Transform your remote work meetings into fun and engaging sessions with our innovative game-based platform.

-

+

Be a part of the first 2300 users for a{" "} 10% discount{" "}

diff --git a/src/components/waitList/WaitlistPayment.tsx b/src/components/waitList/WaitlistPayment.tsx index 9fa2a9991..947bf9096 100644 --- a/src/components/waitList/WaitlistPayment.tsx +++ b/src/components/waitList/WaitlistPayment.tsx @@ -95,8 +95,10 @@ const WaitlistPayment: React.FC = () => { return (
-

We have got you covered

-

+

+ We have got you covered +

+

Transform your Deployment the easy and seamless way with our boilerplates.

diff --git a/src/config/auth.config.ts b/src/config/auth.config.ts index 44768ac63..3206ae8d7 100644 --- a/src/config/auth.config.ts +++ b/src/config/auth.config.ts @@ -43,6 +43,7 @@ export default { } const user = { + ...response.data, ...response.data.user, access_token: response.access_token, }; @@ -73,7 +74,7 @@ export default { return token; } - const response = await googleAuth({ id_token: account?.id_token }); + const response = await googleAuth(account?.id_token); if (!response.data) { token = { @@ -127,21 +128,16 @@ export default { const customToken = token as CustomJWT; session.user = { id: customToken.id as string, - first_name: - customToken.first_name || - customToken.name?.split(" ")[0] || - customToken.fullname?.split(" ")[0] || - "", - last_name: - customToken.last_name || - customToken.name?.split(" ").slice(1).join(" ") || - customToken.fullname?.split(" ").slice(1).join(" ") || - "", - image: token.picture || customToken.avatar_url || "", - role: customToken.role as string, - email: token.email as string, + first_name: customToken.first_name, + last_name: customToken.last_name, + image: customToken.avatar_url || "", + email: customToken.email as string, }; session.access_token = customToken.access_token; + session.userOrg = customToken.organisations; + session.currentOrgId = + customToken.organisations && + customToken.organisations[0]?.organisation_id; return session; }, diff --git a/src/contexts/orgContext.tsx b/src/contexts/orgContext.tsx index 7a90aeecc..9712adfaa 100644 --- a/src/contexts/orgContext.tsx +++ b/src/contexts/orgContext.tsx @@ -1,27 +1,34 @@ "use client"; +import { useSession } from "next-auth/react"; import React, { createContext, useCallback, useContext, useEffect, - useLayoutEffect, - useMemo, useState, - useTransition, } from "react"; -import { getAllProduct } from "~/actions/product"; -import { useLocalStorage } from "~/hooks/use-local-storage"; import { DashboardData, MonthlyData, Organisation, Product } from "~/types"; -import { - getAllOrg, - getAnalytics, - getStatistics, -} from "../actions/organization"; type ActiveFilter = "in stock" | "out of stock" | "preorder" | "all"; +interface NotificationPreview { + message: string; + created_at: string; + is_read: boolean; + id: string; +} + +interface NotificationsData { + data: { + total_unread_notification_count: number; + total_notification_count: number; + notifications: NotificationPreview[]; + }; + message: string; +} + interface OrgContextProperties { organizations: Organisation[]; isLoading: boolean; @@ -41,96 +48,55 @@ interface OrgContextProperties { isActionModal: boolean; setIsActionModal: React.Dispatch>; switchOrganization: (orgId: string) => void; + notifications: NotificationsData | undefined; +} + +interface SessionCheckerProperties { + children: React.ReactNode; } export const OrgContext = createContext({} as OrgContextProperties); -const OrgContextProvider = ({ children }: { children: React.ReactNode }) => { - const [organizations, setOrganizations] = useState([]); - const [isLoading, startTransition] = useTransition(); - const [monthlyData, setMonthlydata] = useState(); - const [dashboardData, setDashboardData] = useState< - DashboardData | undefined - >(); - const [products, setProducts] = useState([]); - const [org_id, setOrgId] = useLocalStorage("current_orgid", ""); - const [selectedProduct, setSelectedProduct] = useState(""); +interface ProviderProperties { + children: React.ReactNode; + initialData: { + monthlyData?: MonthlyData; + dashboardData?: DashboardData; + products?: Product[]; + notifications?: NotificationsData; + }; +} +const OrgContextProvider = (properties: ProviderProperties) => { + const { children, initialData } = properties; + + const monthlyData = initialData.monthlyData; + const dashboardData = initialData.dashboardData; + const products = initialData.products || []; + const notifications = initialData.notifications; + + const { data: session, update, status } = useSession(); + + const [selectedProduct, setSelectedProduct] = useState(""); const [isNewModal, setIsNewModal] = useState(false); const [isDelete, setIsDelete] = useState(false); const [isOpen, updateOpen] = useState(false); const [isActionModal, setIsActionModal] = useState(false); const [active_filter, setActive_filter] = useState("all"); - const [userOrg] = useLocalStorage("user_org", []); - const isAnyModalOpen = isNewModal || isDelete || isOpen || isActionModal; + const isLoading = status === "loading"; const switchOrganization = useCallback( (orgId: string) => { - setOrgId(orgId); + if (session) { + const updatedSession = { ...session, currentOrgId: orgId }; + update(() => updatedSession); + } }, - [setOrgId], + [session, update], ); - - useLayoutEffect(() => { - if (organizations.length === 0) { - startTransition(() => { - getAllOrg().then((data) => { - const fetchedOrganizations = (data && data.organization) || []; - const newOrganizations = fetchedOrganizations.filter( - (fetchedOrg: { organisation_id: string }) => - !organizations.some( - (org) => org.organisation_id === fetchedOrg.organisation_id, - ), - ); - - if (newOrganizations.length > 0) { - setOrganizations((previousOrganizations) => [ - ...previousOrganizations, - ...newOrganizations, - ]); - } - }); - - getStatistics().then((subResponse) => { - setDashboardData(subResponse.data); - }); - - getAnalytics().then((data) => { - if (data && data.data) { - const formattedData: MonthlyData = Object.keys(data.data).map( - (key) => ({ - month: key, - revenue: data.data[key], - }), - ); - setMonthlydata(formattedData); - } - }); - }); - } - }, [organizations]); - - useEffect(() => { - if (userOrg.length > 0) { - const uniqueOrgs = userOrg.filter( - (org) => - !organizations.some( - (existingOrg) => - existingOrg.organisation_id === org.organisation_id, - ), - ); - setOrganizations((previousOrganizations) => [ - ...previousOrganizations, - ...uniqueOrgs, - ]); - } - }, [userOrg, organizations]); - useEffect(() => { - document.body.style.overflow = isAnyModalOpen ? "hidden" : "auto"; - - const handleKeyDown = (entries: KeyboardEvent) => { - if (entries.key === "Escape") { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { setIsNewModal(false); setIsDelete(false); updateOpen(false); @@ -138,60 +104,46 @@ const OrgContextProvider = ({ children }: { children: React.ReactNode }) => { } }; + document.body.style.overflow = isAnyModalOpen ? "hidden" : "auto"; document.addEventListener("keydown", handleKeyDown); - - return () => { - document.removeEventListener("keydown", handleKeyDown); - }; + return () => document.removeEventListener("keydown", handleKeyDown); }, [isAnyModalOpen]); - useLayoutEffect(() => { - if (!org_id || org_id === undefined) return; - startTransition(() => { - getAllProduct(org_id).then((data) => { - setProducts(data.products || []); - }); - }); - }, [org_id]); + function SessionChecker({ children }: SessionCheckerProperties) { + if (status === "loading") { + return
Loading...
; + } - const value = useMemo( - () => ({ - isLoading, - organizations, - monthlyData, - dashboardData, - products, - selectedProduct, - setSelectedProduct, - isNewModal, - setIsNewModal, - isDelete, - setIsDelete, - isOpen, - updateOpen, - isActionModal, - setIsActionModal, - active_filter, - setActive_filter, - switchOrganization, - }), - [ - isLoading, - organizations, - monthlyData, - dashboardData, - products, - selectedProduct, - isNewModal, - isDelete, - isOpen, - isActionModal, - active_filter, - switchOrganization, - ], - ); + return <>{children}; + } - return {children}; + return ( + + {children} + + ); }; export const useOrgContext = () => { @@ -204,234 +156,3 @@ export const useOrgContext = () => { }; export default OrgContextProvider; - -// "use client"; - -// import React, { -// createContext, -// useContext, -// useEffect, -// useLayoutEffect, -// useMemo, -// useReducer, -// useTransition, -// } from "react"; - -// import { getAllProduct } from "~/actions/product"; -// import { useLocalStorage } from "~/hooks/use-local-storage"; -// import { DashboardData, MonthlyData, Organisation, Product } from "~/types"; -// import { -// getAllOrg, -// getAnalytics, -// getStatistics, -// } from "../actions/organization"; - -// type ActiveFilter = "in stock" | "out of stock" | "preorder" | "all"; - -// interface OrgState { -// organizations: Organisation[]; -// isLoading: boolean; -// monthlyData: MonthlyData | undefined; -// dashboardData: DashboardData | undefined; -// products: Product[]; -// selectedProduct: string; -// isNewModal: boolean; -// isDelete: boolean; -// isOpen: boolean; -// isActionModal: boolean; -// active_filter: ActiveFilter; -// } - -// type OrgAction = -// | { type: "SET_ORGANIZATIONS"; payload: Organisation[] } -// | { type: "SET_LOADING"; payload: boolean } -// | { type: "SET_MONTHLY_DATA"; payload: MonthlyData | undefined } -// | { type: "SET_DASHBOARD_DATA"; payload: DashboardData | undefined } -// | { type: "SET_PRODUCTS"; payload: Product[] } -// | { type: "SET_SELECTED_PRODUCT"; payload: string } -// | { type: "TOGGLE_NEW_MODAL"; payload: boolean } -// | { type: "TOGGLE_DELETE"; payload: boolean } -// | { type: "TOGGLE_OPEN"; payload: boolean } -// | { type: "TOGGLE_ACTION_MODAL"; payload: boolean } -// | { type: "SET_ACTIVE_FILTER"; payload: ActiveFilter }; - -// const initialState: OrgState = { -// organizations: [], -// isLoading: false, -// monthlyData: undefined, -// dashboardData: undefined, -// products: [], -// selectedProduct: "", -// isNewModal: false, -// isDelete: false, -// isOpen: false, -// isActionModal: false, -// active_filter: "all", -// }; - -// function orgReducer(state: OrgState, action: OrgAction): OrgState { -// switch (action.type) { -// case "SET_ORGANIZATIONS": { -// return { ...state, organizations: action.payload }; -// } -// case "SET_LOADING": { -// return { ...state, isLoading: action.payload }; -// } -// case "SET_MONTHLY_DATA": { -// return { ...state, monthlyData: action.payload }; -// } -// case "SET_DASHBOARD_DATA": { -// return { ...state, dashboardData: action.payload }; -// } -// case "SET_PRODUCTS": { -// return { ...state, products: action.payload }; -// } -// case "SET_SELECTED_PRODUCT": { -// return { ...state, selectedProduct: action.payload }; -// } -// case "TOGGLE_NEW_MODAL": { -// return { ...state, isNewModal: action.payload }; -// } -// case "TOGGLE_DELETE": { -// return { ...state, isDelete: action.payload }; -// } -// case "TOGGLE_OPEN": { -// return { ...state, isOpen: action.payload }; -// } -// case "TOGGLE_ACTION_MODAL": { -// return { ...state, isActionModal: action.payload }; -// } -// case "SET_ACTIVE_FILTER": { -// return { ...state, active_filter: action.payload }; -// } -// default: { -// return state; -// } -// } -// } - -// export const OrgContext = createContext<{ -// state: OrgState; -// dispatch: React.Dispatch; -// }>({ state: initialState, dispatch: () => {} }); - -// const OrgContextProvider = ({ children }: { children: React.ReactNode }) => { -// const [state, dispatch] = useReducer(orgReducer, initialState); -// const [, startTransition] = useTransition(); -// const [org_id] = useLocalStorage("current_orgid", ""); -// const [userOrg] = useLocalStorage("user_org", []); - -// const isAnyModalOpen = -// state.isNewModal || state.isDelete || state.isOpen || state.isActionModal; - -// useLayoutEffect(() => { -// if (state.organizations.length === 0) { -// startTransition(() => { -// getAllOrg().then((data) => { -// const fetchedOrganizations = data.organization || []; -// const newOrganizations = fetchedOrganizations.filter( -// (fetchedOrg: { organisation_id: string }) => -// !state.organizations.some( -// (org) => org.organisation_id === fetchedOrg.organisation_id, -// ), -// ); - -// if (newOrganizations.length > 0) { -// dispatch({ -// type: "SET_ORGANIZATIONS", -// payload: [...state.organizations, ...newOrganizations], -// }); -// } -// }); - -// getStatistics().then((subResponse) => { -// dispatch({ -// type: "SET_DASHBOARD_DATA", -// payload: subResponse.data, -// }); -// }); - -// getAnalytics().then((data) => { -// const formattedData: MonthlyData = Object.keys(data.data).map( -// (key) => ({ -// month: key, -// revenue: data.data[key], -// }), -// ); -// dispatch({ -// type: "SET_MONTHLY_DATA", -// payload: formattedData, -// }); -// }); -// }); -// } -// }, [state.organizations]); - -// useEffect(() => { -// if (userOrg.length > 0) { -// const uniqueOrgs = userOrg.filter( -// (org) => -// !state.organizations.some( -// (existingOrg) => -// existingOrg.organisation_id === org.organisation_id, -// ), -// ); -// dispatch({ -// type: "SET_ORGANIZATIONS", -// payload: [...state.organizations, ...uniqueOrgs], -// }); -// } -// }, [userOrg]); - -// useEffect(() => { -// document.body.style.overflow = isAnyModalOpen ? "hidden" : "auto"; - -// const handleKeyDown = (entries: KeyboardEvent) => { -// if (entries.key === "Escape") { -// dispatch({ type: "TOGGLE_NEW_MODAL", payload: false }); -// dispatch({ type: "TOGGLE_DELETE", payload: false }); -// dispatch({ type: "TOGGLE_OPEN", payload: false }); -// dispatch({ type: "TOGGLE_ACTION_MODAL", payload: false }); -// } -// }; - -// document.addEventListener("keydown", handleKeyDown); - -// return () => { -// document.removeEventListener("keydown", handleKeyDown); -// }; -// }, [isAnyModalOpen]); - -// useLayoutEffect(() => { -// if (!org_id || org_id === undefined) return; -// startTransition(() => { -// getAllProduct(org_id).then((data) => { -// dispatch({ -// type: "SET_PRODUCTS", -// payload: data.products || [], -// }); -// }); -// }); -// }, [org_id]); - -// const value = useMemo( -// () => ({ -// state, -// dispatch, -// }), -// [state], -// ); - -// return {children}; -// }; - -// export const useOrgContext = () => { -// const context = useContext(OrgContext); - -// if (!context) { -// throw new Error("useOrgContext must be used within a OrgContextProvider"); -// } -// return context; -// }; - -// export default OrgContextProvider; diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 18de73deb..347e07c7c 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,7 +1,7 @@ import NextAuth, { type DefaultSession } from "next-auth"; import authConfig from "~/config/auth.config"; -import { User } from "~/types"; +import { Organisation, User } from "~/types"; export const { handlers: { GET, POST }, @@ -19,11 +19,12 @@ declare module "next-auth" { last_name: User["last_name"]; email: User["email"]; image: User["avatar_url"]; - role: User["role"]; bio?: string; username?: string; is_superadmin?: boolean; } & DefaultSession["user"]; access_token?: string; + currentOrgId?: string; + userOrg?: Organisation[]; } } diff --git a/src/schemas/index.ts b/src/schemas/index.ts index 734a3fe96..03bf949cc 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -121,14 +121,25 @@ export const permissionsSchema = z.object({ "Can create users": z.boolean(), "Can blacklist/whitelist users": z.boolean(), }); + export const roleSchema = z.object({ - name: z.string().min(2, { - message: "name is required", - }), + name: z + .string() + .min(2, { message: "Name is required" }) + .max(50, { message: "Name must be 50 characters or less" }) + .regex(/^[\d !(),.?A-Za-z-]+$/, { + message: + "Name can only include letters, numbers, spaces, and common punctuation marks", + }), + description: z + .string() + .min(10, { message: "Role description must be at least 10 characters" }) + .max(200, { message: "Role description must be 200 characters or less" }) + .regex(/^[\d !(),.?A-Za-z-]+$/, { + message: + "Description can only include letters, numbers, spaces, and common punctuation marks", + }), permissions: z .array(z.string().uuid()) .nonempty("At least one permission must be selected"), - description: z.string().min(2, { - message: "Role description must be at least 2 characters.", - }), }); diff --git a/src/test/blog/comment/comment-body.test.tsx b/src/test/blog/comment/comment-body.test.tsx index 4fac54610..a486ccbf1 100644 --- a/src/test/blog/comment/comment-body.test.tsx +++ b/src/test/blog/comment/comment-body.test.tsx @@ -13,7 +13,6 @@ describe("comment body component", () => { last_name: "User", email: "user@example.com", image: "path/to/image", - role: "user", }, access_token: "some-token", expires: "1", @@ -35,6 +34,7 @@ describe("comment body component", () => { onLike={vi.fn()} onDislike={vi.fn()} onReply={vi.fn()} + isReplyActive={false} />, ); expect(screen.getByTestId("comment-text")).toHaveTextContent( @@ -64,6 +64,7 @@ describe("comment body component", () => { dislikes={0} onLike={mockOnLike} onDislike={mockOnDislike} + isReplyActive={false} />, ); @@ -93,6 +94,7 @@ describe("comment body component", () => { dislikes={3} onLike={vi.fn()} onDislike={vi.fn()} + isReplyActive={false} />, ); @@ -118,6 +120,7 @@ describe("comment body component", () => { onLike={vi.fn()} onDislike={vi.fn()} onReply={vi.fn()} + isReplyActive={false} />, ); @@ -142,6 +145,7 @@ describe("comment body component", () => { onLike={vi.fn()} onDislike={vi.fn()} onReply={vi.fn()} + isReplyActive={false} />, ); expect( diff --git a/src/test/blog/comment/comment-reply.test.tsx b/src/test/blog/comment/comment-reply.test.tsx index ce980678e..9af91c8be 100644 --- a/src/test/blog/comment/comment-reply.test.tsx +++ b/src/test/blog/comment/comment-reply.test.tsx @@ -12,7 +12,6 @@ describe("comment reply component", () => { last_name: "User", email: "user@example.com", image: "path/to/image", - role: "user", }, access_token: "some-token", expires: "1", diff --git a/src/test/blog/comment/index.test.tsx b/src/test/blog/comment/index.test.tsx index 47dc199bd..afc19231d 100644 --- a/src/test/blog/comment/index.test.tsx +++ b/src/test/blog/comment/index.test.tsx @@ -15,7 +15,6 @@ describe("comment box component", () => { last_name: "User", email: "user@example.com", image: "path/to/image", - role: "user", }, access_token: "some-token", expires: "2100-01-01T00:00:00.000Z", diff --git a/src/test/contact.test.tsx b/src/test/contact.test.tsx index f5e2ef608..c0847a0e7 100644 --- a/src/test/contact.test.tsx +++ b/src/test/contact.test.tsx @@ -1,11 +1,19 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { SessionProvider } from "next-auth/react"; +import { ReactNode } from "react"; import Contact from "~/app/(landing-routes)/contact-us/page"; describe("contact Page tests", () => { + const renderWithSession = (component: ReactNode) => { + return render( + {component}, + ); + }; + it("should render the Contact Us form and content card correctly", () => { expect.assertions(4); - render(); + renderWithSession(); expect(screen.getByRole("form")).toBeInTheDocument(); @@ -18,7 +26,7 @@ describe("contact Page tests", () => { it("should validate the form inputs correctly", async () => { expect.assertions(1); - render(); + renderWithSession(); fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "invalid-email" }, @@ -32,7 +40,7 @@ describe("contact Page tests", () => { it("should be responsive", () => { expect.assertions(2); - render(); + renderWithSession(); window.innerWidth = 320; window.dispatchEvent(new Event("resize")); diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 6d117589b..a847ac733 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -6,24 +6,24 @@ export interface CustomJWT extends JWT { email?: string; picture?: string; avatar_url?: string; - role?: string; first_name?: string; last_name?: string; fullname?: string; access_token?: string; + organisations?: Organisation[]; } export interface CustomSession extends Session { user: { id: string; - name: string; first_name: string; last_name: string; email: string; image: string; - role: string; }; expires: DefaultSession["expires"]; access_token?: string; + currentOrgId?: string; + userOrg?: Organisation[]; } export interface User {