mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-11 21:29:09 -05:00
edit team profile
This commit is contained in:
parent
135a0da9f8
commit
6a0efdd025
160
components/t/TeamProfileModal.tsx
Normal file
160
components/t/TeamProfileModal.tsx
Normal 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;
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user