Skip to content

Serena/login #666

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
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
4 changes: 3 additions & 1 deletion frontend2/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
{}
{
"plugins": ["prettier-plugin-tailwindcss"]
}
213 changes: 108 additions & 105 deletions frontend2/package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion frontend2/package.json
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"@headlessui/react": "^1.7.15",
"@headlessui/tailwindcss": "^0.2.0",
"@heroicons/react": "^2.0.18",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
@@ -60,7 +61,8 @@
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.32.2",
"js-cookie": "^3.0.5",
"prettier": "2.8.8",
"prettier": "3.0.2",
"prettier-plugin-tailwindcss": "^0.5.3",
"react-scripts": "5.0.1",
"tailwindcss": "^3.3.2",
"typescript": "^4.9.5"
1 change: 1 addition & 0 deletions frontend2/src/components/EpisodeLayout.tsx
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ import Navbar from "./Navbar";
import Sidebar from "./sidebar";
import { Outlet, useParams } from "react-router-dom";
import { EpisodeContext } from "../contexts/EpisodeContext";
import { useCurrentUser } from "../contexts/CurrentUserContext";

// This component contains the NavBar and SideBar.
// Child route components are rendered with <Outlet />
25 changes: 19 additions & 6 deletions frontend2/src/components/elements/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
import React from "react";
import Icon, { type IconName } from "./Icon";

interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {
variant?: string;
label?: string;
iconName?: IconName;
fullWidth?: boolean;
className?: string;
}

const variants: Record<string, string> = {
"": "bg-gray-50 text-gray-900 hover:bg-gray-100 ring-gray-300 ring-1 ring-inset",
dark: "bg-gray-700 text-gray-50 hover:bg-gray-800",
"": "bg-gray-50 text-gray-800 hover:bg-gray-100 hover:ring-gray-900 hover:text-black ring-gray-500 ring-1 ring-inset",
dark: "bg-cyan-700 text-white hover:bg-cyan-800",
};

