From 2a833b648360ad03ae49894d558d4cfd7dff52ea Mon Sep 17 00:00:00 2001 From: Delightech28 Date: Fri, 29 May 2026 04:39:48 +0100 Subject: [PATCH 1/2] Add sponsor role feature - gate Create access to sponsors --- app/bounty/create/page.tsx | 35 +++++++++++++++ components/global-navbar.tsx | 18 ++++++++ components/settings/profile-tab.tsx | 70 +++++++++++++++++++++++++++-- hooks/use-user-mutations.ts | 1 + lib/auth-client.ts | 1 + lib/server-auth.ts | 5 +++ 6 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 app/bounty/create/page.tsx diff --git a/app/bounty/create/page.tsx b/app/bounty/create/page.tsx new file mode 100644 index 00000000..0df1a911 --- /dev/null +++ b/app/bounty/create/page.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { authClient } from "@/lib/auth-client"; + +export default function CreateBountyPage() { + const router = useRouter(); + const { data: session, isPending } = authClient.useSession(); + const userRole = (session?.user as { role?: string } | undefined)?.role as + | "sponsor" + | "contributor" + | undefined; + + useEffect(() => { + // Redirect to /bounty if the user is not a sponsor + if (!isPending && userRole !== "sponsor") { + router.push("/bounty"); + } + }, [userRole, isPending, router]); + + // Show nothing while checking auth or redirecting + if (isPending || userRole !== "sponsor") { + return null; + } + + // TODO: Implement the bounty creation form here + return ( +
+

Create a Bounty

+ {/* Form will be added here */} +

Coming soon...

+
+ ); +} diff --git a/components/global-navbar.tsx b/components/global-navbar.tsx index 5a0aeb6b..ab0fc1d9 100644 --- a/components/global-navbar.tsx +++ b/components/global-navbar.tsx @@ -21,11 +21,17 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; +import { authClient } from "@/lib/auth-client"; import { Wallet, LogIn, Fingerprint } from "lucide-react"; export function GlobalNavbar() { const pathname = usePathname(); + const { data: session } = authClient.useSession(); + const userRole = (session?.user as { role?: string } | undefined)?.role as + | "sponsor" + | "contributor" + | undefined; const { walletInfo, isConnected, isRegistered, connect, isLoading } = useSmartWallet(); @@ -112,6 +118,18 @@ export function GlobalNavbar() { > Wallet + {userRole === "sponsor" && ( + + Create + + )} ; -type SessionCache = { user?: Partial } & Record< - string, - unknown ->; +type SessionCache = { + user?: Partial; +} & Record; interface ProfileTabProps { defaultValues: ProfileFormValues; @@ -64,6 +65,12 @@ interface ProfileTabProps { export function ProfileTab({ defaultValues }: ProfileTabProps) { const queryClient = useQueryClient(); const { mutateAsync, isPending } = useUpdateUserMutation(); + const { data: session } = authClient.useSession(); + const currentRole = (session?.user as { role?: string } | undefined)?.role as + | "sponsor" + | "contributor" + | undefined; + const [isTogglingRole, setIsTogglingRole] = useState(false); const form = useForm({ resolver: zodResolver(profileSchema), @@ -95,6 +102,29 @@ export function ProfileTab({ defaultValues }: ProfileTabProps) { } }; + const handleRoleToggle = async () => { + setIsTogglingRole(true); + const newRole = currentRole === "sponsor" ? "contributor" : "sponsor"; + + const previous = queryClient.getQueryData(authKeys.session()); + + queryClient.setQueryData(authKeys.session(), (old) => { + if (!old || typeof old !== "object") return old; + return { + ...old, + user: { ...old.user, role: newRole }, + }; + }); + + try { + await mutateAsync({ role: newRole as "sponsor" | "contributor" }); + } catch { + queryClient.setQueryData(authKeys.session(), previous); + } finally { + setIsTogglingRole(false); + } + }; + return (
@@ -178,6 +208,38 @@ export function ProfileTab({ defaultValues }: ProfileTabProps) { /> +
+

Account Settings

+
+
+

Sponsor Access

+

+ {currentRole === "sponsor" + ? "You have sponsor privileges. Click to switch to contributor." + : "You are a contributor. Click to switch to sponsor."} +

+
+ +
+
+ {form.formState.errors.root && (

{form.formState.errors.root.message} diff --git a/hooks/use-user-mutations.ts b/hooks/use-user-mutations.ts index 41a3fe64..28e16e2a 100644 --- a/hooks/use-user-mutations.ts +++ b/hooks/use-user-mutations.ts @@ -10,6 +10,7 @@ export interface UpdateUserParams { github?: string; twitter?: string; website?: string; + role?: "sponsor" | "contributor"; } export async function updateUser(params: UpdateUserParams) { diff --git a/lib/auth-client.ts b/lib/auth-client.ts index 4607d706..bae9d7c7 100644 --- a/lib/auth-client.ts +++ b/lib/auth-client.ts @@ -18,6 +18,7 @@ export const authClient = createAuthClient({ github: { type: "string", required: false }, twitter: { type: "string", required: false }, website: { type: "string", required: false }, + role: { type: "string", required: false }, }, }), ], diff --git a/lib/server-auth.ts b/lib/server-auth.ts index a47c3282..e84c3fdc 100644 --- a/lib/server-auth.ts +++ b/lib/server-auth.ts @@ -10,6 +10,7 @@ export interface User { github?: string; twitter?: string; website?: string; + role?: "sponsor" | "contributor"; } interface SessionUser { @@ -105,6 +106,7 @@ export async function getCurrentUser(): Promise { github?: string | null; twitter?: string | null; website?: string | null; + role?: string | null; }; const id = u.id; @@ -119,6 +121,9 @@ export async function getCurrentUser(): Promise { github: u.github ?? undefined, twitter: u.twitter ?? undefined, website: u.website ?? undefined, + role: (u.role === "sponsor" || u.role === "contributor" + ? u.role + : "contributor") as "sponsor" | "contributor", }; } catch (error) { console.error("Failed to resolve current user from session:", error); From 70ee9fecca230ca0353ad1f39b5a6e40c99532ad Mon Sep 17 00:00:00 2001 From: Delightech28 Date: Fri, 29 May 2026 12:27:13 +0100 Subject: [PATCH 2/2] Improve bounty creation placeholder - use styled card instead of TODO --- app/bounty/create/page.tsx | 34 ++++++++++++++++++++++------- components/global-navbar.tsx | 8 ++----- components/settings/profile-tab.tsx | 8 ++----- hooks/use-user-role.ts | 13 +++++++++++ 4 files changed, 43 insertions(+), 20 deletions(-) create mode 100644 hooks/use-user-role.ts diff --git a/app/bounty/create/page.tsx b/app/bounty/create/page.tsx index 0df1a911..70e00631 100644 --- a/app/bounty/create/page.tsx +++ b/app/bounty/create/page.tsx @@ -3,14 +3,20 @@ import { useEffect } from "react"; import { useRouter } from "next/navigation"; import { authClient } from "@/lib/auth-client"; +import { useUserRole } from "@/hooks/use-user-role"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { AlertCircle } from "lucide-react"; export default function CreateBountyPage() { const router = useRouter(); - const { data: session, isPending } = authClient.useSession(); - const userRole = (session?.user as { role?: string } | undefined)?.role as - | "sponsor" - | "contributor" - | undefined; + const { isPending } = authClient.useSession(); + const userRole = useUserRole(); useEffect(() => { // Redirect to /bounty if the user is not a sponsor @@ -24,12 +30,24 @@ export default function CreateBountyPage() { return null; } - // TODO: Implement the bounty creation form here return (

Create a Bounty

- {/* Form will be added here */} -

Coming soon...

+ + +
+ + Coming Soon +
+ + The bounty creation form is under development + +
+ + We're building a powerful form to help you create bounties. Check back + soon! + +
); } diff --git a/components/global-navbar.tsx b/components/global-navbar.tsx index ab0fc1d9..5d819ee9 100644 --- a/components/global-navbar.tsx +++ b/components/global-navbar.tsx @@ -21,17 +21,13 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; -import { authClient } from "@/lib/auth-client"; +import { useUserRole } from "@/hooks/use-user-role"; import { Wallet, LogIn, Fingerprint } from "lucide-react"; export function GlobalNavbar() { const pathname = usePathname(); - const { data: session } = authClient.useSession(); - const userRole = (session?.user as { role?: string } | undefined)?.role as - | "sponsor" - | "contributor" - | undefined; + const userRole = useUserRole(); const { walletInfo, isConnected, isRegistered, connect, isLoading } = useSmartWallet(); diff --git a/components/settings/profile-tab.tsx b/components/settings/profile-tab.tsx index dc7426f9..fc2f65c0 100644 --- a/components/settings/profile-tab.tsx +++ b/components/settings/profile-tab.tsx @@ -13,7 +13,7 @@ import { Loader2 } from "lucide-react"; import { useUpdateUserMutation } from "@/hooks/use-user-mutations"; import { useQueryClient } from "@tanstack/react-query"; import { authKeys } from "@/lib/query/query-keys"; -import { authClient } from "@/lib/auth-client"; +import { useUserRole } from "@/hooks/use-user-role"; const profileSchema = z.object({ name: z @@ -65,11 +65,7 @@ interface ProfileTabProps { export function ProfileTab({ defaultValues }: ProfileTabProps) { const queryClient = useQueryClient(); const { mutateAsync, isPending } = useUpdateUserMutation(); - const { data: session } = authClient.useSession(); - const currentRole = (session?.user as { role?: string } | undefined)?.role as - | "sponsor" - | "contributor" - | undefined; + const currentRole = useUserRole(); const [isTogglingRole, setIsTogglingRole] = useState(false); const form = useForm({ diff --git a/hooks/use-user-role.ts b/hooks/use-user-role.ts new file mode 100644 index 00000000..0dc62d11 --- /dev/null +++ b/hooks/use-user-role.ts @@ -0,0 +1,13 @@ +import { authClient } from "@/lib/auth-client"; + +/** + * Hook to get the current user's role from the session. + * Returns "sponsor", "contributor", or undefined if not authenticated. + */ +export function useUserRole(): "sponsor" | "contributor" | undefined { + const { data: session } = authClient.useSession(); + return (session?.user as { role?: string } | undefined)?.role as + | "sponsor" + | "contributor" + | undefined; +}