diff --git a/components/team/screens/Team.tsx b/components/team/screens/Team.tsx new file mode 100644 index 0000000000..b064788d3b --- /dev/null +++ b/components/team/screens/Team.tsx @@ -0,0 +1,83 @@ +import React from "react"; +import Text from "@components/ui/Text"; +import Link from "next/link"; +import Avatar from "@components/Avatar"; +import { ArrowRightIcon } from "@heroicons/react/outline"; +import useTheme from "@components/Theme"; +import classnames from "classnames"; + +const Team = ({ team }) => { + useTheme(); + + const Member = ({ member }) => { + const classes = classnames( + "group", + "relative", + "flex flex-col", + "space-y-4", + "p-4", + "bg-white dark:bg-opacity-8", + "border border-neutral-200", + "hover:cursor-pointer", + "hover:border-black hover:border-2 dark:border-neutral-700 dark:hover:border-neutral-600", + "rounded-sm", + "hover:shadow-md" + ); + + return ( + +
+ + + + +
+ {member.user.name} + + {member.user.bio} + +
+
+ + ); + }; + + const Members = ({ members }) => { + if (!members || members.length === 0) { + return null; + } + + return ( +
+ {members.map((member) => { + return ; + })} +
+ ); + }; + + return ( +
+
+ + {team.name} +
+ +
+ ); +}; + +export default Team; diff --git a/components/ui/Text/Body/Body.tsx b/components/ui/Text/Body/Body.tsx index f8332092cf..3dedad9578 100644 --- a/components/ui/Text/Body/Body.tsx +++ b/components/ui/Text/Body/Body.tsx @@ -1,10 +1,9 @@ import React from "react"; import classnames from "classnames"; import { TextProps } from "../Text"; -import Styles from "../Text.module.css"; const Body: React.FunctionComponent = (props: TextProps) => { - const classes = classnames(Styles["text--body"], props?.className, props?.color); + const classes = classnames("text-lg leading-relaxed text-gray-900 dark:text-white"); return

{props.children}

; }; diff --git a/components/ui/Text/Caption/Caption.tsx b/components/ui/Text/Caption/Caption.tsx index 4dadc57c9f..95a340548d 100644 --- a/components/ui/Text/Caption/Caption.tsx +++ b/components/ui/Text/Caption/Caption.tsx @@ -1,10 +1,9 @@ import React from "react"; import classnames from "classnames"; import { TextProps } from "../Text"; -import Styles from "../Text.module.css"; const Caption: React.FunctionComponent = (props: TextProps) => { - const classes = classnames(Styles["text--caption"], props?.className, props?.color); + const classes = classnames("text-sm text-gray-500 dark:text-white leading-tight"); return

{props.children}

; }; diff --git a/components/ui/Text/Caption2/Caption2.tsx b/components/ui/Text/Caption2/Caption2.tsx index ca02e615e5..ffee176b96 100644 --- a/components/ui/Text/Caption2/Caption2.tsx +++ b/components/ui/Text/Caption2/Caption2.tsx @@ -1,10 +1,9 @@ import React from "react"; import classnames from "classnames"; import { TextProps } from "../Text"; -import Styles from "../Text.module.css"; const Caption2: React.FunctionComponent = (props: TextProps) => { - const classes = classnames(Styles["text--caption2"], props?.className, props?.color); + const classes = classnames("text-xs italic text-gray-500 dark:text-white leading-tight"); return

{props.children}

; }; diff --git a/components/ui/Text/Footnote/Footnote.tsx b/components/ui/Text/Footnote/Footnote.tsx index 3beda4fa21..adf789be21 100644 --- a/components/ui/Text/Footnote/Footnote.tsx +++ b/components/ui/Text/Footnote/Footnote.tsx @@ -3,7 +3,7 @@ import classnames from "classnames"; import { TextProps } from "../Text"; const Footnote: React.FunctionComponent = (props: TextProps) => { - const classes = classnames("text--footnote", props?.className, props?.color); + const classes = classnames("text-base font-normal text-gray-900 dark:text-white"); return

{props.children}

; }; diff --git a/components/ui/Text/Headline/Headline.tsx b/components/ui/Text/Headline/Headline.tsx index a7b531a998..7b52dd0e98 100644 --- a/components/ui/Text/Headline/Headline.tsx +++ b/components/ui/Text/Headline/Headline.tsx @@ -1,10 +1,9 @@ import React from "react"; import classnames from "classnames"; import { TextProps } from "../Text"; -import Styles from "../Text.module.css"; const Headline: React.FunctionComponent = (props: TextProps) => { - const classes = classnames(Styles["text--headline"], props?.className, props?.color); + const classes = classnames("text-xl font-bold text-gray-900 dark:text-white"); return

{props.children}

; }; diff --git a/components/ui/Text/Largetitle/Largetitle.tsx b/components/ui/Text/Largetitle/Largetitle.tsx index 8b0c3271b0..0451cf4deb 100644 --- a/components/ui/Text/Largetitle/Largetitle.tsx +++ b/components/ui/Text/Largetitle/Largetitle.tsx @@ -1,10 +1,9 @@ import React from "react"; import classnames from "classnames"; import { TextProps } from "../Text"; -import Styles from "../Text.module.css"; const Largetitle: React.FunctionComponent = (props: TextProps) => { - const classes = classnames(Styles["text--largetitle"], props?.className, props?.color); + const classes = classnames("text-2xl font-normal text-gray-900 dark:text-white"); return

{props.children}

; }; diff --git a/components/ui/Text/Overline/Overline.tsx b/components/ui/Text/Overline/Overline.tsx index 8200335199..94196a7bff 100644 --- a/components/ui/Text/Overline/Overline.tsx +++ b/components/ui/Text/Overline/Overline.tsx @@ -1,10 +1,11 @@ import React from "react"; import classnames from "classnames"; import { TextProps } from "../Text"; -import Styles from "../Text.module.css"; const Overline: React.FunctionComponent = (props: TextProps) => { - const classes = classnames(Styles["text--overline"], props?.className, props?.color); + const classes = classnames( + "text-sm uppercase font-semibold leading-snug tracking-wide text-gray-900 dark:text-white" + ); return

{props.children}

; }; diff --git a/components/ui/Text/Subheadline/Subheadline.tsx b/components/ui/Text/Subheadline/Subheadline.tsx index 550a955f99..535ac74ece 100644 --- a/components/ui/Text/Subheadline/Subheadline.tsx +++ b/components/ui/Text/Subheadline/Subheadline.tsx @@ -1,10 +1,9 @@ import React from "react"; import classnames from "classnames"; import { TextProps } from "../Text"; -import Styles from "../Text.module.css"; const Subheadline: React.FunctionComponent = (props: TextProps) => { - const classes = classnames(Styles["text--subheadline"], props?.className, props?.color); + const classes = classnames("text-xl text-gray-500 dark:text-white leading-relaxed"); return

{props.children}

; }; diff --git a/components/ui/Text/Subtitle/Subtitle.tsx b/components/ui/Text/Subtitle/Subtitle.tsx index 110656512e..302d21f49f 100644 --- a/components/ui/Text/Subtitle/Subtitle.tsx +++ b/components/ui/Text/Subtitle/Subtitle.tsx @@ -1,10 +1,9 @@ import React from "react"; import classnames from "classnames"; import { TextProps } from "../Text"; -import Styles from "../Text.module.css"; const Subtitle: React.FunctionComponent = (props: TextProps) => { - const classes = classnames(Styles["text--subtitle"], props?.className, props?.color); + const classes = classnames("ext-sm text-neutral-500 dark:text-white"); return

{props.children}

; }; diff --git a/components/ui/Text/Text.module.css b/components/ui/Text/Text.module.css deleted file mode 100644 index c850000e76..0000000000 --- a/components/ui/Text/Text.module.css +++ /dev/null @@ -1,52 +0,0 @@ -/* strong { - @apply font-medium; -} */ - -.text--body { - @apply text-lg leading-relaxed; -} - -.text--overline { - @apply text-sm uppercase font-semibold leading-snug tracking-wide; -} - -.text--caption { - @apply text-sm text-gray-500 leading-tight; -} - -.text--caption2 { - @apply text-xs italic text-gray-500 leading-tight; -} - -.text--footnote { - @apply text-base font-normal; -} - -.text--headline { - /* @apply text-base font-normal; */ - @apply text-3xl leading-8 font-semibold tracking-tight text-gray-900 sm:text-4xl; -} - -.text--subheadline { - @apply text-xl text-gray-500 leading-relaxed; -} - -.text--largetitle { - @apply text-2xl font-normal; -} - -.text--subtitle { - @apply text-base font-normal; -} - -.text--title { - @apply text-base font-normal; -} - -.text--title2 { - @apply text-base font-normal; -} - -.text--title3 { - @apply text-xs font-semibold leading-tight; -} diff --git a/components/ui/Text/Title/Title.tsx b/components/ui/Text/Title/Title.tsx index 7ebb409b51..1cfd91bb0c 100644 --- a/components/ui/Text/Title/Title.tsx +++ b/components/ui/Text/Title/Title.tsx @@ -1,10 +1,9 @@ import React from "react"; import classnames from "classnames"; import { TextProps } from "../Text"; -import Styles from "../Text.module.css"; const Title: React.FunctionComponent = (props: TextProps) => { - const classes = classnames(Styles["text--title"], props?.className, props?.color); + const classes = classnames("font-medium text-neutral-900 dark:text-white"); return

{props.children}

; }; diff --git a/components/ui/Text/Title2/Title2.tsx b/components/ui/Text/Title2/Title2.tsx index 3534c02e4c..e2bcd121a6 100644 --- a/components/ui/Text/Title2/Title2.tsx +++ b/components/ui/Text/Title2/Title2.tsx @@ -1,10 +1,9 @@ import React from "react"; import classnames from "classnames"; import { TextProps } from "../Text"; -import Styles from "../Text.module.css"; const Title2: React.FunctionComponent = (props: TextProps) => { - const classes = classnames(Styles["text--title2"], props?.className, props?.color); + const classes = classnames("text-base font-normal text-gray-900 dark:text-white"); return

{props.children}

; }; diff --git a/components/ui/Text/Title3/Title3.tsx b/components/ui/Text/Title3/Title3.tsx index 6f60baed3f..1b35651bdc 100644 --- a/components/ui/Text/Title3/Title3.tsx +++ b/components/ui/Text/Title3/Title3.tsx @@ -1,10 +1,9 @@ import React from "react"; import classnames from "classnames"; import { TextProps } from "../Text"; -import Styles from "../Text.module.css"; const Title3: React.FunctionComponent = (props: TextProps) => { - const classes = classnames(Styles["text--title3"], props?.className, props?.color); + const classes = classnames("text-xs font-semibold leading-tight text-gray-900 dark:text-white"); return

{props.children}

; }; diff --git a/lib/slugify.ts b/lib/slugify.ts new file mode 100644 index 0000000000..a0a530ef6c --- /dev/null +++ b/lib/slugify.ts @@ -0,0 +1,5 @@ +export const slugify = (str: string) => { + return str.replace(/\s+/g, "-").toLowerCase(); +}; + +export default slugify; diff --git a/lib/teams/getTeam.ts b/lib/teams/getTeam.ts new file mode 100644 index 0000000000..60939d0708 --- /dev/null +++ b/lib/teams/getTeam.ts @@ -0,0 +1,54 @@ +import { Team } from "@prisma/client"; +import prisma from "@lib/prisma"; +import logger from "@lib/logger"; + +const log = logger.getChildLogger({ prefix: ["[lib] getTeam"] }); +export const getTeam = async (idOrSlug: string): Promise => { + const teamIdOrSlug = idOrSlug; + + let team = null; + + log.debug(`{teamIdOrSlug} ${teamIdOrSlug}`); + + const teamSelectInput = { + id: true, + name: true, + slug: true, + members: { + where: { + accepted: true, + }, + select: { + user: { + select: { + id: true, + username: true, + email: true, + name: true, + bio: true, + avatar: true, + theme: true, + }, + }, + }, + }, + }; + + team = await prisma.team.findFirst({ + where: { + OR: [ + { + id: parseInt(teamIdOrSlug) || undefined, + }, + { + slug: teamIdOrSlug, + }, + ], + }, + select: teamSelectInput, + }); + + log.debug(`{team}`, { team }); + + return team; +}; diff --git a/pages/api/teams.ts b/pages/api/teams.ts index 73a48bd80c..b386832fa2 100644 --- a/pages/api/teams.ts +++ b/pages/api/teams.ts @@ -1,6 +1,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import prisma from "../../lib/prisma"; import { getSession } from "next-auth/client"; +import slugify from "@lib/slugify"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const session = await getSession({ req: req }); @@ -11,11 +12,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } if (req.method === "POST") { - // TODO: Prevent creating a team with identical names? + const slug = slugify(req.body.name); + + const nameCollisions = await prisma.team.count({ + where: { + OR: [{ name: req.body.name }, { slug: slug }], + }, + }); + + if (nameCollisions > 0) { + return res.status(409).json({ errorCode: "TeamNameCollision", message: "Team name already take." }); + } const createTeam = await prisma.team.create({ data: { name: req.body.name, + slug: slug, }, }); diff --git a/pages/team/[idOrSlug].tsx b/pages/team/[idOrSlug].tsx new file mode 100644 index 0000000000..ab0d9614bb --- /dev/null +++ b/pages/team/[idOrSlug].tsx @@ -0,0 +1,54 @@ +import { GetServerSideProps } from "next"; +import Head from "next/head"; + +import Theme from "@components/Theme"; +import { getTeam } from "@lib/teams/getTeam"; +import Team from "@components/team/screens/Team"; + +export default function Page(props) { + const { isReady } = Theme(); + + return ( + isReady && ( +
+ + {props.team.name} | Calendso + + + +
+ +
+
+ ) + ); +} + +export const getServerSideProps: GetServerSideProps = async (context) => { + const teamIdOrSlug = Array.isArray(context.query?.idOrSlug) + ? context.query.idOrSlug.pop() + : context.query.idOrSlug; + + const team = await getTeam(teamIdOrSlug); + + if (!team) { + return { + notFound: true, + }; + } + + return { + props: { + team, + }, + }; +}; + +// Auxiliary methods +export function getRandomColorCode(): string { + let color = "#"; + for (let idx = 0; idx < 6; idx++) { + color += Math.floor(Math.random() * 10); + } + return color; +} diff --git a/prisma/migrations/20210813194355_add_slug_to_team/migration.sql b/prisma/migrations/20210813194355_add_slug_to_team/migration.sql new file mode 100644 index 0000000000..0e4ef8246c --- /dev/null +++ b/prisma/migrations/20210813194355_add_slug_to_team/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Team" ADD COLUMN "slug" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 62e935610a..02bc0bc6e6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -72,6 +72,7 @@ model User { model Team { id Int @default(autoincrement()) @id name String? + slug String? members Membership[] } diff --git a/tailwind.config.js b/tailwind.config.js index 73d36b9268..ccdace0578 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -95,22 +95,28 @@ module.exports = { inter: ["Inter", "sans-serif"], kollektif: ["Kollektif", "sans-serif"], }, - maxHeight: (theme) => ({ + maxHeight: (theme, { breakpoints }) => ({ 0: "0", 97: "25rem", ...theme("spacing"), + ...breakpoints(theme("screens")), + ...theme("screens"), full: "100%", screen: "100vh", }), - minHeight: (theme) => ({ + minHeight: (theme, { breakpoints }) => ({ 0: "0", ...theme("spacing"), + ...breakpoints(theme("screens")), + ...theme("screens"), full: "100%", screen: "100vh", }), - minWidth: (theme) => ({ + minWidth: (theme, { breakpoints }) => ({ 0: "0", ...theme("spacing"), + ...breakpoints(theme("screens")), + ...theme("screens"), full: "100%", screen: "100vw", }), @@ -118,9 +124,23 @@ module.exports = { 0: "0", ...theme("spacing"), ...breakpoints(theme("screens")), + ...theme("screens"), full: "100%", screen: "100vw", }), + opacity: { + 0: "0", + 8: "0.08", + 10: "0.10", + 20: "0.20", + 40: "0.40", + 60: "0.60", + 80: "0.80", + 25: "0.25", + 50: "0.5", + 75: "0.75", + 100: "1", + }, }, }, variants: {