Archived
Backend: - Add groups Django app with models: Group, GroupMember, GroupInvite, JoinRequest - Create serializers with business rule validation - Implement GroupViewSet with full CRUD + custom actions (members, invites, roles, leave, join requests) - Add JoinGroupViewSet for invite-based joining flow - Register app in Django config and URL routing Frontend: - Add shared types for groups to @cloud-reader/shared - Create groups API client (groupsApi) - Build GroupsListPage, GroupDetailPage (member mgmt, invites, role transfer) - Build CreateGroupPage and JoinGroupPage - Add lazy-loaded routes to App.tsx with ProtectedRoute - Add navigation links to Library header Ref: #28
104 lines
3.7 KiB
TypeScript
104 lines
3.7 KiB
TypeScript
import React, { useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { groupsApi } from "../api/groups";
|
|
import { useToast } from "../hooks/useToast";
|
|
|
|
const S = {
|
|
container: { maxWidth: 500, margin: "0 auto", padding: "24px 16px" } satisfies React.CSSProperties,
|
|
backBtn: {
|
|
background: "none", border: "none", color: "#3b82f6", fontSize: 14, cursor: "pointer",
|
|
padding: 0, marginBottom: 16, minHeight: 44, minWidth: 44,
|
|
} satisfies React.CSSProperties,
|
|
title: { fontSize: 24, fontWeight: 700, marginBottom: 24 } satisfies React.CSSProperties,
|
|
label: { display: "block", fontSize: 14, fontWeight: 500, marginBottom: 6 } satisfies React.CSSProperties,
|
|
input: {
|
|
width: "100%", padding: "10px 12px", border: "1px solid #d1d5db", borderRadius: 8,
|
|
fontSize: 14, marginBottom: 16, boxSizing: "border-box" as const,
|
|
} satisfies React.CSSProperties,
|
|
textarea: {
|
|
width: "100%", padding: "10px 12px", border: "1px solid #d1d5db", borderRadius: 8,
|
|
fontSize: 14, marginBottom: 16, minHeight: 80, resize: "vertical" as const,
|
|
boxSizing: "border-box" as const, fontFamily: "inherit",
|
|
} satisfies React.CSSProperties,
|
|
submitBtn: (disabled: boolean): React.CSSProperties => ({
|
|
width: "100%", padding: "12px", backgroundColor: disabled ? "#93c5fd" : "#3b82f6",
|
|
color: "#fff", border: "none", borderRadius: 8, fontSize: 15, fontWeight: 600,
|
|
cursor: disabled ? "not-allowed" : "pointer", minHeight: 44,
|
|
}),
|
|
errorText: { color: "#ef4444", fontSize: 13, marginBottom: 12 },
|
|
};
|
|
|
|
export function CreateGroupPage() {
|
|
const navigate = useNavigate();
|
|
const { showToast } = useToast();
|
|
|
|
const [name, setName] = useState("");
|
|
const [description, setDescription] = useState("");
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!name.trim()) {
|
|
setError("Group name is required.");
|
|
return;
|
|
}
|
|
if (name.trim().length < 2) {
|
|
setError("Group name must be at least 2 characters.");
|
|
return;
|
|
}
|
|
setSubmitting(true);
|
|
setError(null);
|
|
try {
|
|
const group = await groupsApi.createGroup({
|
|
name: name.trim(),
|
|
description: description.trim() || undefined,
|
|
});
|
|
showToast({ message: "Group created!", variant: "success" });
|
|
navigate(`/groups/${group.id}`);
|
|
} catch (err: unknown) {
|
|
const msg = err instanceof Error ? err.message : "Failed to create group";
|
|
setError(msg);
|
|
showToast({ message: msg, variant: "error" });
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div style={S.container}>
|
|
<button style={S.backBtn} onClick={() => navigate("/groups")}>
|
|
← Back to Groups
|
|
</button>
|
|
<h1 style={S.title}>Create a Group</h1>
|
|
|
|
<form onSubmit={handleSubmit}>
|
|
{error && <div style={S.errorText}>{error}</div>}
|
|
|
|
<label style={S.label} htmlFor="group-name">Group Name *</label>
|
|
<input
|
|
id="group-name"
|
|
style={S.input}
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="e.g., Sci-Fi Book Club"
|
|
maxLength={256}
|
|
autoFocus
|
|
/>
|
|
|
|
<label style={S.label} htmlFor="group-desc">Description</label>
|
|
<textarea
|
|
id="group-desc"
|
|
style={S.textarea}
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
placeholder="What's this group about? (optional)"
|
|
/>
|
|
|
|
<button type="submit" style={S.submitBtn(submitting || !name.trim())} disabled={submitting || !name.trim()}>
|
|
{submitting ? "Creating..." : "Create Group"}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
);
|
|
} |