diff --git a/src/app/company/dashboard/bookings/page.tsx b/src/app/company/dashboard/bookings/page.tsx new file mode 100644 index 0000000..a3363c5 --- /dev/null +++ b/src/app/company/dashboard/bookings/page.tsx @@ -0,0 +1,99 @@ +"use client"; +import { Input } from "@/components/ui/input"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger +} from "@/components/ui/dropdown-menu"; +import { Button } from "@/components/ui/button"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { extractNameInitials } from "@/lib/utils"; +import React from "react"; +import { useRouter } from "next/navigation"; +import { deleteToken } from "@/lib/authActions"; +import Loader from "@/components/layout/Loader"; +import { useCompany } from "@/components/dashboard/CompanyContext"; + +export default function Page() { + const { user, loading, companyLoading, company } = useCompany(); + const router = useRouter(); + + const logout = async () => { + try { + await deleteToken(); + window.location.reload(); + } catch (logoutError) { + console.error("Logout failed", logoutError); + throw logoutError; + } + }; + + if (loading || companyLoading) return ; + + return ( +
+
+

+ {company?.getCompany.name} (ID: {company?.getCompany.id}) +

+
+ + + + + + + +
+

{user?.name}

+

{user?.email}

+
+
+ + { + router.push("/dashboard/settings"); + }}> + Settings + + + + Log out + +
+
+
+
+ +
+
+
+
+ {extractNameInitials(company?.getCompany.name)} +
+
+

{company?.getCompany.name}

+
+
+
+

+ {company?.getCompany.description !== "" ? ( + company?.getCompany.description + ) : ( + This company has not provided a description + )} +

+
+
+ ); +} diff --git a/src/app/company/dashboard/layout.tsx b/src/app/company/dashboard/layout.tsx new file mode 100644 index 0000000..167d2a9 --- /dev/null +++ b/src/app/company/dashboard/layout.tsx @@ -0,0 +1,120 @@ +"use client"; +import { Button } from "@/components/ui/button"; +import { LuBookCopy, LuLayoutDashboard } from "react-icons/lu"; +import React, { Suspense, useEffect, useState } from "react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { cn, extractNameInitials } from "@/lib/utils"; +import Loader from "@/components/layout/Loader"; +import { CompanyProvider, useCompany } from "@/components/dashboard/CompanyContext"; +import { BriefcaseBusiness, Users } from "lucide-react"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; + +function DashboardContent({ children }: { children: React.ReactNode }) { + const { companyLoading, loading, company } = useCompany(); + const [active, setActive] = useState<"dashboard" | "bookings" | "users" | "members">("dashboard"); + const pathname = usePathname(); + + useEffect(() => { + // Derive the active state based on the current pathname + if (pathname.includes("/company/dashboard/bookings")) { + setActive("bookings"); + } else if (pathname.includes("/company/dashboard/users")) { + setActive("users"); + } else if (pathname.includes("/company/dashboard/members")) { + setActive("members"); + } else { + setActive("dashboard"); + } + }, [pathname]); + + return ( +
+ +
+ {loading || companyLoading ? : }>{children}} +
+

MeetMate

+
+
+
+ ); +} + +// Wrap the dashboard with the UserProvider +export default function DashboardLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/src/app/company/dashboard/members/page.tsx b/src/app/company/dashboard/members/page.tsx new file mode 100644 index 0000000..a3363c5 --- /dev/null +++ b/src/app/company/dashboard/members/page.tsx @@ -0,0 +1,99 @@ +"use client"; +import { Input } from "@/components/ui/input"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger +} from "@/components/ui/dropdown-menu"; +import { Button } from "@/components/ui/button"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { extractNameInitials } from "@/lib/utils"; +import React from "react"; +import { useRouter } from "next/navigation"; +import { deleteToken } from "@/lib/authActions"; +import Loader from "@/components/layout/Loader"; +import { useCompany } from "@/components/dashboard/CompanyContext"; + +export default function Page() { + const { user, loading, companyLoading, company } = useCompany(); + const router = useRouter(); + + const logout = async () => { + try { + await deleteToken(); + window.location.reload(); + } catch (logoutError) { + console.error("Logout failed", logoutError); + throw logoutError; + } + }; + + if (loading || companyLoading) return ; + + return ( +
+
+

+ {company?.getCompany.name} (ID: {company?.getCompany.id}) +

