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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ meteor build ../build --directory # Build (5-15 min)
| Method | Args | Description |
| --------------------- | --------------------------------- | ---------------------------- |
| `teams.ensurePersonalWorkspace` | — | Create personal workspace |
| `teams.create` | `{ name }` | Create a new team |
| `teams.create` | `{ name, description? }` | Create a new team |
| `teams.join` | `{ teamCode }` | Join a team via invite code |
| `teams.updateName` | `{ teamId, newName }` | Rename a team |
| `teams.delete` | `teamId` | Delete a team |
Expand Down
54 changes: 40 additions & 14 deletions imports/features/teams/TeamsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export const TeamsPage: React.FC = () => {
const selectedTeam = teams.find((t) => t._id === selectedTeamId) ?? null;

// Methods
const createTeam = useMethod<[{ name: string }], { teamId: string; code: string }>('teams.create');
const createTeam = useMethod<[{ name: string; description?: string }], { teamId: string; code: string }>('teams.create');
const joinTeam = useMethod<[{ teamCode: string }], string>('teams.join');
const updateName = useMethod<[{ teamId: string; newName: string }]>('teams.updateName');
const deleteTeam = useMethod<[string]>('teams.delete');
Expand All @@ -94,9 +94,15 @@ export const TeamsPage: React.FC = () => {
>(null);

const [formValue, setFormValue] = useState('');
const [createDescription, setCreateDescription] = useState('');
const [formError, setFormError] = useState<string | null>(null);

const closeModal = () => { setModal(null); setFormValue(''); setFormError(null); };
const closeModal = () => {
setModal(null);
setFormValue('');
setCreateDescription('');
setFormError(null);
};

const [menuOpen, setMenuOpen] = useState<string | null>(null);

Expand All @@ -114,14 +120,18 @@ export const TeamsPage: React.FC = () => {
const handleCreate = useCallback(async () => {
if (!formValue.trim()) return;
try {
const result = await createTeam.call({ name: formValue.trim() });
const result = await createTeam.call({
name: formValue.trim(),
description: createDescription.trim() || undefined,
});
setSelectedTeamId(result.teamId);
setModal({ type: 'created', code: result.code });
setFormValue('');
setCreateDescription('');
} catch (e: any) {
setFormError(e.reason || 'Failed to create team');
}
}, [formValue, createTeam, setSelectedTeamId]);
}, [formValue, createDescription, createTeam, setSelectedTeamId]);

const handleJoin = useCallback(async () => {
if (!formValue.trim()) return;
Expand Down Expand Up @@ -246,6 +256,11 @@ export const TeamsPage: React.FC = () => {
<CardTitle>
{selectedTeam.isPersonal ? 'Personal Workspace' : selectedTeam.name}
</CardTitle>
{selectedTeam.description && (
<Text variant="muted" size="sm" className="mt-1 max-w-xl">
{selectedTeam.description}
</Text>
)}
{!selectedTeam.isPersonal && (
<div className="mt-1 flex items-center gap-2">
<Badge variant="secondary" size="sm">{selectedTeam.code}</Badge>
Expand Down Expand Up @@ -369,16 +384,27 @@ export const TeamsPage: React.FC = () => {
<ModalClose />
</ModalHeader>
<ModalBody>
<Input
label="Team name"
hideLabel
placeholder="Team name"
value={formValue}
onChange={(e) => setFormValue(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
error={formError ?? undefined}
autoFocus
/>
<div className="space-y-3">
<Input
label="Team name"
hideLabel
placeholder="Team name"
value={formValue}
onChange={(e) => setFormValue(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
error={formError ?? undefined}
autoFocus
/>
<textarea
aria-label="Team description"
placeholder="Team description (optional)"
value={createDescription}
onChange={(e) => setCreateDescription(e.target.value)}
rows={4}
maxLength={500}
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 outline-none transition focus:border-primary-500 focus:ring-2 focus:ring-primary-500/30 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100"
/>
</div>
</ModalBody>
<ModalFooter>
<Button
Expand Down
3 changes: 2 additions & 1 deletion imports/features/teams/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,14 @@ if (Meteor.isServer) {
return await ensurePersonalWorkspace(this.userId);
},

async 'teams.create'(fields: { name: string }) {
async 'teams.create'(fields: { name: string; description?: string }) {
if (!this.userId) throw new Meteor.Error('not-authorized');
const result = createTeamSchema.safeParse(fields);
if (!result.success) throw new Meteor.Error('validation', result.error.issues[0]?.message ?? 'Invalid input');
const code = generateTeamCode();
const teamId = await Teams.insertAsync({
name: result.data.name,
description: result.data.description,
members: [this.userId],
admins: [this.userId],
code,
Expand Down
7 changes: 7 additions & 0 deletions imports/features/teams/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export const teamNameSchema = z
.min(1, 'Team name is required')
.max(100, 'Team name must be 100 characters or less');

export const teamDescriptionSchema = z
.string()
.trim()
.max(500, 'Team description must be 500 characters or less');

export const teamCodeSchema = z
.string()
.trim()
Expand All @@ -16,6 +21,7 @@ export const teamCodeSchema = z

export const createTeamSchema = z.object({
name: teamNameSchema,
description: teamDescriptionSchema.optional(),
});

export const joinTeamSchema = z.object({
Expand Down Expand Up @@ -55,6 +61,7 @@ export type SetTeamMemberPasswordInput = z.infer<typeof setTeamMemberPasswordSch
export interface TeamDoc {
_id?: string;
name: string;
description?: string;
members: string[];
admins: string[];
code: string;
Expand Down
Loading