edit team profile

This commit is contained in:
Kalle (Sendou) 2021-01-14 23:27:31 +02:00
parent 135a0da9f8
commit 6a0efdd025
4 changed files with 264 additions and 10 deletions

View File

@ -0,0 +1,160 @@
import {
Box,
Button,
FormControl,
FormErrorMessage,
FormLabel,
Input,
InputGroup,
InputLeftAddon,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
useToast,
} from "@chakra-ui/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { t, Trans } from "@lingui/macro";
import { useLingui } from "@lingui/react";
import MarkdownTextarea from "components/common/MarkdownTextarea";
import { getToastOptions } from "lib/getToastOptions";
import { sendData } from "lib/postData";
import {
teamSchema,
TEAM_BIO_CHARACTER_LIMIT,
TEAM_RECRUITING_POST_CHARACTER_LIMIT,
} from "lib/validators/team";
import { GetTeamData } from "prisma/queries/getTeam";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { FaTwitter } from "react-icons/fa";
import { FiEdit } from "react-icons/fi";
import { mutate } from "swr";
import * as z from "zod";
interface Props {
team: NonNullable<GetTeamData>;
}
type FormData = z.infer<typeof teamSchema>;
const TeamProfileModal: React.FC<Props> = ({ team }) => {
const { i18n } = useLingui();
const toast = useToast();
const [isOpen, setIsOpen] = useState(false);
const [sending, setSending] = useState(false);
const { handleSubmit, errors, register, watch } = useForm<FormData>({
resolver: zodResolver(teamSchema),
defaultValues: team,
});
const watchBio = watch("bio", team.bio);
const watchRecruitingPost = watch("recruitingPost", team.recruitingPost);
const onSubmit = async (formData: FormData) => {
setSending(true);
const success = await sendData("PUT", "/api/teams", formData);
setSending(false);
if (!success) return;
mutate(`/api/teams/${team.id}`);
toast(getToastOptions(t`Team profile updated`, "success"));
setIsOpen(false);
};
return (
<>
<Button leftIcon={<FiEdit />} onClick={() => setIsOpen(true)}>
<Trans>Edit team profile</Trans>
</Button>
{isOpen && (
<Modal
isOpen
onClose={() => setIsOpen(false)}
size="xl"
closeOnOverlayClick={false}
>
<ModalOverlay>
<ModalContent>
<ModalHeader>
<Trans>Editing team profile</Trans>
</ModalHeader>
<ModalCloseButton borderRadius="50%" />
<form onSubmit={handleSubmit(onSubmit)}>
<ModalBody pb={6}>
<FormControl isInvalid={!!errors.twitterName}>
<FormLabel htmlFor="twitterName">
<Box
as={FaTwitter}
display="inline-block"
mr={2}
mb={1}
color="#1DA1F2"
/>{" "}
<Trans>Twitter name</Trans>
</FormLabel>
<InputGroup>
<InputLeftAddon children="https://twitter.com/" />
<Input
name="twitterName"
ref={register}
placeholder="olivesplatoon"
/>
</InputGroup>
<FormErrorMessage>
{errors.twitterName?.message}
</FormErrorMessage>
</FormControl>
<MarkdownTextarea
fieldName="bio"
title={i18n._(t`Bio`)}
error={errors.bio}
register={register}
value={watchBio ?? ""}
maxLength={TEAM_BIO_CHARACTER_LIMIT}
placeholder={i18n._(
t`# I'm a header
I'm **bolded**. Embedding weapon images is easy too: :luna_blaster:`
)}
/>
<MarkdownTextarea
fieldName="recruitingPost"
title={i18n._(t`Recruiting post`)}
error={errors.recruitingPost}
register={register}
value={watchRecruitingPost ?? ""}
maxLength={TEAM_RECRUITING_POST_CHARACTER_LIMIT}
placeholder={i18n._(
t`# I'm a header
I'm **bolded**. Embedding weapon images is easy too: :luna_blaster:`
)}
/>
</ModalBody>
<ModalFooter>
<Button mr={3} type="submit" isLoading={sending}>
<Trans>Save</Trans>
</Button>
<Button onClick={() => setIsOpen(false)} variant="outline">
<Trans>Cancel</Trans>
</Button>
</ModalFooter>
</form>
</ModalContent>
</ModalOverlay>
</Modal>
)}
</>
);
};
export default TeamProfileModal;

View File