+
+ + + + + + + +
+

{user?.name}

+

{user?.email}

+
+
+ + { + router.push("/dashboard/settings"); + }}> + Settings + + + + Log out + +
+
+
+
+ +
+
+
+
+ {extractNameInitials(company?.getCompany.name)} +
+
+

{company?.getCompany.name}

+
+
+
+

+ {company?.getCompany.description !== "" ? ( + company?.getCompany.description + ) : ( + This company has not provided a description + )} +

+
+
+ ); +} diff --git a/src/app/company/dashboard/users/page.tsx b/src/app/company/dashboard/users/page.tsx new file mode 100644 index 0000000..a3363c5 --- /dev/null +++ b/src/app/company/dashboard/users/page.tsx @@ -0,0 +1,99 @@ +"use client"; +import { Input } from "@/components/ui/input"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger +} from "@/components/ui/dropdown-menu"; +import { Button } from "@/components/ui/button"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { extractNameInitials } from "@/lib/utils"; +import React from "react"; +import { useRouter } from "next/navigation"; +import { deleteToken } from "@/lib/authActions"; +import Loader from "@/components/layout/Loader"; +import { useCompany } from "@/components/dashboard/CompanyContext"; + +export default function Page() { + const { user, loading, companyLoading, company } = useCompany(); + const router = useRouter(); + + const logout = async () => { + try { + await deleteToken(); + window.location.reload(); + } catch (logoutError) { + console.error("Logout failed", logoutError); + throw logoutError; + } + }; + + if (loading || companyLoading) return ; + + return ( +
+
+

+ {company?.getCompany.name} (ID: {company?.getCompany.id}) +

+
+ + + + + + + +
+

{user?.name}

+

{user?.email}

+
+
+ + { + router.push("/dashboard/settings"); + }}> + Settings + + + + Log out + +
+
+
+
+ +
+
+
+
+ {extractNameInitials(company?.getCompany.name)} +
+
+

{company?.getCompany.name}

+
+
+
+

+ {company?.getCompany.description !== "" ? ( + company?.getCompany.description + ) : ( + This company has not provided a description + )} +

+
+
+ ); +} diff --git a/src/app/dashboard/bookings/page.tsx b/src/app/dashboard/bookings/page.tsx index e12bf11..6bde682 100644 --- a/src/app/dashboard/bookings/page.tsx +++ b/src/app/dashboard/bookings/page.tsx @@ -40,40 +40,15 @@ import { Progress } from "@/components/ui/progress"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Calendar } from "@/components/ui/calendar"; import { useDashboardData } from "@/components/dashboard/DashboardContext"; -import { type Appointment, type Company } from "@/types"; +import { type Appointment } from "@/types"; import { useSelector } from "react-redux"; import { type RootState } from "@/store/store"; function Bookings() { - const { user } = useDashboardData(); + const { user, companies } = useDashboardData(); const [searchQuery, setSearchQuery] = useState(""); const appointments = useSelector((state: RootState) => state.collection.appointments); - const [companies, setCompanies] = useState([ - { - id: "1", - name: "Github", - createdAt: "2024-01-01", - description: "Version control platform", - owner: user!, - members: [], - settings: { - appointmentDuration: 60, - appointmentBuffer: 15, - appointmentTypes: ["Code Review", "Project Planning"], - appointmentLocations: ["Online", "Office"], - openingHours: { - from: "09:00", - to: "17:00" - } - } - } - ]); - - useEffect(() => { - if (false) setCompanies([]); - }, []); - const [filteredAppointments, setFilteredAppointments] = useState(appointments); const router = useRouter(); @@ -246,7 +221,7 @@ function Bookings() { - {companies.map((company) => ( + {companies?.getCompanies.map((company) => ( {company.name} @@ -291,7 +266,7 @@ function Bookings() { Confirm Booking Please confirm your appointment details:
-

Company: {companies.find((c) => c.id === bookingState.selectedCompany)?.name}

+

Company: {companies?.getCompanies.find((c) => c.id === bookingState.selectedCompany)?.name}

Date: {bookingState.selectedDate.toDateString()}

Time: {bookingState.selectedTime}

diff --git a/src/app/dashboard/browse/page.tsx b/src/app/dashboard/browse/page.tsx new file mode 100644 index 0000000..cb02e4b --- /dev/null +++ b/src/app/dashboard/browse/page.tsx @@ -0,0 +1,89 @@ +"use client"; +import React from "react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger +} from "@/components/ui/dropdown-menu"; +import { Button } from "@/components/ui/button"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { extractNameInitials } from "@/lib/utils"; +import { useRouter } from "next/navigation"; +import { deleteToken } from "@/lib/authActions"; +import FollowButton from "@/components/dashboard/FollowButton"; +import { useDashboardData } from "@/components/dashboard/DashboardContext"; + +function CompanyBrowse() { + const { user, companies } = useDashboardData(); + const router = useRouter(); + + const logout = async () => { + try { + await deleteToken(); + window.location.reload(); + } catch (error) { + console.error("Logout failed", error); + throw error; + } + }; + + return ( +
+
+

Browse Companies

+
+ + + + + + +
+

{user?.name}

+

{user?.email}

+
+
+ + { + router.push("/dashboard/settings"); + }}> + Settings + + + + Log out + +
+
+
+
+ +
+ {companies?.getCompanies.map((company) => ( +
+

{company.name}

+

{company.description}

+ +
+ ))} +
+
+ ); +} + +export default CompanyBrowse; diff --git a/src/app/dashboard/company/[id]/page.tsx b/src/app/dashboard/company/[id]/page.tsx index b2efe61..84c436a 100644 --- a/src/app/dashboard/company/[id]/page.tsx +++ b/src/app/dashboard/company/[id]/page.tsx @@ -14,27 +14,43 @@ import { extractNameInitials } from "@/lib/utils"; import React from "react"; import { useRouter } from "next/navigation"; import { deleteToken } from "@/lib/authActions"; -import Image from "next/image"; +import FollowButton from "@/components/dashboard/FollowButton"; +import { useQuery } from "@apollo/client"; +import { getCompany } from "@/lib/graphql/queries"; +import Loader from "@/components/layout/Loader"; import { useDashboardData } from "@/components/dashboard/DashboardContext"; export default function Page({ params }: { params: { id: string } }) { - const user = useDashboardData().user; - + const { user } = useDashboardData(); const router = useRouter(); + const { loading, error, data } = useQuery(getCompany, { + variables: { id: params.id }, + // Optional: configure caching and refetching + fetchPolicy: "cache-and-network", + // Optional: refetch every 5 minutes + pollInterval: 300000 + }); const logout = async () => { try { await deleteToken(); window.location.reload(); - } catch (error) { - console.error("Logout failed", error); - throw error; + } catch (logoutError) { + console.error("Logout failed", logoutError); + throw logoutError; } }; + + if (loading) return ; + + if (error !== undefined) return
Error: {error.message}
; + return (
-

GitHub (ID: {params.id})

+

+ {data.getCompany.name} (ID: {params.id}) +

@@ -68,40 +84,27 @@ export default function Page({ params }: { params: { id: string } }) {
- {""} -
- {""} +
+ {extractNameInitials(data.getCompany.name as string)} +
-

GitHub

-

- GitHub is a code hosting platform for version control and collaboration. It lets you and others work - together on projects from anywhere. -

+

{data.getCompany.name}

- +

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore - magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo - consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla - pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. + {data.getCompany.description !== "" ? ( + data.getCompany.description + ) : ( + This company has not provided a description + )}

diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index dd5b240..2412cb9 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -1,46 +1,19 @@ "use client"; import Image from "next/image"; import { Button } from "@/components/ui/button"; -import { LuBookCopy, LuBuilding, LuGauge, LuLayoutDashboard, LuSettings, LuUser2, LuHome } from "react-icons/lu"; +import { LuBookCopy, LuHome, LuLayoutDashboard, LuSettings } from "react-icons/lu"; import React, { Suspense, useEffect, useState } from "react"; -import type { User } from "@/types"; -import { getAccessToken, getUser } from "@/lib/authActions"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { cn } from "@/lib/utils"; import Loader from "@/components/layout/Loader"; -import { DashboardProvider } from "@/components/dashboard/DashboardContext"; +import { DashboardProvider, useDashboardData } from "@/components/dashboard/DashboardContext"; +import { FaPlus } from "react-icons/fa6"; -export default function DashboardLayout({ children }: { children: React.ReactNode }) { - const [loading, setLoading] = useState(true); - const [user, setUser] = useState(); - const [isAdmin, setIsAdmin] = useState(user?.role === "ADMIN"); +function DashboardContent({ children }: { children: React.ReactNode }) { + const { loading, companies, companiesLoading, user } = useDashboardData(); const [active, setActive] = useState<"dashboard" | "bookings" | "settings">("dashboard"); const [companyIndicatorTop, setCompanyIndicatorTop] = useState(0); - - const companies = [ - { id: "29103", name: "Company 1" }, - { id: "61241", name: "Company 2" }, - { id: "1241", name: "Company 3" } - ]; - - useEffect(() => { - const fetchUser = async () => { - setLoading(true); - try { - const accessToken = await getAccessToken(); - setUser(await getUser(accessToken)); - setIsAdmin(user?.role === "ADMIN"); - } catch (error) { - setIsAdmin(false); - console.error("Failed to fetch user", error); - } finally { - setLoading(false); - } - }; - fetchUser().catch(console.error); - }, []); - const pathname = usePathname(); useEffect(() => { @@ -53,10 +26,18 @@ export default function DashboardLayout({ children }: { children: React.ReactNod setActive("dashboard"); } - // Derive the top position of the company indicator - const companyIndex = companies.findIndex((company) => pathname.includes(company.id)); - setCompanyIndicatorTop(companyIndex === -1 ? 40 : 144 + 72 * companyIndex); - }, [pathname]); + if (pathname.includes("/dashboard/browse") && companies !== undefined) { + if ( + companies?.getCompanies.filter((company) => user?.subscribedCompanies.includes(Number(company.id))).length === 0 + ) { + setCompanyIndicatorTop(144); + } else setCompanyIndicatorTop(companies?.getCompanies.length * 72 + 144); + } else { + // Derive the top position of the company indicator + const companyIndex = companies?.getCompanies.findIndex((company) => pathname.includes(company.id)) ?? 0; + setCompanyIndicatorTop(companyIndex === -1 ? 40 : 144 + 72 * companyIndex); + } + }, [pathname, companies]); return (
@@ -74,11 +55,23 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
- {companies.map((company) => ( - -
- - ))} + {companies?.getCompanies + ?.filter((company) => user?.subscribedCompanies.includes(Number(company.id))) + .map((company) => ( + +
+ {company.name[0] + company.name[1]} +
+ + ))} + +
+ +
+
@@ -92,27 +85,30 @@ export default function DashboardLayout({ children }: { children: React.ReactNod active === "dashboard" ? "text-foreground" : "text-muted-foreground" )} variant={active === "dashboard" ? "default" : "ghost"}> - {!pathname.includes("/dashboard/company") ? ( + {!pathname.includes("/dashboard/company") && !pathname.includes("/dashboard/browse") ? ( ) : ( )} - {pathname.includes("/dashboard/company") ? "Home" : "Dashboard"} + {pathname.includes("/dashboard/company") || pathname.includes("/dashboard/browse") + ? "Home" + : "Dashboard"} - {!companies.some((company) => pathname.includes(company.id)) && ( - - - - )} + {companies?.getCompanies.some((company) => pathname.includes(company.id)) === false && + !pathname.includes("/dashboard/browse") && ( + + + + )} - )} + )} */}
- {loading ? ( - - ) : ( - }> - {children} - - )} + {loading || companiesLoading ? : }>{children}}

MeetMate

@@ -161,3 +151,12 @@ export default function DashboardLayout({ children }: { children: React.ReactNod ); } + +// Wrap the dashboard with the UserProvider +export default function DashboardLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 5bdb258..76668f3 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -109,7 +109,7 @@ function Dashboard() {

MeetMate Dashboard

Create your appointments in minutes

- + diff --git a/src/components/dashboard/CompanyContext.tsx b/src/components/dashboard/CompanyContext.tsx new file mode 100644 index 0000000..c8f463f --- /dev/null +++ b/src/components/dashboard/CompanyContext.tsx @@ -0,0 +1,62 @@ +import React, { createContext, useContext, useEffect, useState } from "react"; +import { type Company, type CompanyUser } from "@/types"; +import { getAccessToken, getUser } from "@/lib/authActions"; +import { type ApolloQueryResult, useQuery } from "@apollo/client"; +import { getCompany } from "@/lib/graphql/queries"; + +type CompanyContextType = { + user: CompanyUser | undefined; + loading: boolean; + refreshUser: () => Promise; + + company: { getCompany: Company } | undefined; + companyLoading: boolean; + refreshCompany: () => Promise>; +}; + +const CompanyContext = createContext(undefined); + +export function CompanyProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(); + const { + loading: companyLoading, + data: company, + refetch: refreshCompany + } = useQuery(getCompany, { variables: { id: user?.associatedCompany }, pollInterval: 300000 }); + const [loading, setLoading] = useState(true); + + const fetchUser = async () => { + setLoading(true); + try { + const accessToken = await getAccessToken(); + const userData = await getUser(accessToken); + setUser(userData as CompanyUser); + } catch (error) { + console.error("Failed to fetch user", error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + void fetchUser(); + }, []); + + const refreshUser = async () => { + await fetchUser(); + }; + + return ( + + {children} + + ); +} + +export function useCompany() { + const context = useContext(CompanyContext); + if (context === undefined) { + throw new Error("useCompany must be used within a CompanyProvider"); + } + return context; +} diff --git a/src/components/dashboard/DashboardContext.tsx b/src/components/dashboard/DashboardContext.tsx index d2fdcc1..673830b 100644 --- a/src/components/dashboard/DashboardContext.tsx +++ b/src/components/dashboard/DashboardContext.tsx @@ -1,20 +1,72 @@ -import React, { createContext, useContext } from "react"; -import type { User } from "@/types"; +import React, { createContext, useContext, useEffect, useState } from "react"; +import type { ClientUser, Company } from "@/types"; +import { getAccessToken, getUser } from "@/lib/authActions"; +import { useQuery } from "@apollo/client"; +import { GET_COMPANIES } from "@/lib/graphql/queries"; type DashboardContextProps = { - user: User | null; + user: ClientUser | undefined; + loading: boolean; + refreshUser: () => Promise; + + companies: { getCompanies: Company[] } | undefined; + companiesLoading: boolean; + refreshCompanies: () => Promise; }; const DashboardContext = createContext(undefined); -export const DashboardProvider: React.FC<{ user: User | null; children: React.ReactNode }> = ({ user, children }) => { - return {children}; -}; +export function DashboardProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(); + const [loading, setLoading] = useState(true); + + const fetchUser = async () => { + setLoading(true); + try { + const accessToken = await getAccessToken(); + const userData = await getUser(accessToken); + setUser(userData as ClientUser); + } catch (error) { + console.error("Failed to fetch user", error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + void fetchUser(); + }, []); + + const refreshUser = async () => { + await fetchUser(); + }; + + const { + loading: companiesLoading, + data: companies = { getCompanies: [] }, + refetch + } = useQuery(GET_COMPANIES, { + pollInterval: 300000, + onError: (error) => { + console.error("GraphQL Error:", error); + } + }); + + const refreshCompanies = async () => { + await refetch(); + }; + + return ( + + {children} + + ); +} export const useDashboardData = (): DashboardContextProps => { const context = useContext(DashboardContext); if (context === null) { - throw new Error("useDashboardData must be used within a UserProvider"); + throw new Error("useDashboardData must be used within a DashboardProvider"); } return context!; }; diff --git a/src/components/dashboard/FollowButton.tsx b/src/components/dashboard/FollowButton.tsx new file mode 100644 index 0000000..3c66d0b --- /dev/null +++ b/src/components/dashboard/FollowButton.tsx @@ -0,0 +1,57 @@ +import { useFormState } from "react-dom"; +import { subscribeToCompany } from "@/lib/companyActions"; +import { Button } from "@/components/ui/button"; +import { useOptimistic, useEffect, useTransition } from "react"; +import { useDashboardData } from "@/components/dashboard/DashboardContext"; +import { useRouter } from "next/navigation"; + +export default function FollowButton({ companyId }: { companyId: string }) { + const { user, refreshUser } = useDashboardData(); + const [isPending, startTransition] = useTransition(); + const router = useRouter(); + + const [state, formAction] = useFormState(subscribeToCompany, { + message: "success" + }); + + const baseSubscribed = user?.subscribedCompanies?.includes(Number(companyId)); + + const [optimisticSubscribed, setOptimisticSubscribed] = useOptimistic( + baseSubscribed, + (_, newValue: boolean) => newValue + ); + + // Update the optimistic state when the actual subscription status changes + useEffect(() => { + if (state.message === "success" && state.isSubscribed !== undefined) { + startTransition(() => { + if (state.isSubscribed === true) { + setOptimisticSubscribed(state.isSubscribed); + } + void refreshUser(); + router.push("/dashboard"); + }); + } + }, [state.message, state.isSubscribed, refreshUser]); + + const handleAction = async (formData: FormData) => { + startTransition(() => { + setOptimisticSubscribed(optimisticSubscribed === false); + }); + formAction(formData); + }; + + return ( +
+ + + {state.message === "error" &&

{state.error}

} +
+ ); +} diff --git a/src/lib/authActions.ts b/src/lib/authActions.ts index f460808..a00b4e0 100644 --- a/src/lib/authActions.ts +++ b/src/lib/authActions.ts @@ -86,7 +86,7 @@ export async function getUser(accessToken?: string): Promise { if (response.ok) { return (await response.json()) as User; - } else if (response.status === 403 || response.status === 500) { + } else if (response.status === 403 || response.status === 500 || response.status === 401) { return null; } else { throw new Error("There was a problem fetching the user: " + response.statusText + " " + response.status); diff --git a/src/lib/companyActions.ts b/src/lib/companyActions.ts index 908fe79..86b72f5 100644 --- a/src/lib/companyActions.ts +++ b/src/lib/companyActions.ts @@ -1 +1,75 @@ "use server"; + +import { z } from "zod"; +import { getAccessToken, getUser } from "@/lib/authActions"; +import { isClientUser } from "@/types/role"; + +type SubscriptionState = { + message: "success" | "error"; + error?: string; + isSubscribed?: boolean; +}; + +export async function subscribeToCompany(prevState: SubscriptionState, formData: FormData): Promise { + const companyId = formData.get("companyId") as string; + + // Validate the company ID + const schema = z.object({ + companyId: z.string().min(1, { message: "Company ID is required" }) + }); + + const parse = schema.safeParse({ companyId }); + + if (!parse.success) { + return { + message: "error", + error: parse.error.flatten().fieldErrors.companyId?.[0] ?? "Invalid company ID" + }; + } + + // Get the access token for authentication + const accessToken = await getAccessToken(); + + if (accessToken === undefined) { + return { + message: "error", + error: "Not authenticated" + }; + } + + // Function to make the subscription request + const makeSubscriptionRequest = async (token: string) => { + return fetch(`${process.env.FRONTEND_DOMAIN}/api/user/subscribe?companyId=${companyId}`, { + method: "PUT", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Authorization: "Bearer " + token + } + }); + }; + + // First attempt with current access token + const response = await makeSubscriptionRequest(accessToken); + + if (response.ok) { + // Get fresh user data + const user = await getUser(accessToken); + const isSubscribed = isClientUser(user!) ? user?.subscribedCompanies.includes(Number(companyId)) : false; + + return { + message: "success", + isSubscribed + }; + } else if (response.status === 429) { + return { + message: "error", + error: "Too many requests" + }; + } else { + console.log(await response.text()); + return { + message: "error", + error: "Failed to update subscription" + }; + } +} diff --git a/src/lib/graphql/queries.ts b/src/lib/graphql/queries.ts index b1bd918..c549a80 100644 --- a/src/lib/graphql/queries.ts +++ b/src/lib/graphql/queries.ts @@ -1,13 +1,26 @@ import { gql } from "@apollo/client"; export const getCompany = gql` - query { - getCompany { + query GetCompany($id: ID!) { + getCompany(id: $id) { id name description businessType - memberEmails + memberIds + ownerEmail + } + } +`; + +export const GET_COMPANIES = gql` + query GetCompanies { + getCompanies { + id + name + description + businessType + memberIds ownerEmail } } diff --git a/src/lib/graphql/schema.graphql b/src/lib/graphql/schema.graphql index c82baa5..88b90a8 100644 --- a/src/lib/graphql/schema.graphql +++ b/src/lib/graphql/schema.graphql @@ -1,5 +1,6 @@ type Query { - getCompany: Company + getCompany(id: ID!): Company + getCompanies: [Company] } type Mutation { @@ -24,6 +25,6 @@ type Company { name: String description: String businessType: String - memberEmails: [String] + memberIds: [ID] ownerEmail: String } \ No newline at end of file diff --git a/src/store/features/collectionSlice.ts b/src/store/features/collectionSlice.ts index ae68b2b..dc951da 100644 --- a/src/store/features/collectionSlice.ts +++ b/src/store/features/collectionSlice.ts @@ -1,9 +1,8 @@ -import { type Appointment, type Company } from "@/types"; import { createSlice, type PayloadAction } from "@reduxjs/toolkit"; +import { type Appointment } from "@/types"; type CollectionState = { appointments: Appointment[]; - companies: Company[]; }; const initialState: CollectionState = { @@ -41,8 +40,7 @@ const initialState: CollectionState = { client: null, status: "BOOKED" } - ], - companies: [] + ] }; export const collectionSlice = createSlice({ @@ -51,13 +49,10 @@ export const collectionSlice = createSlice({ reducers: { setAppointments(state, action: PayloadAction) { state.appointments = action.payload; - }, - setCompanies(state, action: PayloadAction) { - state.companies = action.payload; } } }); -export const { setAppointments, setCompanies } = collectionSlice.actions; +export const { setAppointments } = collectionSlice.actions; export default collectionSlice.reducer; diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 5f95e8d..1fec22c 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,39 +1,14 @@ -import { type ReactThreeFiber } from "@react-three/fiber"; -import { type ShaderMaterialParameters } from "three"; -import { type shaderMaterial } from "@react-three/drei"; - -declare global { - namespace JSX { - type IntrinsicElements = { - customShaderMaterial: ReactThreeFiber.Node; - }; - } -} +import type { Role } from "./role"; // ----- AUTH ----- // -// eslint-disable-next-line no-shadow -export enum Role { - CLIENT = "CLIENT", - ADMIN = "ADMIN", - COMPANY_MEMBER = "COMPANY_MEMBER", - COMPANY_ADMIN = "COMPANY_ADMIN" -} - -export type User = { - id: number; - name: string; - email: string; - role: keyof typeof Role; -}; - -type StoreTokenRequest = { +export type StoreTokenRequest = { access_token: string; refresh_token?: string; expires_at: string; }; -type LoginFormState = { +export type LoginFormState = { message: string; errors: Record | undefined; fieldValues: { email: string; password: string }; @@ -60,6 +35,43 @@ export type SignupFormState = { }; }; +// USERS // + +// Base user properties that all users share +type BaseUser = { + id: number; + name: string; + email: string; + role: Role; +}; + +// Specific user types +type ClientUser = BaseUser & { + role: Role.CLIENT; + subscribedCompanies: number[]; +}; + +type CompanyMemberUser = BaseUser & { + role: Role.COMPANY_MEMBER; + associatedCompany: string; +}; + +type CompanyAdminUser = BaseUser & { + role: Role.COMPANY_ADMIN; + associatedCompany: string; +}; + +type AdminUser = BaseUser & { + role: Role.ADMIN; +}; + +type CompanyUser = BaseUser & (CompanyMemberUser | CompanyAdminUser); + +// Union type for all possible user types +export type User = ClientUser | CompanyMemberUser | CompanyAdminUser | AdminUser; + +// ----- APPOINTMENTS ----- // + export type Appointment = { id: number; from: Date; diff --git a/src/types/role.ts b/src/types/role.ts new file mode 100644 index 0000000..17eb860 --- /dev/null +++ b/src/types/role.ts @@ -0,0 +1,21 @@ +import { type ClientUser, type CompanyAdminUser, type CompanyMemberUser, type User } from "@/types/index"; + +// eslint-disable-next-line no-shadow +export enum Role { + CLIENT = "CLIENT", + ADMIN = "ADMIN", + COMPANY_MEMBER = "COMPANY_MEMBER", + COMPANY_ADMIN = "COMPANY_ADMIN" +} + +// Type guard functions to help with type narrowing +export function isClientUser(user: User): user is ClientUser { + return user.role === Role.CLIENT; +} + +export function isCompanyUser(user: User): user is CompanyMemberUser | CompanyAdminUser { + return user.role === Role.COMPANY_MEMBER || user.role === Role.COMPANY_ADMIN; +} + +// Helper type if you need to get a specific user type +export type UserOfRole = Extract; diff --git a/tsconfig.json b/tsconfig.json index 6bba34b..41437dd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,7 @@ "paths": { "@/*": ["./src/*"] }, - "typeRoots": ["node_modules/@types"], + "typeRoots": ["node_modules/@types", "./src/types"], "types": ["three"] }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/types/*.d.ts"],