Skip to content

Commit 21bbbad

Browse files
authored
fix(groups): fix bugs (#9)
* fix(groups): delete group * fix(groups): redirect to group after creation * fix(groups): add members from group view * fix(groups): invite case-insensitive * feat(groups): create group dialog * feat(groups): use mutations in creategroup * fix(groups): leave group
1 parent 37f2092 commit 21bbbad

16 files changed

+685
-514
lines changed

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"Bash(git add:*)",
1414
"Bash(npm run lint:*)",
1515
"Bash(mv:*)",
16-
"mcp__ide__getDiagnostics"
16+
"mcp__ide__getDiagnostics",
17+
"Bash(git checkout:*)"
1718
],
1819
"deny": []
1920
}

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ src/
109109
- **Function Style**: Always use function declarations (`function name() {}`) instead of arrow function expressions (`const name = () => {}`) for components and named functions
110110
- **Dialog Components**: Always include both `DialogTitle` AND `DialogDescription` in `DialogHeader` to prevent accessibility warnings
111111
- **React Router**: Use future flags `v7_startTransition` and `v7_relativeSplatPath` in BrowserRouter to prepare for v7 upgrade
112+
- **Component Extraction**: When a section of JSX + logic becomes substantial (>30 lines) or reusable, extract it into a separate component. Place in appropriate directory: page-specific components in `components/PageName/`, reusable ones in `components/`
112113

