diff --git a/apps/website/app/(home)/auth/group/[groupId]/page.tsx b/apps/website/app/(home)/auth/group/[groupId]/page.tsx new file mode 100644 index 000000000..abbcd45e6 --- /dev/null +++ b/apps/website/app/(home)/auth/group/[groupId]/page.tsx @@ -0,0 +1,78 @@ +import { redirect, notFound } from "next/navigation"; +import Link from "next/link"; +import { createClient } from "~/utils/supabase/server"; +import { getSessionBaseUserData } from "~/utils/supabase/account"; +import { GroupMemberList } from "~/components/auth/GroupMemberList"; +import { GroupInvite } from "~/components/auth/GroupInvite"; +import internalError from "~/utils/internalErrorSsr"; + +const Page = async ({ + params, + searchParams, +}: { + params: Promise<{ groupId: string }>; + searchParams: Promise<{ + token?: string; + tokenError?: string; + removeError?: string; + }>; +}) => { + const { groupId } = await params; + const sp = await searchParams; + + const client = await createClient(); + const userData = await getSessionBaseUserData(client); + if (!userData) + redirect("/auth/error?error=" + encodeURIComponent("Not logged in")); + + const membershipReq = await client + .from("group_membership") + .select("admin") + .eq("group_id", groupId) + .eq("member_id", userData.id) + .maybeSingle(); + if (membershipReq.error) { + internalError({ error: membershipReq.error }); + redirect("/auth/error?error=" + encodeURIComponent("Could not load group")); + } + if (!membershipReq.data) notFound(); + const isAdmin = membershipReq.data.admin === true; + + const groupReq = await client + .from("my_groups") + .select("name") + .eq("id", groupId) + .maybeSingle(); + if (groupReq.error) { + internalError({ error: groupReq.error }); + redirect("/auth/error?error=" + encodeURIComponent("Could not load group")); + } + const groupName = groupReq.data?.name ?? groupId; + + return ( +
+
+ + Back to group page + +
+

{groupName}

+
+ + {isAdmin && ( + + )} +
+
+ ); +}; + +export default Page; diff --git a/apps/website/app/(home)/auth/group/page.tsx b/apps/website/app/(home)/auth/group/page.tsx index 7d31fa17b..0f5ff8c8e 100644 --- a/apps/website/app/(home)/auth/group/page.tsx +++ b/apps/website/app/(home)/auth/group/page.tsx @@ -1,11 +1,27 @@ import { ListGroups } from "~/components/auth/ListGroups"; +import { JoinGroup } from "~/components/auth/JoinGroup"; +import { CreateGroup } from "~/components/auth/CreateGroup"; -const Page = () => ( -
-
- -
-
-); +const Page = async ({ + searchParams, +}: { + searchParams: Promise<{ + error?: string; + joined?: string; + createError?: string; + created?: string; + }>; +}) => { + const params = await searchParams; + return ( +
+
+ + + +
+
+ ); +}; export default Page; diff --git a/apps/website/app/components/auth/CreateGroup.tsx b/apps/website/app/components/auth/CreateGroup.tsx new file mode 100644 index 000000000..c140c93be --- /dev/null +++ b/apps/website/app/components/auth/CreateGroup.tsx @@ -0,0 +1,50 @@ +import { redirect } from "next/navigation"; +import { createClient } from "~/utils/supabase/server"; +import { createGroup } from "~/utils/supabase/account"; +import { Button } from "@repo/ui/components/ui/button"; +import { Input } from "@repo/ui/components/ui/input"; + +export const CreateGroup = ({ + error, + created, +}: { + error?: string; + created?: string; +}) => { + const createGroupAction = async (formData: FormData) => { + "use server"; + const name = formData.get("name"); + if (typeof name !== "string" || !name.trim()) { + redirect( + "/auth/group?createError=" + + encodeURIComponent("Please enter a group name"), + ); + } + const client = await createClient(); + const { groupId, error: err } = await createGroup(client, name.trim()); + if (err) { + redirect("/auth/group?createError=" + encodeURIComponent(err)); + } + if (!groupId) { + redirect( + "/auth/group?createError=" + + encodeURIComponent("Failed to create group"), + ); + } + redirect("/auth/group?created=" + encodeURIComponent(groupId)); + }; + + return ( +
+

Create a group

+ {created && ( +

Group created successfully.

+ )} + {error &&

{error}

} +
+ + +
+
+ ); +}; diff --git a/apps/website/app/components/auth/GroupInvite.tsx b/apps/website/app/components/auth/GroupInvite.tsx new file mode 100644 index 000000000..65507840f --- /dev/null +++ b/apps/website/app/components/auth/GroupInvite.tsx @@ -0,0 +1,49 @@ +import { redirect } from "next/navigation"; +import { createClient } from "~/utils/supabase/server"; +import { createGroupInvitation } from "~/utils/supabase/account"; +import { Button } from "@repo/ui/components/ui/button"; + +export const GroupInvite = ({ + groupId, + token, + tokenError, +}: { + groupId: string; + token?: string; + tokenError?: string; +}) => { + const createToken = async (formData: FormData) => { + "use server"; + const admin = formData.get("admin") === "true"; + const client = await createClient(); + const t = await createGroupInvitation({ client, groupId, admin }); + if (!t) { + redirect( + `/auth/group/${groupId}?tokenError=` + + encodeURIComponent("Could not create invitation token"), + ); + } + redirect(`/auth/group/${groupId}?token=` + encodeURIComponent(t)); + }; + + return ( +
+

Invite to group

+ {tokenError &&

{tokenError}

} + {token && ( +
+

Invitation token (valid 60 days):

+ {token} +
+ )} +
+
+ + +
+
+
+ ); +}; diff --git a/apps/website/app/components/auth/GroupMemberList.tsx b/apps/website/app/components/auth/GroupMemberList.tsx new file mode 100644 index 000000000..fa4a0d0d8 --- /dev/null +++ b/apps/website/app/components/auth/GroupMemberList.tsx @@ -0,0 +1,112 @@ +import { redirect } from "next/navigation"; +import { createClient } from "~/utils/supabase/server"; +import { + removeFromGroup, + getSessionBaseUserData, +} from "~/utils/supabase/account"; +import { Button } from "@repo/ui/components/ui/button"; +import internalError from "~/utils/internalErrorSsr"; + +export const GroupMemberList = async ({ + groupId, + isAdmin, + removeError, +}: { + groupId: string; + isAdmin: boolean; + removeError?: string; +}) => { + const client = await createClient(); + const clientData = await getSessionBaseUserData(client); + const myUserId = clientData?.id; + if (!myUserId) { + internalError({ error: "Not logged in" }); + redirect( + "/auth/error?error=" + + encodeURIComponent("Not logged in.\nPlease log in from application."), + ); + } + + const pseudoAccountReq = await client + .from("my_pseudo_accounts") + .select() + .eq("group_id", groupId); + + if (pseudoAccountReq.error) { + internalError({ error: pseudoAccountReq.error }); + redirect( + "/auth/error?error=" + encodeURIComponent("Could not load group members"), + ); + } + const pseudoAccountInfo = pseudoAccountReq.data ?? []; + const numAdmins = pseudoAccountInfo + .map((pa) => (pa.admin ? 1 : 0) as number) + .reduce((acc, cur) => acc + cur, 0); + + const removeSpace = async (formData: FormData) => { + "use server"; + const memberId = formData.get("memberId"); + if (typeof memberId !== "string") return; + const c = await createClient(); + const error = await removeFromGroup({ client: c, groupId, memberId }); + if (error) { + redirect( + `/auth/group/${groupId}?removeError=` + encodeURIComponent(error), + ); + } + redirect(`/auth/group/${groupId}`); + }; + + return ( +
+

Member spaces

+ {removeError &&

{removeError}

} + {pseudoAccountInfo.length === 0 ? ( +

No spaces yet.

+ ) : ( + + )} +
+ ); +}; diff --git a/apps/website/app/components/auth/JoinGroup.tsx b/apps/website/app/components/auth/JoinGroup.tsx new file mode 100644 index 000000000..dffe92f23 --- /dev/null +++ b/apps/website/app/components/auth/JoinGroup.tsx @@ -0,0 +1,47 @@ +import { redirect } from "next/navigation"; +import { createClient } from "~/utils/supabase/server"; +import { acceptGroupInvitation } from "~/utils/supabase/account"; +import { Button } from "@repo/ui/components/ui/button"; +import { Input } from "@repo/ui/components/ui/input"; + +export const JoinGroup = ({ + error, + joined, +}: { + error?: string; + joined?: boolean; +}) => { + const joinGroup = async (formData: FormData) => { + "use server"; + const token = formData.get("token"); + if (typeof token !== "string" || !token.trim()) { + redirect( + "/auth/group?error=" + encodeURIComponent("Please enter a token"), + ); + } + const client = await createClient(); + const err = await acceptGroupInvitation(client, token.trim()); + if (err) { + redirect("/auth/group?error=" + encodeURIComponent(err)); + } + redirect("/auth/group?joined=1"); + }; + + return ( +
+

Join a group

+ {joined && ( +

Successfully joined the group.

+ )} + {error &&

{error}

} +
+ + +
+
+ ); +}; diff --git a/packages/ui/src/components/ui/input.tsx b/packages/ui/src/components/ui/input.tsx new file mode 100644 index 000000000..d4a9306ee --- /dev/null +++ b/packages/ui/src/components/ui/input.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; + +import { cn } from "@repo/ui/lib/utils"; + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ); + }, +); +Input.displayName = "Input"; + +export { Input };