const Button: React.FC<ButtonProps> = ({ variant, label, ...rest }) => {
variant = variant ?? "";
const variantStyle = variants[variant];
const Button: React.FC<ButtonProps> = ({
variant = "",
label,
iconName,
fullWidth = false,
className = "",
...rest
}) => {
const variantStyle = `${variants[variant]} ${
fullWidth ? "w-full" : ""
} ${className}`;
return (
<button
// default button type
type="button"
className={`rounded-md px-2.5 py-1.5 text-sm font-semibold shadow-sm ${variantStyle}`}
className={`flex h-9 flex-row items-center justify-center gap-1.5 rounded px-2.5 py-1.5 font-medium shadow-sm ${variantStyle}`}
{...rest}
>
{iconName !== undefined && <Icon name={iconName} size="sm" />}
{label}
</button>
);
17 changes: 17 additions & 0 deletions frontend2/src/components/elements/FormError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from "react";

const FormError: React.FC<{ message?: string; className?: string }> = ({
message,
className,
}) => {
return (
<span
role="alert"
className={`absolute mt-0.5 text-xs text-red-700 ${className ?? ""}`}
>
{message ?? "This field is invalid."}
</span>
);
};

export default FormError;
40 changes: 40 additions & 0 deletions frontend2/src/components/elements/FormLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from "react";
import Icon from "./Icon";
import Tooltip from "./Tooltip";

const FormLabel: React.FC<{
label?: string;
required?: boolean;
className?: string;
tooltip?: string;
}> = ({ label, required = false, className, tooltip }) => {
return (
<div
className={`flex flex-row items-center gap-1 text-sm font-medium leading-6 text-gray-700 ${
className ?? ""
}`}
>
<Tooltip tooltip="asdflkajdsf">
<Icon
className="ml-0.5 mt-0.5 inline text-gray-400"
size="xs"
name="information_circle"
/>
</Tooltip>

{label}
{required && <span className="text-red-700"> *</span>}
{tooltip !== undefined && (
<Tooltip tooltip={tooltip}>
<Icon
className="ml-0.5 mt-0.5 inline text-gray-400"
size="xs"
name="information_circle"
/>
</Tooltip>
)}
</div>
);
};

export default FormLabel;
85 changes: 85 additions & 0 deletions frontend2/src/components/elements/Icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React from "react";
import {
ClipboardDocumentIcon as ClipboardDocumentIcon24,
HomeIcon as HomeIcon24,
MapIcon as MapIcon24,
TrophyIcon as TrophyIcon24,
ChartBarIcon as ChartBarIcon24,
ClockIcon as ClockIcon24,
UserGroupIcon as UserGroupIcon24,
ArrowUpTrayIcon as ArrowUpTrayIcon24,
PlayCircleIcon as PlayCircleIcon24,
ChevronDownIcon as ChevronDownIcon24,
CheckIcon as CheckIcon24,
InformationCircleIcon as InformationCircleIcon24,
} from "@heroicons/react/24/outline";

import {
ClipboardDocumentIcon as ClipboardDocumentIcon20,
HomeIcon as HomeIcon20,
MapIcon as MapIcon20,
TrophyIcon as TrophyIcon20,
ChartBarIcon as ChartBarIcon20,
ClockIcon as ClockIcon20,
UserGroupIcon as UserGroupIcon20,
ArrowUpTrayIcon as ArrowUpTrayIcon20,
PlayCircleIcon as PlayCircleIcon20,
ChevronDownIcon as ChevronDownIcon20,
CheckIcon as CheckIcon20,
InformationCircleIcon as InformationCircleIcon20,
} from "@heroicons/react/20/solid";

const icons24 = {
clipboard_document: ClipboardDocumentIcon24,
home: HomeIcon24,
map: MapIcon24,
trophy: TrophyIcon24,
chart_bar: ChartBarIcon24,
clock: ClockIcon24,
user_group: UserGroupIcon24,
arrow_up_tray: ArrowUpTrayIcon24,
play_circle: PlayCircleIcon24,
chevron_down: ChevronDownIcon24,
check: CheckIcon24,
information_circle: InformationCircleIcon24,
};

const icons20 = {
clipboard_document: ClipboardDocumentIcon20,
home: HomeIcon20,
map: MapIcon20,
trophy: TrophyIcon20,
chart_bar: ChartBarIcon20,
clock: ClockIcon20,
user_group: UserGroupIcon20,
arrow_up_tray: ArrowUpTrayIcon20,
play_circle: PlayCircleIcon20,
chevron_down: ChevronDownIcon20,
check: CheckIcon20,
information_circle: InformationCircleIcon20,
};

export type IconName = keyof typeof icons24 | keyof typeof icons20;

export interface IconProps {
name: IconName;
size?: "sm" | "md" | "xs";
className?: string;
}

const sizeToClass = {
sm: "h-5 w-5",
md: "h-6 w-6",
xs: "h-4 w-4"
};

const Icon: React.FC<IconProps> = ({
name,
size = "md",
className = "",
}: IconProps) => {
const IconComponent = size === "md" ? icons24[name] : icons20[name];
return <IconComponent className={`${sizeToClass[size]} ${className}`} />;
};

export default Icon;
43 changes: 24 additions & 19 deletions frontend2/src/components/elements/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,37 @@
import React, { forwardRef } from "react";
import FormError from "./FormError";
import FormLabel from "./FormLabel";

interface InputProps extends React.ComponentPropsWithoutRef<"input"> {
label?: string;
required?: boolean;
className?: string;
errorMessage?: string;
tooltip?: string;
}

const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
{ label, required, ...rest },
ref
{ label, required = false, className = "", errorMessage, tooltip, ...rest },
ref,
) {
required = required ?? false;
const invalid = errorMessage !== undefined;
return (
<div className="w-full">
{label !== undefined && (
<label className="flex flex-col w-full gap-1 text-sm font-medium leading-6 text-gray-900">
<span>
{label}
{required && <span className="text-red-700"> *</span>}
</span>
</label>
)}
<div className="relative rounded-md shadow-sm">
<input
ref={ref}
className="block w-full rounded-md border-0 py-1.5 px-2 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-700 sm:text-sm sm:leading-6"
{...rest}
/>
</div>
<div className={`relative ${invalid ? "mb-1" : ""} ${className}`}>
<label>
<FormLabel label={label} required={required} tooltip={tooltip} />
<div className="relative rounded-md shadow-sm">
<input
ref={ref}
aria-invalid={errorMessage !== undefined ? "true" : "false"}
className={`block w-full rounded-md border-0 px-2 py-1.5 text-gray-900 ring-1 ring-inset
ring-gray-300 placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-inset
focus:ring-cyan-600 sm:text-sm sm:leading-6
${invalid ? "ring-red-500" : ""}`}
{...rest}
/>
</div>
{invalid && <FormError message={errorMessage} />}
</label>
</div>
);
});
90 changes: 90 additions & 0 deletions frontend2/src/components/elements/SelectMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React, { Fragment, useMemo, useState } from "react";
import { Listbox, Transition } from "@headlessui/react";
import Icon from "./Icon";
import FormError from "./FormError";
import FormLabel from "./FormLabel";

