Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions app/bounty/create/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"use client";

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 { isPending } = authClient.useSession();
const userRole = useUserRole();

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;
}

return (
<div className="container max-w-2xl py-8">
<h1 className="text-3xl font-bold mb-8">Create a Bounty</h1>
<Card className="border-amber-200 bg-amber-50">
<CardHeader>
<div className="flex items-center gap-2">
<AlertCircle className="h-5 w-5 text-amber-600" />
<CardTitle className="text-amber-900">Coming Soon</CardTitle>
</div>
<CardDescription className="text-amber-800">
The bounty creation form is under development
</CardDescription>
</CardHeader>
<CardContent className="text-sm text-amber-900">
We're building a powerful form to help you create bounties. Check back

Check failure on line 47 in app/bounty/create/page.tsx

View workflow job for this annotation

GitHub Actions / build-and-lint (24.x)

`'` can be escaped with `&apos;`, `&lsquo;`, `&#39;`, `&rsquo;`
soon!
</CardContent>
</Card>
</div>
);
}
14 changes: 14 additions & 0 deletions components/global-navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { useUserRole } from "@/hooks/use-user-role";

import { Wallet, LogIn, Fingerprint } from "lucide-react";

export function GlobalNavbar() {
const pathname = usePathname();
const userRole = useUserRole();
const { walletInfo, isConnected, isRegistered, connect, isLoading } =
useSmartWallet();

Expand Down Expand Up @@ -112,6 +114,18 @@ export function GlobalNavbar() {
>
Wallet
</Link>
{userRole === "sponsor" && (
<Link
href="/bounty/create"
className={`transition-colors hover:text-foreground/80 ${
pathname.startsWith("/bounty/create")
? "text-foreground"
: "text-foreground/60"
}`}
>
Create
</Link>
)}
<Link
href="/bounty/review"
className={`transition-colors hover:text-foreground/80 ${
Expand Down
66 changes: 62 additions & 4 deletions components/settings/profile-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useState } from "react";
import { Form } from "@/components/ui/form";
import { FormFieldWrapper } from "@/components/ui/form-field-wrapper";
import { Input } from "@/components/ui/input";
Expand All @@ -12,6 +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 { useUserRole } from "@/hooks/use-user-role";

const profileSchema = z.object({
name: z
Expand Down Expand Up @@ -52,10 +54,9 @@ const profileSchema = z.object({

type ProfileFormValues = z.infer<typeof profileSchema>;

type SessionCache = { user?: Partial<ProfileFormValues> } & Record<
string,
unknown
>;
type SessionCache = {
user?: Partial<ProfileFormValues & { role?: string }>;
} & Record<string, unknown>;

interface ProfileTabProps {
defaultValues: ProfileFormValues;
Expand All @@ -64,6 +65,8 @@ interface ProfileTabProps {
export function ProfileTab({ defaultValues }: ProfileTabProps) {
const queryClient = useQueryClient();
const { mutateAsync, isPending } = useUpdateUserMutation();
const currentRole = useUserRole();
const [isTogglingRole, setIsTogglingRole] = useState(false);

const form = useForm<ProfileFormValues>({
resolver: zodResolver(profileSchema),
Expand Down Expand Up @@ -95,6 +98,29 @@ export function ProfileTab({ defaultValues }: ProfileTabProps) {
}
};

const handleRoleToggle = async () => {
setIsTogglingRole(true);
const newRole = currentRole === "sponsor" ? "contributor" : "sponsor";

const previous = queryClient.getQueryData<SessionCache>(authKeys.session());

queryClient.setQueryData<SessionCache>(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 (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
Expand Down Expand Up @@ -178,6 +204,38 @@ export function ProfileTab({ defaultValues }: ProfileTabProps) {
/>
</div>

<div className="space-y-4 pt-4 border-t">
<h3 className="text-sm font-medium">Account Settings</h3>
<div className="flex items-center justify-between p-3 rounded-lg border bg-muted/50">
<div className="flex flex-col gap-1">
<p className="text-sm font-medium">Sponsor Access</p>
<p className="text-xs text-muted-foreground">
{currentRole === "sponsor"
? "You have sponsor privileges. Click to switch to contributor."
: "You are a contributor. Click to switch to sponsor."}
</p>
</div>
<Button
type="button"
variant={currentRole === "sponsor" ? "default" : "outline"}
size="sm"
onClick={handleRoleToggle}
disabled={isTogglingRole}
>
{isTogglingRole ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Updating...
</>
) : currentRole === "sponsor" ? (
"Switch to Contributor"
) : (
"Switch to Sponsor"
)}
</Button>
</div>
</div>

{form.formState.errors.root && (
<p className="text-sm text-destructive">
{form.formState.errors.root.message}
Expand Down
1 change: 1 addition & 0 deletions hooks/use-user-mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface UpdateUserParams {
github?: string;
twitter?: string;
website?: string;
role?: "sponsor" | "contributor";
}

export async function updateUser(params: UpdateUserParams) {
Expand Down
13 changes: 13 additions & 0 deletions hooks/use-user-role.ts
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions lib/auth-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
},
}),
],
Expand Down
5 changes: 5 additions & 0 deletions lib/server-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface User {
github?: string;
twitter?: string;
website?: string;
role?: "sponsor" | "contributor";
}

interface SessionUser {
Expand Down Expand Up @@ -105,6 +106,7 @@ export async function getCurrentUser(): Promise<User | null> {
github?: string | null;
twitter?: string | null;
website?: string | null;
role?: string | null;
};

const id = u.id;
Expand All @@ -119,6 +121,9 @@ export async function getCurrentUser(): Promise<User | null> {
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);
Expand Down
Loading