feat: team page

This commit is contained in:
2024-11-14 17:58:19 +07:00
parent 7a00992ff9
commit 71dfbd5db3
23 changed files with 790 additions and 67 deletions
@@ -0,0 +1,33 @@
import React from "react";
import MenuButton from "@/components/ui/menu-button";
import { Button } from "tamagui";
import Icons from "@/components/ui/icons";
import { teamFormModal } from "./team-form";
type Props = {
team: any;
};
export default function HeaderActions({ team }: Props) {
return (
<MenuButton
placement="bottom-end"
width={200}
trigger={
<Button
circular
bg="$colorTransparent"
mr="$2"
icon={<Icons name="dots-vertical" size={20} />}
/>
}
>
<MenuButton.Item
icon={<Icons name="pencil" size={16} />}
onPress={() => teamFormModal.onOpen(team)}
>
Update Team
</MenuButton.Item>
</MenuButton>
);
}
@@ -0,0 +1,84 @@
import Icons from "@/components/ui/icons";
import Modal from "@/components/ui/modal";
import { useZForm } from "@/hooks/useZForm";
import { createDisclosure } from "@/lib/utils";
import React from "react";
import { ScrollView, XStack } from "tamagui";
import { InputField } from "@/components/ui/input";
import FormField from "@/components/ui/form";
import { useInviteMutation } from "../hooks/query";
import { ErrorAlert } from "@/components/ui/alert";
import Button from "@/components/ui/button";
import {
InviteSchema,
inviteSchema,
teamMemberRoles,
} from "../schema/team-form";
import { SelectField } from "@/components/ui/select";
export const inviteFormModal = createDisclosure<InviteSchema>();
const InviteForm = () => {
const { data } = inviteFormModal.use();
const form = useZForm(inviteSchema, data);
const invite = useInviteMutation(data?.teamId || "");
const onSubmit = form.handleSubmit((values) => {
invite.mutate(values, {
onSuccess: () => {
inviteFormModal.onClose();
form.reset();
},
});
});
return (
<Modal
disclosure={inviteFormModal}
title="Invite"
description="Add new team member."
maxHeight={360}
>
<ErrorAlert mx="$4" mb="$4" error={invite.error} />
<ScrollView contentContainerStyle={{ padding: "$4", pt: 0, gap: "$4" }}>
<FormField label="Username/Email">
<InputField
f={1}
form={form}
name="username"
placeholder="john.doe"
/>
</FormField>
<FormField label="Role">
<SelectField
items={teamMemberRoles}
form={form}
name="role"
placeholder="Select Role..."
/>
</FormField>
</ScrollView>
<XStack p="$4" gap="$4">
<Button
flex={1}
onPress={inviteFormModal.onClose}
bg="$colorTransparent"
>
Cancel
</Button>
<Button
flex={1}
icon={<Icons name="account-plus" size={18} />}
onPress={onSubmit}
isLoading={invite.isPending}
>
Invite User
</Button>
</XStack>
</Modal>
);
};
export default InviteForm;
@@ -0,0 +1,89 @@
import React, { useMemo, useState } from "react";
import { Avatar, Button, ListItem, View, YGroup } from "tamagui";
import MenuButton from "@/components/ui/menu-button";
import Icons from "@/components/ui/icons";
import SearchInput from "@/components/ui/search-input";
type Props = {
members?: any[];
allowWrite?: boolean;
};
const MemberList = ({ members, allowWrite }: Props) => {
const [search, setSearch] = useState("");
const memberList = useMemo(() => {
let items = members || [];
if (search) {
items = items.filter((item: any) => {
const q = search.toLowerCase();
return (
item.user?.name.toLowerCase().includes(q) ||
item.user?.username.toLowerCase().includes(q) ||
item.user?.email.toLowerCase().includes(q)
);
});
}
return items;
}, [members, search]);
return (
<View mt="$4">
<SearchInput
placeholder="Search member.."
value={search}
onChangeText={setSearch}
/>
<YGroup bordered mt="$4">
{memberList?.map((member: any) => (
<YGroup.Item key={member.userId}>
<ListItem
hoverTheme
title={member.user?.name}
subTitle={member.role}
pr="$2"
icon={
<Avatar size="$3" circular>
<Avatar.Fallback bg="$blue5" />
</Avatar>
}
iconAfter={
allowWrite ? <MemberActionButton member={member} /> : undefined
}
/>
</YGroup.Item>
))}
</YGroup>
</View>
);
};
type MemberActionButtonProps = {
member: any;
};
const MemberActionButton = ({ member }: MemberActionButtonProps) => (
<MenuButton
size="$1"
placement="bottom-end"
trigger={
<Button
icon={<Icons name="dots-vertical" size={20} />}
circular
bg="$colorTransparent"
/>
}
>
<MenuButton.Item icon={<Icons name="account-key" size={16} />}>
Change Role
</MenuButton.Item>
<MenuButton.Item color="$red10" icon={<Icons name="trash-can" size={16} />}>
Remove Member
</MenuButton.Item>
</MenuButton>
);
export default MemberList;
@@ -0,0 +1,73 @@
import Icons from "@/components/ui/icons";
import Modal from "@/components/ui/modal";
import { useZForm } from "@/hooks/useZForm";
import { createDisclosure } from "@/lib/utils";
import React from "react";
import { ScrollView, XStack } from "tamagui";
import { InputField } from "@/components/ui/input";
import FormField from "@/components/ui/form";
import { useSaveTeam } from "../hooks/query";
import { ErrorAlert } from "@/components/ui/alert";
import Button from "@/components/ui/button";
import { TeamFormSchema, teamFormSchema } from "../schema/team-form";
export const teamFormModal = createDisclosure<TeamFormSchema>();
const TeamForm = () => {
const { data } = teamFormModal.use();
const form = useZForm(teamFormSchema, data);
const isEditing = data?.id != null;
const saveMutation = useSaveTeam();
const onSubmit = form.handleSubmit((values) => {
saveMutation.mutate(values, {
onSuccess: () => {
teamFormModal.onClose();
form.reset();
},
});
});
return (
<Modal
disclosure={teamFormModal}
title="Team"
description={`${isEditing ? "Edit" : "Add new"} team.`}
maxHeight={320}
>
<ErrorAlert mx="$4" mb="$4" error={saveMutation.error} />
<ScrollView contentContainerStyle={{ padding: "$4", pt: 0, gap: "$4" }}>
<FormField label="Name">
<InputField
f={1}
form={form}
name="name"
placeholder="Team Name..."
/>
</FormField>
<FormField label="Icon">
<InputField form={form} name="icon" placeholder="Icon" />
</FormField>
</ScrollView>
<XStack p="$4" gap="$4">
<Button flex={1} onPress={teamFormModal.onClose} bg="$colorTransparent">
Cancel
</Button>
<Button
flex={1}
icon={<Icons name="content-save" size={18} />}
onPress={onSubmit}
isLoading={saveMutation.isPending}
>
Save
</Button>
</XStack>
</Modal>
);
};
export default TeamForm;