-
Notifications
You must be signed in to change notification settings - Fork 0
Project Page Detail Part 4 #50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b7b2b76
0bb18df
ac82ab3
c292c06
e3fc9b1
26a6f2c
eee15e2
f980ac1
af4df0d
43ddcd7
b4624df
bcfdacc
f50ac77
f287dd6
9803b75
c19bf60
3d21f7d
6b4529a
324e6e5
9f5e076
ae1d23d
0f83bf6
21fad43
aebed13
6c3834b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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> | ||
| </> | ||
| ) | ||
| } |
| 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' |
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: [], | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. still reviewing but have my doubts if we need the logic of |
||
| 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() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}`}, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Project Set Detail" provides no value as a breadcrumb item. |
||
| ]} | ||
| > | ||
| <ProjectsProvider> | ||
| <ProjectSearchProvider> | ||
| <ProjectSetSidebar allProjectSets={await getOutlinedProjectSetsData()}/> | ||
| </ProjectSearchProvider> | ||
| </ProjectsProvider> | ||
| </PageView> | ||
| } | ||
|
|
||
| export default ProjectSetPage | ||
There was a problem hiding this comment.
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