interface SelectMenuProps<T extends React.Key | null | undefined> {
options: Array<{ value: T; label: string }>;
label?: string;
required?: boolean;
value?: T;
placeholder?: string;
className?: string;
errorMessage?: string;
onChange?: (value: T) => void;
}

function SelectMenu<T extends React.Key | null | undefined>({
label,
required = false,
options,
value,
placeholder,
className = "",
errorMessage,
onChange,
}: SelectMenuProps<T>): JSX.Element {
const valueToLabel = useMemo(
() => new Map(options.map((option) => [option.value, option.label])),
[options],
);
const invalid = errorMessage !== undefined;
return (
<div className={`relative ${invalid ? "mb-2" : ""} ${className}`}>
<Listbox value={value} onChange={onChange}>
<div className="relative">
{label !== undefined && (
<Listbox.Label>
<FormLabel label={label} required={required} />
</Listbox.Label>
)}
<Listbox.Button
className={`relative h-9 w-full truncate rounded-md bg-white py-1.5
pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300
focus:outline-none focus:ring-1 focus:ring-cyan-600 ui-open:ring-cyan-600
sm:text-sm sm:leading-6 ${invalid ? "ring-red-500" : ""}`}
>
<span className={`${value === undefined ? "text-gray-400" : ""}`}>
{value === undefined ? placeholder : valueToLabel.get(value)}
</span>
<div
className="absolute inset-y-0 right-0 mr-2 flex transform items-center
transition duration-300 ui-open:rotate-180"
>
<Icon name="chevron_down" size="sm" />
</div>
</Listbox.Button>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
className="absolute z-10 mt-1 max-h-48 w-full overflow-auto rounded-md
bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none
sm:max-h-60 sm:text-sm"
>
{options.map((option) => (
<Listbox.Option
className="flex cursor-default flex-row justify-between py-1.5 pl-4 pr-2 ui-active:bg-cyan-100"
key={option.value}
value={option.value}
>
<div className="overflow-x-auto pr-2">{option.label}</div>
<span className=" hidden items-center text-cyan-900 ui-selected:flex">
<Icon name="check" size="sm" />
</span>
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
{invalid && <FormError message={errorMessage} />}
</div>
);
}

export default SelectMenu;
27 changes: 27 additions & 0 deletions frontend2/src/components/elements/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from "react";

interface TooltipProps {
children?: React.ReactNode;
tooltip: string;
className?: string;
}

const Tooltip: React.FC<TooltipProps> = ({
children,
tooltip,
className = "",
}) => {
return (
<div className={`${className}`}>
{children}
<div
className="absolute -top-5 rounded-md bg-cyan-50 px-1.5 py-1 text-sm text-cyan-700 outline-1
outline-cyan-500 sm:text-xs"
>
{tooltip}
</div>
</div>
);
};

export default Tooltip;
11 changes: 8 additions & 3 deletions frontend2/src/components/sidebar/SidebarItem.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import React from "react";
import { NavLink } from "react-router-dom";
import Icon, { type IconName } from "../elements/Icon";

interface SidebarItemProps {
icon: React.ReactNode;
iconName: IconName;
text: string;
linkTo: string;
}

const SidebarItem: React.FC<SidebarItemProps> = ({ icon, text, linkTo }) => {
const SidebarItem: React.FC<SidebarItemProps> = ({
iconName,
text,
linkTo,
}) => {
const baseStyle = "text-base flex items-center gap-3 ";
const colorVariants = {
gray: "text-gray-800 hover:text-gray-400",
@@ -20,7 +25,7 @@ const SidebarItem: React.FC<SidebarItemProps> = ({ icon, text, linkTo }) => {
}
to={linkTo}
>
{icon}
<Icon name={iconName} size="md" />
{text}
</NavLink>
);
33 changes: 9 additions & 24 deletions frontend2/src/components/sidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,6 @@
import React, { useContext } from "react";
import SidebarSection from "./SidebarSection";
import SidebarItem from "./SidebarItem";
import {
ClipboardDocumentIcon,
HomeIcon,
MapIcon,
TrophyIcon,
ChartBarIcon,
ClockIcon,
UserGroupIcon,
ArrowUpTrayIcon,
PlayCircleIcon,
} from "@heroicons/react/24/outline";
import { EpisodeContext } from "../../contexts/EpisodeContext";

interface SidebarProps {
@@ -26,52 +15,48 @@ const Sidebar: React.FC<SidebarProps> = ({ collapsed }) => {
return collapsed ? null : (
<div className="flex flex-col gap-8 py-4 h-full bg-gray-50 shadow-gray-200 shadow-sm">
<SidebarSection title="">
<SidebarItem iconName="home" text="Home" linkTo={`${linkBase}home`} />
<SidebarItem
icon={<HomeIcon className="h-6 w-6" />}
text="Home"
linkTo={`${linkBase}home`}
/>
<SidebarItem
icon={<MapIcon className="h-6 w-6" />}
iconName="map"
text="Quick Start"
linkTo={`${linkBase}quickstart`}
/>
<SidebarItem
icon={<ClipboardDocumentIcon className="h-6 w-6" />}
iconName="clipboard_document"
text="Resources"
linkTo={`${linkBase}resources`}
/>
</SidebarSection>
<SidebarSection title="compete">
<SidebarItem
icon={<TrophyIcon className="h-6 w-6" />}
iconName="trophy"
text="Tournaments"
linkTo={`${linkBase}tournaments`}
/>
<SidebarItem
icon={<ChartBarIcon className="h-6 w-6" />}
iconName="chart_bar"
text="Rankings"
linkTo={`${linkBase}rankings`}
/>
<SidebarItem
icon={<ClockIcon className="h-6 w-6" />}
iconName="clock"
text="Queue"
linkTo={`${linkBase}queue`}
/>
</SidebarSection>
<SidebarSection title="team management">
<SidebarItem
icon={<UserGroupIcon className="h-6 w-6" />}
iconName="user_group"
text="My Team"
linkTo={`${linkBase}team`}
/>
<SidebarItem
icon={<ArrowUpTrayIcon className="h-6 w-6" />}
iconName="arrow_up_tray"
text="Submissions"
linkTo={`${linkBase}submission`}
/>
<SidebarItem
icon={<PlayCircleIcon className="h-6 w-6" />}
iconName="play_circle"
text="Scrimmaging"
linkTo={`${linkBase}scrimmaging`}
/>
1 change: 1 addition & 0 deletions frontend2/src/index.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url("https://fonts.googleapis.com/css2?family=Inter&family=Josefin+Sans&display=swap");
505 changes: 253 additions & 252 deletions frontend2/src/utils/apiTypes.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions frontend2/src/utils/utilTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Maybe<T> = T | undefined;
33 changes: 32 additions & 1 deletion frontend2/src/views/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,38 @@
import React from "react";
import * as Auth from "../utils/auth";
import Input from "../components/elements/Input";
import Button from "../components/elements/Button";
import { type SubmitHandler, useForm } from "react-hook-form";

interface LoginFormInput {
username: string;
password: string;
}

const Login: React.FC = () => {
return <p>login page</p>;
const { register, handleSubmit } = useForm<LoginFormInput>();
const onSubmit: SubmitHandler<LoginFormInput> = async (data) => {
console.log("submitted");
try {
await Auth.login(data.username, data.password);
} catch (error) {}
console.log("logged in successfully");
};
return (
<div className="flex h-screen flex-col items-center bg-gray-50">
<span className="text-center">BATTLECODE</span>
{/* https://github.com/orgs/react-hook-form/discussions/8622 */}
<form
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onSubmit={handleSubmit(onSubmit)}
className="flex w-96 flex-col gap-3 bg-gray-100 p-4"
>
<Input label="Username" {...register("username")} />
<Input label="Password" {...register("password")} type="password" />
<Button label="Submit" type="submit" variant="dark" />
</form>
</div>
);
};

export default Login;
169 changes: 167 additions & 2 deletions frontend2/src/views/Register.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,172 @@
import React from "react";
import React, { useState } from "react";
import * as Auth from "../utils/auth";
import Input from "../components/elements/Input";
import Button from "../components/elements/Button";
import { type SubmitHandler, useForm } from "react-hook-form";
import { useCurrentUser } from "../contexts/CurrentUserContext";
import {
GenderEnum,
type Country,
type CreateUserInput,
COUNTRIES,
} from "../utils/apiTypes";
import SelectMenu from "../components/elements/SelectMenu";
import { type Maybe } from "../utils/utilTypes";

const REQUIRED_ERROR_MSG = "This field is required.";

const Register: React.FC = () => {
return <p>register page</p>;
const { login } = useCurrentUser();
const {
register,
handleSubmit,
setValue,
setError,
clearErrors,
formState: { errors },
} = useForm<CreateUserInput>();
const [gender, setGender] = useState<Maybe<GenderEnum>>();
const [country, setCountry] = useState<Maybe<Country>>();

const onSubmit: SubmitHandler<CreateUserInput> = async (data) => {
if (gender === undefined || country === undefined) {
return;
}
try {
const newUser = await Auth.register(data);
login(newUser);
console.log("logged in successfully");
} catch (error) {
console.log("failure to register", error);
}
};
return (
<div className="flex h-screen flex-col items-center justify-center bg-gradient-to-tr from-cyan-200 to-cyan-700 p-2">
<h2 className="flex flex-1 items-end text-center font-display text-5xl tracking-wide text-white sm:text-6xl">
BATTLECODE
</h2>
{/* https://github.com/orgs/react-hook-form/discussions/8622 */}
<form
/* eslint-disable-next-line @typescript-eslint/no-misused-promises */
onSubmit={async (event) => {
// validate gender and country
await handleSubmit(onSubmit)(event);
if (gender === undefined) {
setError("profile.gender", { message: REQUIRED_ERROR_MSG });
}
if (country === undefined) {
setError("profile.country", { message: REQUIRED_ERROR_MSG });
}
}}
className="m-6 flex w-11/12 flex-col gap-5 rounded-lg bg-gray-100 p-6 shadow-md sm:w-[550px]"
>
<Input
required
placeholder="battlecode_player_6.9610"
label="Username"
errorMessage={errors.username?.message}
{...register("username", { required: REQUIRED_ERROR_MSG })}
/>
<Input
required
placeholder="************"
label="Password"
type="password"
errorMessage={errors.password?.message}
{...register("password", { required: REQUIRED_ERROR_MSG })}
/>
<Input
required
placeholder="player@example.com"
label="Email"
type="email"
errorMessage={errors.email?.message}
{...register("email", { required: REQUIRED_ERROR_MSG })}
/>
<div className="grid grid-cols-2 gap-5">
<Input
className="flex-grow basis-0"
required
placeholder="Tim"
label="First name"
errorMessage={errors.firstName?.message}
{...register("firstName", { required: REQUIRED_ERROR_MSG })}
/>
<Input
className="flex-grow basis-0"
required
placeholder="Beaver"
label="Last name"
errorMessage={errors.lastName?.message}
{...register("lastName", { required: REQUIRED_ERROR_MSG })}
/>
</div>
{/* begin profile fields */}
<div className="grid grid-cols-2 gap-5">
<SelectMenu<Country>
required
onChange={(newCountry) => {
setCountry(newCountry);
setValue("profile.country", newCountry);
clearErrors("profile.country");
}}
errorMessage={errors.profile?.country?.message}
value={country}
label="Country"
placeholder="Select country"
options={Object.entries(COUNTRIES).map(([code, name]) => ({
value: code as Country,
label: name,
}))}
/>
<Input
placeholder="MIT"
label="School"
{...register("profile.school")}
/>
</div>
<div className="grid grid-cols-2 gap-5">
<SelectMenu<GenderEnum>
required
onChange={(newGender) => {
setGender(newGender);
setValue("profile.gender", newGender);
clearErrors("profile.gender");
}}
errorMessage={errors.profile?.gender?.message}
value={gender}
label="Gender identity"
placeholder="Select gender"
options={[
{ value: GenderEnum.FEMALE, label: "Female" },
{ value: GenderEnum.MALE, label: "Male" },
{ value: GenderEnum.NONBINARY, label: "Non-binary" },
{
value: GenderEnum.SELF_DESCRIBED,
label: "Prefer to self describe",
},
{ value: GenderEnum.RATHER_NOT_SAY, label: "Rather not say" },
]}
/>
{gender === GenderEnum.SELF_DESCRIBED && (
<Input
label="Self described gender identity"
{...register("profile.genderDetails")}
/>
)}
</div>

<Button
className="mt-1"
fullWidth
label="Register"
type="submit"
variant="dark"
/>
</form>
<div className="flex-1" />
</div>
);
};

export default Register;
10 changes: 6 additions & 4 deletions frontend2/tailwind.config.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
/** @type {import('tailwindcss').Config} */
const colors = require("tailwindcss/colors");
const defaultTheme = require("tailwindcss/defaultTheme");

module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {
colors: {
teal: "#00A28E",
},
container: {
center: true,
},
fontFamily: {
sans: ["Inter", ...defaultTheme.fontFamily.sans],
display: ['"Josefin Sans"', "sans-serif"],
},
},
},
plugins: [require("@tailwindcss/forms")],
plugins: [require("@tailwindcss/forms"), require("@headlessui/tailwindcss")],
};