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
3 changes: 2 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"Bash(git add:*)",
"Bash(npm run lint:*)",
"Bash(mv:*)",
"mcp__ide__getDiagnostics"
"mcp__ide__getDiagnostics",
"Bash(git checkout:*)"
],
"deny": []
}
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ src/
- **Function Style**: Always use function declarations (`function name() {}`) instead of arrow function expressions (`const name = () => {}`) for components and named functions
- **Dialog Components**: Always include both `DialogTitle` AND `DialogDescription` in `DialogHeader` to prevent accessibility warnings
- **React Router**: Use future flags `v7_startTransition` and `v7_relativeSplatPath` in BrowserRouter to prepare for v7 upgrade
- **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/`

### Important Notes

Expand Down
96 changes: 96 additions & 0 deletions src/components/Groups/AddMemberForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { useForm } from "react-hook-form";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { UserPlus } from "lucide-react";
import { useInviteToGroupMutation } from "@/hooks/queries/useGroupsQuery";

interface AddMemberFormProps {
groupId: string;
}

interface FormData {
usernameOrEmail: string;
}

export function AddMemberForm({ groupId }: AddMemberFormProps) {
const inviteToGroupMutation = useInviteToGroupMutation(groupId);

// Form for inviting members
const {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting },
} = useForm<FormData>();

async function onSubmitInvite(data: FormData) {
const input = data.usernameOrEmail.trim();
// For email format, make it lowercase for case-insensitive matching
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const processedInput = emailRegex.test(input) ? input.toLowerCase() : input;

inviteToGroupMutation.mutate(processedInput, {
onSuccess() {
reset(); // Clear the form on success
},
});
}

return (
<Card className="bg-white/10 border-purple-400/30">
<CardHeader>
<CardTitle className="flex items-center space-x-2 text-white">
<UserPlus className="h-5 w-5" />
<span>Add Member</span>
</CardTitle>
<CardDescription className="text-purple-200">
Invite someone to join this group by username or email
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmitInvite)} className="space-y-4">
<div>
<Label htmlFor="usernameOrEmail" className="text-white">
Username or Email
</Label>
<Input
id="usernameOrEmail"
placeholder="Enter username or email address"
className="bg-white/10 border-purple-400/30 text-white placeholder:text-purple-300"
{...register("usernameOrEmail", {
required: "Username or email is required",
minLength: {
value: 1,
message: "Username or email cannot be empty",
},
})}
/>
{errors.usernameOrEmail && (
<p className="text-red-400 text-sm mt-1">
{errors.usernameOrEmail.message}
</p>
)}
</div>
<Button
type="submit"
disabled={isSubmitting || inviteToGroupMutation.isPending}
className="w-full bg-purple-600 hover:bg-purple-700"
>
<UserPlus className="h-4 w-4 mr-2" />
{isSubmitting || inviteToGroupMutation.isPending
? "Adding Member..."
: "Add Member"}
</Button>
</form>
</CardContent>
</Card>
);
}
134 changes: 134 additions & 0 deletions src/components/Groups/CreateGroupDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { useForm } from "react-hook-form";
// Removed unused useState import
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useCreateGroupMutation } from "@/hooks/queries/useGroupsQuery";
import { useAuth } from "@/contexts/AuthContext";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Users } from "lucide-react";

interface CreateGroupDialogProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
onGroupCreated: (groupId: string) => void;
}

interface FormData {
name: string;
description: string;
}
export function CreateGroupDialog({
isOpen,
onOpenChange,
onGroupCreated,
}: CreateGroupDialogProps) {
const { user } = useAuth();
const createGroupMutation = useCreateGroupMutation();
// No local creating state needed; use createGroupMutation.isLoading
const {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting },
} = useForm<FormData>();

function onSubmit(data: FormData) {
if (!user?.id) return;
createGroupMutation.mutate(
{
name: data.name.trim(),
description: data.description.trim() || undefined,
userId: user.id,
},
{
onSuccess: (group) => {
if (group && group.id) {
reset();
onOpenChange(false);
onGroupCreated(group.id);
}
},
},
);
}

function handleOpenChange(open: boolean) {
if (!open) {
reset(); // Clear form when closing
}
onOpenChange(open);
}

return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center space-x-2">
<Users className="h-5 w-5" />
<span>Create New Group</span>
</DialogTitle>
<DialogDescription>
Create a group to share and compare votes with friends
</DialogDescription>
</DialogHeader>

<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<Label htmlFor="name">Group Name</Label>
<Input
id="name"
placeholder="e.g., Festival Squad, Close Friends, Work Colleagues"
{...register("name", {
required: "Group name is required",
minLength: {
value: 1,
message: "Group name cannot be empty",
},
})}
/>
{errors.name && (
<p className="text-red-400 text-sm mt-1">{errors.name.message}</p>
)}
</div>

<div>
<Label htmlFor="description">Description (Optional)</Label>
<Textarea
id="description"
placeholder="What's this group for?"
rows={3}
{...register("description")}
/>
</div>

<div className="flex justify-end space-x-2 pt-4">
<Button
type="button"
variant="outline"
onClick={() => handleOpenChange(false)}
disabled={isSubmitting || createGroupMutation.isPending}
>
Cancel
</Button>
<Button
type="submit"
disabled={isSubmitting || createGroupMutation.isPending}
>
{isSubmitting || createGroupMutation.isPending
? "Creating..."
: "Create Group"}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}
97 changes: 97 additions & 0 deletions src/components/Groups/GroupCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Users, Trash2, Crown } from "lucide-react";
import { Link } from "react-router-dom";
import { useLeaveGroupMutation } from "@/hooks/queries/useGroupsQuery";
import { Group } from "@/types/groups";
import { useAuth } from "@/contexts/AuthContext";

export function GroupCard({
group,
onDelete,
}: {
group: Group;
onDelete: () => void;
}) {
const { user } = useAuth();
const leaveGroupMutation = useLeaveGroupMutation();

return (
<Link to={`/groups/${group.id}`} className="block">
<Card className="bg-white/10 border-purple-400/30">
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div>
<CardTitle className="flex items-center space-x-2 text-white">
<span>{group.name}</span>
{group.is_creator && (
<Badge
variant="secondary"
className="text-xs bg-purple-600/50 text-purple-100"
>
<Crown className="h-3 w-3 mr-1" />
Creator
</Badge>
)}
</CardTitle>
{group.description && (
<CardDescription className="mt-1 text-purple-200">
{group.description}
</CardDescription>
)}
</div>
<div className="flex space-x-2">
{group.is_creator ? (
<Button
variant="destructive"
size="sm"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onDelete();
}}
>
<Trash2 className="h-4 w-4" />
</Button>
) : (
<Button
variant="outline"
size="sm"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleLeaveGroup();
}}
className="bg-white/10 border-purple-400/30 text-white hover:bg-white/20"
>
Leave
</Button>
)}
</div>
</div>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2 text-sm text-purple-200">
<Users className="h-4 w-4" />
<span>{group.member_count} members</span>
</div>
</div>
</CardContent>
</Card>
</Link>
);

async function handleLeaveGroup() {
if (window.confirm("Are you sure you want to leave this group?")) {
leaveGroupMutation.mutate({ groupId: group.id, userId: user?.id || "" });
}
}
}
27 changes: 27 additions & 0 deletions src/components/Groups/GroupsHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { AppHeader } from "@/components/AppHeader";
import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";

export function GroupsHeader({ onCreate }: { onCreate: () => void }) {
return (
<div>
<AppHeader
showBackButton
backTo="/"
backLabel="Back to Artists"
title="My Groups"
subtitle="Create and manage your festival groups"
/>
<div className="flex justify-between items-center mb-6">
<div />
<Button
onClick={onCreate}
className="bg-purple-600 hover:bg-purple-700"
>
<Plus className="h-4 w-4 mr-2" />
Create Group
</Button>
</div>
</div>
);
}
Loading
Loading