219 lines
8.0 KiB
TypeScript
219 lines
8.0 KiB
TypeScript
import { json } from '@remix-run/node';
|
||
import { redirect } from '@remix-run/node';
|
||
import { auth, getSignedInUser, sessionStorage } from '../auth.server';
|
||
import { z } from 'zod';
|
||
import { useState } from 'react';
|
||
import { Form, useActionData, useLoaderData, useNavigation } from '@remix-run/react';
|
||
import { TextInput } from '../components/TextInput';
|
||
import OptionSelect from '../components/OptionSelect';
|
||
import { Button } from '../components/Button';
|
||
import { validateFormData } from '../utils/utils';
|
||
import { createTeam, NO_TEAM_MARKER } from '../data/zippo.server';
|
||
import { Alert } from '../components/Alert';
|
||
|
||
import type { ActionArgs, LoaderArgs, MetaFunction } from '@remix-run/node';
|
||
import type { Option } from '../components/OptionSelect';
|
||
|
||
export const meta: MetaFunction = () => {
|
||
return {
|
||
title: 'Create Team | 0x',
|
||
description: 'Create a team on 0x',
|
||
};
|
||
};
|
||
|
||
const optionValues = z.enum(['cex', 'dex', 'self-custody-wallet', 'fintech', 'other']);
|
||
|
||
const options: Option[] = [
|
||
{ label: 'CEX', value: 'cex' },
|
||
{ label: 'DEX', value: 'dex' },
|
||
{ label: 'Self-custody Wallet', value: 'self-custody-wallet' },
|
||
{ label: 'Fintech', value: 'fintech' },
|
||
{ label: 'Other', value: 'other' },
|
||
];
|
||
|
||
const zodTeamVerifyModel = z.object({
|
||
teamName: z
|
||
.string()
|
||
.min(1, 'Please enter a valid team name')
|
||
.regex(/^[a-zA-Z0-9].*/, 'Team name must start with a letter or number'),
|
||
teamCategory: optionValues,
|
||
teamCategoryOthers: z.string().optional(),
|
||
});
|
||
|
||
type ActionInput = z.TypeOf<typeof zodTeamVerifyModel>;
|
||
|
||
type Errors = Partial<{ general: string } & Record<keyof ActionInput, string>>;
|
||
|
||
export async function loader({ request }: LoaderArgs) {
|
||
const [user, headers] = await getSignedInUser(request);
|
||
|
||
// if the user doesn't exist, we want to redirect them to the create account page
|
||
if (!user) {
|
||
throw redirect('/create-account', { headers });
|
||
}
|
||
// if the user has a team, we don't want them to be able to create a new team
|
||
if (user.teamName !== NO_TEAM_MARKER) {
|
||
throw redirect('/apps', { headers });
|
||
}
|
||
|
||
return { email: user.email };
|
||
}
|
||
|
||
export async function action({ request }: ActionArgs) {
|
||
const [user, headers] = await getSignedInUser(request);
|
||
|
||
// if the user doesn't exist, we want to redirect them to the create account page
|
||
if (!user) {
|
||
throw redirect('/create-account', { headers });
|
||
}
|
||
// if the user has a team, we don't want them to be able to create a new team
|
||
if (user.teamName !== NO_TEAM_MARKER) {
|
||
throw redirect('/apps', { headers });
|
||
}
|
||
|
||
const formData = await request.formData();
|
||
|
||
const { body, errors } = validateFormData<ActionInput>({
|
||
formData: formData,
|
||
schema: zodTeamVerifyModel,
|
||
});
|
||
|
||
if (errors || (body.teamCategory === 'other' && !body.teamCategoryOthers)) {
|
||
return json(
|
||
{
|
||
errors: {
|
||
...(errors || {}),
|
||
teamCategoryOthers:
|
||
body.teamCategory === 'other' && !body.teamCategoryOthers
|
||
? 'Please enter a valid team category'
|
||
: undefined,
|
||
} as Errors,
|
||
values: body,
|
||
},
|
||
{ headers },
|
||
);
|
||
}
|
||
|
||
const session = await sessionStorage.getSession(request.headers.get('Cookie'));
|
||
const result = await createTeam({
|
||
userId: user.id,
|
||
teamName: body.teamName,
|
||
productType: body.teamCategory === 'other' ? (body.teamCategoryOthers as string) : body.teamCategory,
|
||
});
|
||
|
||
if (result.result === 'ERROR') {
|
||
return json(
|
||
{
|
||
errors: {
|
||
general: result.error.message,
|
||
} as Errors,
|
||
},
|
||
{ headers },
|
||
);
|
||
}
|
||
|
||
const authSession = session.get(auth.sessionKey);
|
||
|
||
authSession.teamName = result.data;
|
||
|
||
session.set(auth.sessionKey, authSession);
|
||
|
||
headers.append('Set-Cookie', await sessionStorage.commitSession(session));
|
||
|
||
throw redirect('/apps', { headers });
|
||
}
|
||
|
||
function StandardCopy() {
|
||
return (
|
||
<p className="text-grey-400">
|
||
You’re creating a team on 0x. You will need to share this account with your teammates pending when
|
||
invitations are enabled.
|
||
</p>
|
||
);
|
||
}
|
||
|
||
function TeamNameInputCopy({ name }: { name: string }) {
|
||
return (
|
||
<p className="text-grey-400">
|
||
You’re creating the <strong>{name}</strong> team on 0x. You will need to share this account with your
|
||
teammates pending when invitations are enabled.
|
||
</p>
|
||
);
|
||
}
|
||
|
||
export default function CreateTeam() {
|
||
const actionData = useActionData<typeof action>();
|
||
const { email } = useLoaderData<typeof loader>();
|
||
|
||
const [teamName, setTeamName] = useState('');
|
||
const [teamType, setTeamType] = useState('');
|
||
|
||
const navigation = useNavigation();
|
||
|
||
return (
|
||
<main className="min-w-screen flex h-full min-h-full w-full flex-col bg-white">
|
||
<div className=" flex h-full w-full justify-center">
|
||
<div className="relative mt-40 inline-block w-full max-w-[456px]">
|
||
{actionData?.errors?.general && (
|
||
<Alert variant="error" className="mb-4">
|
||
{actionData?.errors?.general}
|
||
</Alert>
|
||
)}
|
||
<h1 className="text-2.5xl mb-4 text-black">Create a team</h1>
|
||
{teamName ? <TeamNameInputCopy name={teamName} /> : <StandardCopy />}
|
||
<Form method="post" className="relative mt-8 flex flex-col gap-[15px]">
|
||
<TextInput
|
||
label="Team name"
|
||
placeholder="Enter team name"
|
||
name="teamName"
|
||
value={teamName}
|
||
hiddenLabel
|
||
onChange={(val) => setTeamName(val.currentTarget.value)}
|
||
error={actionData?.errors?.teamName}
|
||
/>
|
||
<OptionSelect
|
||
options={options}
|
||
label="Team category"
|
||
hiddenLabel
|
||
name="teamCategory"
|
||
placeholder="What are you building?"
|
||
error={actionData?.errors?.teamCategory}
|
||
onChange={(val) => setTeamType(val)}
|
||
/>
|
||
{teamType === 'other' && (
|
||
<TextInput
|
||
label="What are you building (other)"
|
||
placeholder="Enter team category"
|
||
name="teamCategoryOthers"
|
||
hiddenLabel
|
||
id="teamCategoryOthers"
|
||
error={actionData?.errors?.teamCategoryOthers}
|
||
/>
|
||
)}
|
||
|
||
<Button
|
||
type="submit"
|
||
className="col-span-2 justify-center text-center"
|
||
size="md"
|
||
disabled={navigation.state !== 'idle'}
|
||
>
|
||
Finish Setup →
|
||
</Button>
|
||
</Form>
|
||
</div>
|
||
</div>
|
||
<footer className="fixed bottom-0 flex w-full items-center justify-between bg-white p-8 text-center text-black">
|
||
<div className="flex flex-col items-start text-sm">
|
||
<span className="text-grey-400">Logged in as:</span>
|
||
<span>{email}</span>
|
||
</div>
|
||
<Form action="/logout" method="post">
|
||
<button type="submit" className="text-grey-800 text-base">
|
||
Log out
|
||
</button>
|
||
</Form>
|
||
</footer>
|
||
</main>
|
||
);
|
||
}
|