-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(teams, projects): implement create and read
- Loading branch information
Showing
38 changed files
with
5,262 additions
and
1,079 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 1,7 @@ | ||
{ | ||
"extends": ["next/core-web-vitals", "next/typescript"] | ||
"extends": [ | ||
"next/core-web-vitals", | ||
"next/typescript", | ||
"plugin:@tanstack/query/recommended" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1 @@ | ||
public-hoist-pattern[]=*@nextui-org/* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,47 @@ | ||
'use server'; | ||
|
||
import { createProject, findProjectsByTeamId } from '@/db/repo/projects'; | ||
import { findTeamByIdAndMemberId } from '@/db/repo/teams'; | ||
import { TeamError } from '@/errors/teams'; | ||
import { authedClient } from '@/lib/server/clients'; | ||
import { createProjectSchema } from '@/models/projects'; | ||
import { teamIdSchema } from '@/models/teams'; | ||
|
||
export const actionGetTeamProjects = authedClient | ||
.schema(teamIdSchema) | ||
.action(async ({ ctx, parsedInput: teamId }) => { | ||
const { user } = ctx; | ||
|
||
const team = await findTeamByIdAndMemberId(teamId, user.id); | ||
|
||
if (team.length === 0) throw new Error(TeamError.NotFound); | ||
|
||
const projects = await findProjectsByTeamId(teamId); | ||
|
||
return projects; | ||
}); | ||
|
||
export const actionCreateProject = authedClient | ||
.bindArgsSchemas([teamIdSchema.nullable()]) | ||
.schema(createProjectSchema) | ||
.action( | ||
async ({ | ||
ctx: { user }, | ||
parsedInput: dto, | ||
bindArgsParsedInputs: [teamId], | ||
}) => { | ||
if (!teamId) throw new Error(TeamError.BadRequestTeamIdNotProvided); | ||
|
||
const team = await findTeamByIdAndMemberId(teamId, user.id); | ||
|
||
if (team.length === 0) throw new Error(TeamError.NotFound); | ||
|
||
const project = await createProject({ | ||
teamId, | ||
name: dto.name, | ||
description: dto.description, | ||
}); | ||
|
||
return project; | ||
} | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,18 @@ | ||
'use server'; | ||
|
||
import { createTeam, findTeamsByMemberId } from '@/db/repo/teams'; | ||
import { authedClient } from '@/lib/server/clients'; | ||
import { mutateTeamSchema } from '@/models/teams'; | ||
|
||
export const actionGetUserTeams = authedClient.action(async ({ ctx }) => { | ||
return await findTeamsByMemberId(ctx.user.id); | ||
}); | ||
|
||
export const actionCreateTeam = authedClient | ||
.schema(mutateTeamSchema) | ||
.action(async ({ ctx, parsedInput: data }) => { | ||
return await createTeam({ | ||
name: data.name, | ||
ownedBy: ctx.user.id, | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,29 @@ | ||
'use client'; | ||
|
||
import { Link } from '@nextui-org/link'; | ||
import { UserAvatar } from './user-avatar'; | ||
|
||
type AppNavbarProps = { | ||
name: string; | ||
avatarUrl: string; | ||
}; | ||
|
||
export function AppNavbar({ name, avatarUrl }: AppNavbarProps) { | ||
return ( | ||
<nav className="w-full grid grid-flow-col px-8 py-6"> | ||
<div className="grid grid-cols-6 gap-x-6"> | ||
<div> | ||
<Link href="/">ReqLink</Link> | ||
</div> | ||
|
||
<div className="col-span-5 space-x-6"> | ||
<Link href="/projects">Projects</Link> | ||
</div> | ||
</div> | ||
|
||
<div className="flex flex-row-reverse"> | ||
<UserAvatar name={name} url={avatarUrl} /> | ||
</div> | ||
</nav> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,27 @@ | ||
'use client'; | ||
|
||
import { User } from '@nextui-org/user'; | ||
import { User2Icon } from 'lucide-react'; | ||
|
||
type UserAvatarProps = { | ||
url: string; | ||
name: string; | ||
}; | ||
|
||
export function UserAvatar({ name, url }: UserAvatarProps) { | ||
const initials = name.charAt(0).toUpperCase(); | ||
|
||
return ( | ||
<User | ||
name={name} | ||
isFocusable | ||
className="flex-row-reverse" | ||
avatarProps={{ | ||
src: url, | ||
fallback: <User2Icon className="size-4" />, | ||
showFallback: true, | ||
name: initials, | ||
}} | ||
/> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,22 @@ | ||
import { getCurrentSession } from '@/auth/sessions'; | ||
import { redirect } from 'next/navigation'; | ||
import React from 'react'; | ||
import { AppNavbar } from './_components/app-navbar'; | ||
|
||
export default async function RootLayout({ | ||
children, | ||
}: React.PropsWithChildren) { | ||
const { user } = await getCurrentSession(); | ||
|
||
if (!user) { | ||
return redirect('/login'); | ||
} | ||
|
||
return ( | ||
<> | ||
<AppNavbar name={user.name} avatarUrl={user.picture} /> | ||
|
||
<main className="flex-1 flex flex-col">{children}</main> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,19 @@ | ||
'use client'; | ||
|
||
import { Card, CardBody } from '@nextui-org/card'; | ||
import { Skeleton } from '@nextui-org/skeleton'; | ||
|
||
export function ProjectCardSkeleton() { | ||
return ( | ||
<Card className="w-full bg-background"> | ||
<CardBody className="gap-y-8"> | ||
<Skeleton className="w-full h-20 rounded-lg" /> | ||
|
||
<div className="grid grid-cols-2 gap-x-8"> | ||
<Skeleton className="h-12 rounded-lg" /> | ||
<Skeleton className="h-12 rounded-lg" /> | ||
</div> | ||
</CardBody> | ||
</Card> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,56 @@ | ||
import { formatDistanceDate } from '@/lib/date'; | ||
import { Card, CardBody, CardFooter, CardHeader } from '@nextui-org/card'; | ||
import { Divider } from '@nextui-org/divider'; | ||
import Link from 'next/link'; | ||
|
||
type ProjectCardProps = { | ||
projectId: string; | ||
name: string; | ||
lastUpdated: Date; | ||
requirementSpecLeft: number; | ||
backlogLeft: number; | ||
}; | ||
|
||
export function ProjectCard({ | ||
projectId, | ||
name, | ||
backlogLeft, | ||
lastUpdated, | ||
requirementSpecLeft, | ||
}: ProjectCardProps) { | ||
return ( | ||
<Card | ||
className="max-w-sm p-2 h-fit" | ||
isPressable | ||
isHoverable | ||
as={Link} | ||
href={`/projects/${projectId}`} | ||
> | ||
<CardHeader className="justify-between items-start"> | ||
<div className="text-left"> | ||
<h5 className="text-lg font-medium">{name}</h5> | ||
<p className="text-sm text-default-500"> | ||
Last edit{' '} | ||
{formatDistanceDate({ from: lastUpdated, to: new Date() })} | ||
</p> | ||
</div> | ||
</CardHeader> | ||
|
||
<CardBody> | ||
<Divider /> | ||
</CardBody> | ||
|
||
<CardFooter className="grid grid-cols-2 text-left gap-x-4"> | ||
<div> | ||
<h6 className="text-sm text-default-500">Requirements Left</h6> | ||
<p className="text-xl font-medium">{requirementSpecLeft}</p> | ||
</div> | ||
|
||
<div> | ||
<h6 className="text-sm text-default-500">Backlog Left</h6> | ||
<p className="text-xl font-medium">{backlogLeft}</p> | ||
</div> | ||
</CardFooter> | ||
</Card> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,18 @@ | ||
import { CreateProjectModal } from '@/components/projects/create-project-modal'; | ||
|
||
export function ProjectEmpty() { | ||
return ( | ||
<div className="flex flex-col items-center justify-center space-y-4 w-full h-full flex-1"> | ||
<div className="text-center"> | ||
<h6 className="text-xl font-medium">There are no project here</h6> | ||
<p className="text-default-500"> | ||
Getting started by create new project | ||
</p> | ||
</div> | ||
|
||
<div> | ||
<CreateProjectModal buttonClassName="block" /> | ||
</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,67 @@ | ||
'use client'; | ||
|
||
import { useQueryGetTeamProjects } from '@/hooks/queries/use-get-team-projects'; | ||
import { useSafeContext } from '@/lib/utils'; | ||
import { TeamsContext } from '@/providers/teams-provider'; | ||
import { ProjectCard } from './project-card'; | ||
import { ProjectCardSkeleton } from './project-card-skeleton'; | ||
import { ProjectEmpty } from './project-empty'; | ||
import { ScrollShadow } from '@nextui-org/scroll-shadow'; | ||
|
||
export function ProjectList() { | ||
const { selectedTeamId } = useSafeContext(TeamsContext); | ||
|
||
const { data, isPending } = useQueryGetTeamProjects({ | ||
teamId: selectedTeamId, | ||
}); | ||
const projects = data ?? []; | ||
|
||
const isEmpty = projects.length === 0; | ||
|
||
if (!isPending && isEmpty) return <ProjectEmpty />; | ||
|
||
return ( | ||
<ScrollShadow className="px-32 py-12 max-h-[70svh]"> | ||
<div className="w-full grid grid-cols-3 gap-6"> | ||
{!isPending && | ||
projects.map((project) => ( | ||
<ProjectCard | ||
key={project.id} | ||
projectId={project.id} | ||
name={project.name} | ||
lastUpdated={project.updatedAt} | ||
backlogLeft={project.backlogLeft} | ||
requirementSpecLeft={project.requirementSpecLeft} | ||
/> | ||
))} | ||
{!isPending && | ||
projects.map((project) => ( | ||
<ProjectCard | ||
key={project.id} | ||
projectId={project.id} | ||
name={project.name} | ||
lastUpdated={project.updatedAt} | ||
backlogLeft={project.backlogLeft} | ||
requirementSpecLeft={project.requirementSpecLeft} | ||
/> | ||
))} | ||
{!isPending && | ||
projects.map((project) => ( | ||
<ProjectCard | ||
key={project.id} | ||
projectId={project.id} | ||
name={project.name} | ||
lastUpdated={project.updatedAt} | ||
backlogLeft={project.backlogLeft} | ||
requirementSpecLeft={project.requirementSpecLeft} | ||
/> | ||
))} | ||
|
||
{isPending && | ||
Array.from({ length: 6 }).map((_, index) => ( | ||
<ProjectCardSkeleton key={index} /> | ||
))} | ||
</div> | ||
</ScrollShadow> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,24 @@ | ||
'use client'; | ||
|
||
import { CreateTeamModal } from '@/components/teams/create-team-modal'; | ||
import { TeamSelector } from '@/components/teams/team-selector'; | ||
import { useSafeContext } from '@/lib/utils'; | ||
import { TeamsContext } from '@/providers/teams-provider'; | ||
|
||
export function TeamNavs() { | ||
const { getTeams, selectedTeamId, setSelectedTeamId } = | ||
useSafeContext(TeamsContext); | ||
|
||
return ( | ||
<div className="grid grid-flow-col items-center gap-x-6"> | ||
<TeamSelector | ||
isLoading={getTeams.isLoading} | ||
teams={getTeams.items} | ||
selectedTeamId={selectedTeamId} | ||
onSelectionChange={setSelectedTeamId} | ||
/> | ||
|
||
<CreateTeamModal reloadTeams={getTeams.reload} /> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,22 @@ | ||
import { CreateProjectModal } from '@/components/projects/create-project-modal'; | ||
import { TeamsProvider } from '@/providers/teams-provider'; | ||
import { ProjectList } from './_components/project-list'; | ||
import { TeamNavs } from './_components/team-navs'; | ||
|
||
export default async function ProjectsPage() { | ||
return ( | ||
<section id="projects-page" className="my-8 flex-1 flex-col flex"> | ||
<TeamsProvider isSelectFirstTeam> | ||
<> | ||
<div className="flex flex-row items-center justify-between px-32"> | ||
<TeamNavs /> | ||
|
||
<CreateProjectModal /> | ||
</div> | ||
|
||
<ProjectList /> | ||
</> | ||
</TeamsProvider> | ||
</section> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.