113114
### Important Notes
114115

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { useForm } from "react-hook-form";
2+
import {
3+
Card,
4+
CardContent,
5+
CardDescription,
6+
CardHeader,
7+
CardTitle,
8+
} from "@/components/ui/card";
9+
import { Button } from "@/components/ui/button";
10+
import { Input } from "@/components/ui/input";
11+
import { Label } from "@/components/ui/label";
12+
import { UserPlus } from "lucide-react";
13+
import { useInviteToGroupMutation } from "@/hooks/queries/useGroupsQuery";
14+
15+
interface AddMemberFormProps {
16+
groupId: string;
17+
}
18+
19+
interface FormData {
20+
usernameOrEmail: string;
21+
}
22+
23+
export function AddMemberForm({ groupId }: AddMemberFormProps) {
24+
const inviteToGroupMutation = useInviteToGroupMutation(groupId);
25+
26+
// Form for inviting members
27+
const {
28+
register,
29+
handleSubmit,
30+
reset,
31+
formState: { errors, isSubmitting },
32+
} = useForm<FormData>();
33+
34+
async function onSubmitInvite(data: FormData) {
35+
const input = data.usernameOrEmail.trim();
36+
// For email format, make it lowercase for case-insensitive matching
37+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
38+
const processedInput = emailRegex.test(input) ? input.toLowerCase() : input;
39+
40+
inviteToGroupMutation.mutate(processedInput, {
41+
onSuccess() {
42+
reset(); // Clear the form on success
43+
},
44+
});
45+
}
46+
47+
return (
48+
<Card className="bg-white/10 border-purple-400/30">
49+
<CardHeader>
50+
<CardTitle className="flex items-center space-x-2 text-white">
51+
<UserPlus className="h-5 w-5" />
52+
<span>Add Member</span>
53+
</CardTitle>
54+
<CardDescription className="text-purple-200">
55+
Invite someone to join this group by username or email
56+
</CardDescription>
57+
</CardHeader>
58+
<CardContent>
59+
<form onSubmit={handleSubmit(onSubmitInvite)} className="space-y-4">
60+
<div>
61+
<Label htmlFor="usernameOrEmail" className="text-white">
62+
Username or Email
63+
</Label>
64+
<Input
65+
id="usernameOrEmail"
66+
placeholder="Enter username or email address"
67+
className="bg-white/10 border-purple-400/30 text-white placeholder:text-purple-300"
68+
{...register("usernameOrEmail", {
69+
required: "Username or email is required",
70+
minLength: {
71+
value: 1,
72+
message: "Username or email cannot be empty",
73+
},
74+
})}
75+
/>
76+
{errors.usernameOrEmail && (
77+
<p className="text-red-400 text-sm mt-1">
78+
{errors.usernameOrEmail.message}
79+
</p>
80+
)}
81+
</div>
82+
<Button
83+
type="submit"
84+
disabled={isSubmitting || inviteToGroupMutation.isPending}
85+
className="w-full bg-purple-600 hover:bg-purple-700"
86+
>
87+
<UserPlus className="h-4 w-4 mr-2" />
88+
{isSubmitting || inviteToGroupMutation.isPending
89+
? "Adding Member..."
90+
: "Add Member"}
91+
</Button>
92+
</form>
93+
</CardContent>
94+
</Card>
95+
);
96+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { useForm } from "react-hook-form";
2+
// Removed unused useState import
3+
import {
4+
Dialog,
5+
DialogContent,
6+
DialogDescription,
7+
DialogHeader,
8+
DialogTitle,
9+
} from "@/components/ui/dialog";
10+
import { useCreateGroupMutation } from "@/hooks/queries/useGroupsQuery";
11+
import { useAuth } from "@/contexts/AuthContext";
12+
import { Button } from "@/components/ui/button";
13+
import { Input } from "@/components/ui/input";
14+
import { Label } from "@/components/ui/label";
15+
import { Textarea } from "@/components/ui/textarea";
16+
import { Users } from "lucide-react";
17+
18+
interface CreateGroupDialogProps {
19+
isOpen: boolean;
20+
onOpenChange: (open: boolean) => void;
21+
onGroupCreated: (groupId: string) => void;
22+
}
23+
24+
interface FormData {
25+
name: string;
26+
description: string;
27+
}
28+
export function CreateGroupDialog({
29+
isOpen,
30+
onOpenChange,
31+
onGroupCreated,
32+
}: CreateGroupDialogProps) {
33+
const { user } = useAuth();
34+
const createGroupMutation = useCreateGroupMutation();
35+
// No local creating state needed; use createGroupMutation.isLoading
36+
const {
37+
register,
38+
handleSubmit,
39+
reset,
40+
formState: { errors, isSubmitting },
41+
} = useForm<FormData>();
42+
43+
function onSubmit(data: FormData) {
44+
if (!user?.id) return;
45+
createGroupMutation.mutate(
46+
{
47+
name: data.name.trim(),
48+
description: data.description.trim() || undefined,
49+
userId: user.id,
50+
},
51+
{
52+
onSuccess: (group) => {
53+
if (group && group.id) {
54+
reset();
55+
onOpenChange(false);
56+
onGroupCreated(group.id);
57+
}
58+
},
59+
},
60+
);
61+
}
62+
63+
function handleOpenChange(open: boolean) {
64+
if (!open) {
65+
reset(); // Clear form when closing
66+
}
67+
onOpenChange(open);
68+
}
69+
70+
return (
71+
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
72+
<DialogContent className="sm:max-w-md">
73+
<DialogHeader>
74+
<DialogTitle className="flex items-center space-x-2">
75+
<Users className="h-5 w-5" />
76+
<span>Create New Group</span>
77+
</DialogTitle>
78+
<DialogDescription>
79+
Create a group to share and compare votes with friends
80+
</DialogDescription>
81+
</DialogHeader>
82+
83+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
84+
<div>
85+
<Label htmlFor="name">Group Name</Label>
86+
<Input
87+
id="name"
88+
placeholder="e.g., Festival Squad, Close Friends, Work Colleagues"
89+
{...register("name", {
90+
required: "Group name is required",
91+
minLength: {
92+
value: 1,
93+
message: "Group name cannot be empty",
94+
},
95+
})}
96+
/>
97+
{errors.name && (
98+
<p className="text-red-400 text-sm mt-1">{errors.name.message}</p>
99+
)}
100+
</div>
101+
102+
<div>
103+
<Label htmlFor="description">Description (Optional)</Label>
104+
<Textarea
105+
id="description"
106+
placeholder="What's this group for?"
107+
rows={3}
108+
{...register("description")}
109+
/>
110+
</div>
111+
112+
<div className="flex justify-end space-x-2 pt-4">
113+
<Button
114+
type="button"
115+
variant="outline"
116+
onClick={() => handleOpenChange(false)}
117+
disabled={isSubmitting || createGroupMutation.isPending}
118+
>
119+
Cancel
120+
</Button>
121+
<Button
122+
type="submit"
123+
disabled={isSubmitting || createGroupMutation.isPending}
124+
>
125+
{isSubmitting || createGroupMutation.isPending
126+
? "Creating..."
127+
: "Create Group"}
128+
</Button>
129+
</div>
130+
</form>
131+
</DialogContent>
132+
</Dialog>
133+
);
134+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { Button } from "@/components/ui/button";
2+
import {
3+
Card,
4+
CardHeader,
5+
CardTitle,
6+
CardDescription,
7+
CardContent,
8+
} from "@/components/ui/card";
9+
import { Badge } from "@/components/ui/badge";
10+
import { Users, Trash2, Crown } from "lucide-react";
11+
import { Link } from "react-router-dom";
12+
import { useLeaveGroupMutation } from "@/hooks/queries/useGroupsQuery";
13+
import { Group } from "@/types/groups";
14+
import { useAuth } from "@/contexts/AuthContext";
15+
16+
export function GroupCard({
17+
group,
18+
onDelete,
19+
}: {
20+
group: Group;
21+
onDelete: () => void;
22+
}) {
23+
const { user } = useAuth();
24+
const leaveGroupMutation = useLeaveGroupMutation();
25+
26+
return (
27+
<Link to={`/groups/${group.id}`} className="block">
28+
<Card className="bg-white/10 border-purple-400/30">
29+
<CardHeader className="pb-3">
30+
<div className="flex items-start justify-between">
31+
<div>
32+
<CardTitle className="flex items-center space-x-2 text-white">
33+
<span>{group.name}</span>
34+
{group.is_creator && (
35+
<Badge
36+
variant="secondary"
37+
className="text-xs bg-purple-600/50 text-purple-100"
38+
>
39+
<Crown className="h-3 w-3 mr-1" />
40+
Creator
41+
</Badge>
42+
)}
43+
</CardTitle>
44+
{group.description && (
45+
<CardDescription className="mt-1 text-purple-200">
46+
{group.description}
47+
</CardDescription>
48+
)}
49+
</div>
50+
<div className="flex space-x-2">
51+
{group.is_creator ? (
52+
<Button
53+
variant="destructive"
54+
size="sm"
55+
onClick={(e) => {
56+
e.preventDefault();
57+
e.stopPropagation();
58+
onDelete();
59+
}}
60+
>
61+
<Trash2 className="h-4 w-4" />
62+
</Button>
63+
) : (
64+
<Button
65+
variant="outline"
66+
size="sm"
67+
onClick={(e) => {
68+
e.preventDefault();
69+
e.stopPropagation();
70+
handleLeaveGroup();
71+
}}
72+
className="bg-white/10 border-purple-400/30 text-white hover:bg-white/20"
73+
>
74+
Leave
75+
</Button>
76+
)}
77+
</div>
78+
</div>
79+
</CardHeader>
80+
<CardContent>
81+
<div className="flex items-center justify-between">
82+
<div className="flex items-center space-x-2 text-sm text-purple-200">
83+
<Users className="h-4 w-4" />
84+
<span>{group.member_count} members</span>
85+
</div>
86+
</div>
87+
</CardContent>
88+
</Card>
89+
</Link>
90+
);
91+
92+
async function handleLeaveGroup() {
93+
if (window.confirm("Are you sure you want to leave this group?")) {
94+
leaveGroupMutation.mutate({ groupId: group.id, userId: user?.id || "" });
95+
}
96+
}
97+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { AppHeader } from "@/components/AppHeader";
2+
import { Button } from "@/components/ui/button";
3+
import { Plus } from "lucide-react";
4+
5+
export function GroupsHeader({ onCreate }: { onCreate: () => void }) {
6+
return (
7+
<div>
8+
<AppHeader
9+
showBackButton
10+
backTo="/"
11+
backLabel="Back to Artists"
12+
title="My Groups"
13+
subtitle="Create and manage your festival groups"
14+
/>
15+
<div className="flex justify-between items-center mb-6">
16+
<div />
17+
<Button
18+
onClick={onCreate}
19+
className="bg-purple-600 hover:bg-purple-700"
20+
>
21+
<Plus className="h-4 w-4 mr-2" />
22+
Create Group
23+
</Button>
24+
</div>
25+
</div>
26+
);
27+
}

0 commit comments

Comments
 (0)