@ -1,7 +1,7 @@
import * as z from "zod";
const TEAM_BIO_CHARACTER_LIMIT = 7000;
const TEAM_RECRUITING_POST_CHARACTER_LIMIT = 2000;
export const TEAM_BIO_CHARACTER_LIMIT = 7000;
export const TEAM_RECRUITING_POST_CHARACTER_LIMIT = 2000;
export const teamSchema = z.object({
twitterName: z.string().max(15).optional().nullable(),

View File

@ -1,5 +1,6 @@
import { getMySession } from "lib/getMySession";
import { makeNameUrlFriendly } from "lib/makeNameUrlFriendly";
import { teamSchema } from "lib/validators/team";
import { NextApiRequest, NextApiResponse } from "next";
import prisma from "prisma/client";
import { v4 as uuidv4 } from "uuid";
@ -12,6 +13,9 @@ const teamsHandler = async (req: NextApiRequest, res: NextApiResponse) => {
case "DELETE":
await deleteHandler(req, res);
break;
case "PUT":
await putHandler(req, res);
break;
default:
res.status(405).end();
}
@ -77,4 +81,22 @@ async function deleteHandler(req: NextApiRequest, res: NextApiResponse) {
res.status(200).end();
}
async function putHandler(req: NextApiRequest, res: NextApiResponse) {
const parsed = teamSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).end();
}
const user = await getMySession(req);
if (!user) return res.status(401).end();
const team = await prisma.team.findUnique({ where: { captainId: user.id } });
if (!team) return res.status(400).end();
await prisma.team.update({ where: { id: team.id }, data: parsed.data });
res.status(200).end();
}
export default teamsHandler;

View File

@ -1,8 +1,23 @@
import { Box, Button, Heading, useToast } from "@chakra-ui/react";
import {
Box,
Button,
Divider,
Flex,
Heading,
useToast,
Wrap,
WrapItem,
} from "@chakra-ui/react";
import { t, Trans } from "@lingui/macro";
import Markdown from "components/common/Markdown";
import MyContainer from "components/common/MyContainer";
import Section from "components/common/Section";
import SubTextCollapse from "components/common/SubTextCollapse";
import UserAvatar from "components/common/UserAvatar";
import WeaponImage from "components/common/WeaponImage";
import TeamManagementModal from "components/t/TeamManagementModal";
import { getEmojiFlag } from "countries-list";
import TeamProfileModal from "components/t/TeamProfileModal";
import { countries, getEmojiFlag } from "countries-list";
import { getToastOptions } from "lib/getToastOptions";
import { sendData } from "lib/postData";
import useUser from "lib/useUser";
@ -47,7 +62,7 @@ const TeamPage: React.FC<Props> = (props) => {
<Heading fontFamily="'Rubik', sans-serif" textAlign="center">
{team.name}
</Heading>
<Box textAlign="center">
{/* <Box textAlign="center">
{team.roster
.reduce((acc: [string, number][], cur) => {
if (!cur.profile?.country) return acc;
@ -62,12 +77,17 @@ const TeamPage: React.FC<Props> = (props) => {
}, [])
.sort((a, b) => b[1] - a[1])
.map(([country]) => getEmojiFlag(country))}
</Box>
</Box> */}
{user?.id === team.captainId && (
<TeamManagementModal
roster={team.roster.filter((teamMember) => teamMember.id !== user.id)}
teamId={team.id}
/>
<>
<TeamManagementModal
roster={team.roster.filter(
(teamMember) => teamMember.id !== user.id
)}
teamId={team.id}
/>
<TeamProfileModal team={team} />
</>
)}
{user &&
user.id !== team.captainId &&
@ -82,6 +102,58 @@ const TeamPage: React.FC<Props> = (props) => {
<Trans>Leave team</Trans>
</Button>
)}
<Divider my={8} />
{team.bio && <Markdown value={team.bio} smallHeaders />}
{team.recruitingPost && (
<SubTextCollapse title={t`Recruiting post`} mt={4}>
<Markdown value={team.recruitingPost} smallHeaders />
</SubTextCollapse>
)}
{(team.bio || team.recruitingPost) && <Divider my={8} />}
<Wrap justify="center">
{team.roster
.sort(
(a, b) =>
Number(a.id === team.captainId) - Number(b.id === team.captainId)
)
.map((user) => (
<WrapItem key={user.id}>
<Section textAlign="center">
<UserAvatar user={user} />
<Box
my={2}
fontWeight="bold"
>{`${user.username}#${user.discriminator}`}</Box>
{user.profile?.country && (
<Box mx="auto" my={1}>
<Box as="span" mr={1}>
{getEmojiFlag(user.profile.country)}{" "}
</Box>
{
Object.entries(countries).find(
([key]) => key === user.profile!.country
)![1].name
}
</Box>
)}
{user.profile?.weaponPool.length && (
<Flex
mt={2}
w="100%"
alignItems="center"
justifyContent="center"
>
{user.profile.weaponPool.map((wpn) => (
<Box mx="0.2em" key={wpn}>
<WeaponImage name={wpn} size={32} />
</Box>
))}
</Flex>
)}
</Section>
</WrapItem>
))}
</Wrap>
</MyContainer>
);
};