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}}
+
+
+
+ );
+}
+
+// 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 (
-
-
-
+
+ {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}}
@@ -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
-
+
Book now
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 (
+
+ );
+}
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"],