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 (
+
+
+
+
+ );
+};
+
+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: {