Skip to content
Open
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use client'

import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select"
import * as React from "react"
import {useParams, useRouter} from "next/navigation"
import {type ApiTeamSetTemplate} from "@/_temp_types/api/teams"

export type ProjectSetSelectProps = {
allProjectSets: ApiTeamSetTemplate[]
}

export function ProjectSetSelect({allProjectSets}: ProjectSetSelectProps) {
const {courseId, projectSetId} = useParams<{ courseId: string, projectSetId: string }>()
const router = useRouter()
const handleProjectSetChanged = (newProjectSetId: string) => {
router.push(`/course/${courseId}/project-sets/${newProjectSetId}`)
}

return (
<Select
value={projectSetId}
onValueChange={(newProjectSetId) => handleProjectSetChanged(newProjectSetId)}
>
<SelectTrigger>
<SelectValue/>
</SelectTrigger>
<SelectContent>
{allProjectSets.map((projectSet) => (
<SelectItem key={projectSet.id} value={projectSet.id.toString()}>
{projectSet.name}
</SelectItem>
))}
</SelectContent>
</Select>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
ProjectSetSelect,
type ProjectSetSelectProps, SidebarProjectList,
} from "."
import {Text} from "@/components/ui/text"

type ProjectSetSidebarProps = ProjectSetSelectProps

export const ProjectSetSidebar = ({allProjectSets}: ProjectSetSidebarProps) => {
return (
<div className="max-w-96 min-w-60">
<div className="flex flex-col w-full">
<div>
<Text element="h5" as="h5" className="pb-2">
Project Set
</Text>
<ProjectSetSelect allProjectSets={allProjectSets}/>
</div>
</div>
<div className="mt-4 w-full">
<Text element="h5" as="h5" className="pb-2">
Projects
</Text>
<SidebarProjectList/>
</div>
</div>)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use client'

import {
useProjectsContext,
useProjectSearchContext,
} from "../(hooks)"
import {SearchBar} from "@/components/SearchBar"
import {Button} from "@/components/ui/button"

export const SidebarProjectList = () => {
const {displayProjects, currentProject, setCurrentProject} = useProjectsContext()
const {searchText, setSearchText} = useProjectSearchContext()

return (
<>
<SearchBar
className="ml-0"
placeholder="Search Projects"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
<div className="flex flex-col w-full mt-2 gap-1">
{currentProject && displayProjects.map((project) => (
<Button
className="justify-start"
variant={project.id === currentProject.id ? "secondary" : "ghost"}
key={project.id}
onClick={() => setCurrentProject(project)}
>
{project.name}
</Button>
))}
</div>
</>
)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's not do this imo

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export {ProjectSetSelect, type ProjectSetSelectProps} from './ProjectSetSelect'
export {SidebarProjectList} from './SidebarProjectList'
export {ProjectSetSidebar} from './ProjectSetSidebar'
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {useProjectSearchContext, ProjectSearchProvider} from './useProjectSearch'
export {useProjectsContext, ProjectsProvider} from './useProjects'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idk why this is a context tbh? This seems like straight internal behaviour of the search sidebar on its own and really didn't need to be a context. Nothing else consumes this and I don't think anything ever will

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use client'

import {createContext, useContext, useEffect, useState, type PropsWithChildren, type FC} from "react"
import {useProjectsContext} from "@/app/(app)/course/[courseId]/project-sets/[projectSetId]/(hooks)/useProjects"

type ProjectSearchContextType = {
searchText: string
setSearchText: (searchText: string) => void
}

const ProjectSearchContext = createContext<ProjectSearchContextType>({
searchText: '',
setSearchText: () => {},
})

const useProjectSearch = () => {
const {projects: allProjects, setDisplayProjects, currentProject, setCurrentProject} = useProjectsContext()
const [searchText, setSearchText] = useState('')

useEffect(() => {
const filteredProjects = allProjects.filter(project => project.name.toLowerCase().includes(searchText.toLowerCase()))
setDisplayProjects(filteredProjects)
setSearchText(searchText)

// If the current project is not in the filtered projects, set the current project to the first filtered project
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idea;

I think a nice UX flow would be to have the right side of the page render a nice empty state whenever the search changes. Then make it possible to set null for the current project. Then whenever the search changes we set the current project to null.

const isCurrentProjectInFilteredProjects = !!currentProject && filteredProjects.some(project => project.id === currentProject.id)
if (!isCurrentProjectInFilteredProjects && filteredProjects.length > 0) {
setCurrentProject(filteredProjects[0])
}
}, [allProjects, currentProject, searchText, setCurrentProject, setDisplayProjects])

return {
searchText,
setSearchText,
}
}

export const ProjectSearchProvider: FC<PropsWithChildren> = ({children}) => {
const projectSearch = useProjectSearch()

return (
<ProjectSearchContext.Provider value={projectSearch}>
{children}
</ProjectSearchContext.Provider>
)
}

export const useProjectSearchContext = () => useContext(ProjectSearchContext)
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use client'

import {createContext, useContext, useEffect, useState, type PropsWithChildren, type FC} from "react"
import {useParams} from "next/navigation"
import {type Project} from "@/_temp_types/projects"
import {toast} from "@/hooks/use-toast"

type ProjectSetContextType = {
projectSetId: string
projects: Project[]
displayProjects: Project[]
setDisplayProjects: (projects: Project[]) => void
currentProject: Project | undefined
setCurrentProject: (project: Project) => void
}

const ProjectSetContext = createContext<ProjectSetContextType>({
projectSetId: "",
projects: [],
displayProjects: [],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still reviewing but have my doubts if we need the logic of displayProjects to exist at the provider level. I kinda think the left sidebar could handle this logic without anything else under the context needing to be aware of it

setDisplayProjects: () => {},
currentProject: undefined,
setCurrentProject: () => {},
})

const useProjects = () => {
const {projectSetId} = useParams<{ projectSetId: string }>()
const [projects, setProjects] = useState<Project[]>([])
const [displayProjects, setDisplayProjects] = useState<Project[]>([])
const [currentProject, setCurrentProject] = useState<Project>()

useEffect(() => {
const fetchProjects = async () => {
const projectsURL = new URL(`/api/v1/teamset-templates/${projectSetId}/team-templates`, process.env.NEXT_PUBLIC_BACKEND_URL)
const projectsResponse = await fetch(projectsURL)
const projects = await projectsResponse.json()
setProjects(projects)
}
fetchProjects().catch((e) => {
toast({
title: "There was an error fetching the projects.",
variant: "destructive",
})
console.error(e)
})
}, [projectSetId])

return {
projectSetId,
projects,
displayProjects,
setDisplayProjects,
currentProject,
setCurrentProject,
}
}

export const ProjectsProvider: FC<PropsWithChildren> = ({children}) => {
const projectSet = useProjects()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit;

do we really gain value fro making an entire other hook just so we can use it in one place and set all the values in the provider. Seems like we could just do all the useState and useEffect stuff without putting it in another hook


return (
<ProjectSetContext.Provider value={projectSet}>
{children}
</ProjectSetContext.Provider>
)
}

export const useProjectsContext = () => useContext(ProjectSetContext)
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {toast} from "@/hooks/use-toast"
import {type ApiTeamSetTemplate} from "@/_temp_types/api/teams"
import PageView from "@/components/views/Page"
import {
ProjectSetSidebar,
} from "./(components)"
import {ProjectsProvider, ProjectSearchProvider} from "./(hooks)"

const getOutlinedProjectSetsData = async (): Promise<ApiTeamSetTemplate[]> => {
const projectSetsURL = new URL('/api/v1/teamset-templates', process.env.NEXT_PUBLIC_BACKEND_URL)
const response = await fetch(projectSetsURL)
if (!response.ok) {
const errMsg = "Unable to fetch project sets from API."
toast({
title: errMsg,
variant: "destructive",
})
throw new Error(errMsg)
}
return await response.json()
}

type ProjectPageType = {
params: {
courseId: string,
projectSetId: string,
},
}

const ProjectSetPage = async ({params: {courseId, projectSetId}}: ProjectPageType) => {
return <PageView
title="Project Sets"
breadcrumbs={[
{title: "Home", href: `/course/${courseId}`},
{title: "Project Sets", href: `/course/${courseId}/project-sets`},
{title: `Project Set Detail`, href: `/course/${courseId}/project-sets/${projectSetId}`},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Project Set Detail" provides no value as a breadcrumb item.
At minimum, it should just be the name of the project set

]}
>
<ProjectsProvider>
<ProjectSearchProvider>
<ProjectSetSidebar allProjectSets={await getOutlinedProjectSetsData()}/>
</ProjectSearchProvider>
</ProjectsProvider>
</PageView>
}

export default ProjectSetPage
17 changes: 12 additions & 5 deletions src/app/(app)/course/[courseId]/project-sets/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {redirect} from "next/navigation"
import {columns} from "./columns"

const getProjectSetsData = async (): Promise<ProjectSet[]> => {
const response = await fetch(process.env.NEXT_PUBLIC_BACKEND_URL + "/api/v1/teamset-templates")
const projectSetsURL = new URL('/api/v1/teamset-templates', process.env.NEXT_PUBLIC_BACKEND_URL) + "?detailed=true"
const response = await fetch(projectSetsURL)
if (!response.ok) {
throw new Error("Unable to fetch project sets from API.")
}
Expand All @@ -20,18 +21,24 @@ const getProjectSetsData = async (): Promise<ProjectSet[]> => {
}) as ProjectSet,)
}

async function ProjectSetsPage() {
type ProjectPageType = {
params: {
courseId: string,
},
}

async function ProjectSetsPage({params: {courseId}}: ProjectPageType) {
const handleRowClick = async (row: ProjectSet) => {
"use server"
redirect(`project-sets/${row.id}`)
redirect(`/course/${courseId}/project-sets/${row.id}`)
}

return (
<PageView
title="Project Sets"
breadcrumbs={[
{title: "Home", href: "/"},
{title: "Project Sets", href: `/project-sets`},
{title: "Home", href: `/course/${courseId}}`},
{title: "Project Sets", href: `/course/${courseId}/project-sets`},
]}
>
<DataTable<ProjectSet>
Expand Down
4 changes: 2 additions & 2 deletions src/components/SearchBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ interface SearchProps extends InputProps {}

const SearchBar = React.forwardRef<HTMLInputElement, SearchProps>(({ className, type, ...props }, ref) => {
return (
<div className="flex items-center w-full -ml-4">
<MagnifyingGlassIcon className="relative left-6" />
<div className="flex items-center w-full">
<MagnifyingGlassIcon className="absolute ml-3" />
<Input
type={type || "Search"}
ref={ref}
Expand Down