commit
						83bef7d95e
					
				
					 21 changed files with 249 additions and 79 deletions
				
			
		
							
								
								
									
										83
									
								
								components/team/screens/Team.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								components/team/screens/Team.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 ( | ||||
|       <Link key={member.id} href={`/${member.user.username}`}> | ||||
|         <div className={classes}> | ||||
|           <ArrowRightIcon | ||||
|             className={classnames( | ||||
|               "text-black dark:text-white", | ||||
|               "absolute top-4 right-4", | ||||
|               "h-4 w-4", | ||||
|               "transition-opacity", | ||||
|               "opacity-0 group-hover:opacity-100 group-hover:block" | ||||
|             )} | ||||
|           /> | ||||
| 
 | ||||
|           <Avatar user={member.user} className="w-12 h-12 rounded-full" /> | ||||
| 
 | ||||
|           <section className="space-y-2"> | ||||
|             <Text variant="title">{member.user.name}</Text> | ||||
|             <Text variant="subtitle" className="w-6/8"> | ||||
|               {member.user.bio} | ||||
|             </Text> | ||||
|           </section> | ||||
|         </div> | ||||
|       </Link> | ||||
|     ); | ||||
|   }; | ||||
| 
 | ||||
|   const Members = ({ members }) => { | ||||
|     if (!members || members.length === 0) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <section className="mx-auto min-w-full lg:min-w-lg max-w-5xl grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-12 gap-y-6"> | ||||
|         {members.map((member) => { | ||||
|           return <Member key={member.id} member={member} />; | ||||
|         })} | ||||
|       </section> | ||||
|     ); | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <article className="flex flex-col space-y-8 lg:space-y-12"> | ||||
|       <div className="mb-8 text-center"> | ||||
|         <Avatar | ||||
|           user={{ | ||||
|             email: team.name, | ||||
|           }} | ||||
|           className="mx-auto w-20 h-20 rounded-full mb-4" | ||||
|         /> | ||||
|         <Text variant="headline">{team.name}</Text> | ||||
|       </div> | ||||
|       <Members members={team.members} /> | ||||
|     </article> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default Team; | ||||
|  | @ -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<TextProps> = (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 <p className={classes}>{props.children}</p>; | ||||
| }; | ||||
|  |  | |||
|  | @ -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<TextProps> = (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 <p className={classes}>{props.children}</p>; | ||||
| }; | ||||
|  |  | |||
|  | @ -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<TextProps> = (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 <p className={classes}>{props.children}</p>; | ||||
| }; | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import classnames from "classnames"; | |||
| import { TextProps } from "../Text"; | ||||
| 
 | ||||
| const Footnote: React.FunctionComponent<TextProps> = (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 <p className={classes}>{props.children}</p>; | ||||
| }; | ||||
|  |  | |||
|  | @ -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<TextProps> = (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 <h1 className={classes}>{props.children}</h1>; | ||||
| }; | ||||
|  |  | |||
|  | @ -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<TextProps> = (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 <p className={classes}>{props.children}</p>; | ||||
| }; | ||||
|  |  | |||
|  | @ -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<TextProps> = (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 <p className={classes}>{props.children}</p>; | ||||
| }; | ||||
|  |  | |||
|  | @ -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<TextProps> = (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 <p className={classes}>{props.children}</p>; | ||||
| }; | ||||
|  |  | |||
|  | @ -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<TextProps> = (props: TextProps) => { | ||||
|   const classes = classnames(Styles["text--subtitle"], props?.className, props?.color); | ||||
|   const classes = classnames("ext-sm text-neutral-500 dark:text-white"); | ||||
| 
 | ||||
|   return <p className={classes}>{props.children}</p>; | ||||
| }; | ||||
|  |  | |||
|  | @ -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; | ||||
| } | ||||
|  | @ -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<TextProps> = (props: TextProps) => { | ||||
|   const classes = classnames(Styles["text--title"], props?.className, props?.color); | ||||
|   const classes = classnames("font-medium text-neutral-900 dark:text-white"); | ||||
| 
 | ||||
|   return <p className={classes}>{props.children}</p>; | ||||
| }; | ||||
|  |  | |||
|  | @ -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<TextProps> = (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 <p className={classes}>{props.children}</p>; | ||||
| }; | ||||
|  |  | |||
|  | @ -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<TextProps> = (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 <p className={classes}>{props.children}</p>; | ||||
| }; | ||||
|  |  | |||
							
								
								
									
										5
									
								
								lib/slugify.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								lib/slugify.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| export const slugify = (str: string) => { | ||||
|   return str.replace(/\s+/g, "-").toLowerCase(); | ||||
| }; | ||||
| 
 | ||||
| export default slugify; | ||||
							
								
								
									
										54
									
								
								lib/teams/getTeam.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								lib/teams/getTeam.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -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<Team | null> => { | ||||
|   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; | ||||
| }; | ||||
|  | @ -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, | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										54
									
								
								pages/team/[idOrSlug].tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								pages/team/[idOrSlug].tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 && ( | ||||
|       <div> | ||||
|         <Head> | ||||
|           <title>{props.team.name} | Calendso</title> | ||||
|           <link rel="icon" href="/favicon.ico" /> | ||||
|         </Head> | ||||
| 
 | ||||
|         <main className="mx-auto py-24 px-4"> | ||||
|           <Team team={props.team} /> | ||||
|         </main> | ||||
|       </div> | ||||
|     ) | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| 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; | ||||
| } | ||||
|  | @ -0,0 +1,2 @@ | |||
| -- AlterTable | ||||
| ALTER TABLE "Team" ADD COLUMN     "slug" TEXT; | ||||
|  | @ -72,6 +72,7 @@ model User { | |||
| model Team { | ||||
|   id            Int       @default(autoincrement()) @id | ||||
|   name          String? | ||||
|   slug          String? | ||||
|   members       Membership[] | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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: { | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue
	
	 Peer_Rich
						Peer_Rich