diff --git a/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/(components)/ProjectSetSelect.tsx b/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/(components)/ProjectSetSelect.tsx new file mode 100644 index 0000000..dc243a2 --- /dev/null +++ b/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/(components)/ProjectSetSelect.tsx @@ -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 ( + + ) +} diff --git a/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/(components)/ProjectSetSidebar.tsx b/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/(components)/ProjectSetSidebar.tsx new file mode 100644 index 0000000..0950c6c --- /dev/null +++ b/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/(components)/ProjectSetSidebar.tsx @@ -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 ( +
+
+
+ + Project Set + + +
+
+
+ + Projects + + +
+
) +} diff --git a/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/(components)/SidebarProjectList.tsx b/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/(components)/SidebarProjectList.tsx new file mode 100644 index 0000000..b505bc3 --- /dev/null +++ b/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/(components)/SidebarProjectList.tsx @@ -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 ( + <> + setSearchText(e.target.value)} + /> +
+ {currentProject && displayProjects.map((project) => ( + + ))} +
+ + ) +} diff --git a/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/(components)/index.tsx b/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/(components)/index.tsx new file mode 100644 index 0000000..876473e --- /dev/null +++ b/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/(components)/index.tsx @@ -0,0 +1,3 @@ +export {ProjectSetSelect, type ProjectSetSelectProps} from './ProjectSetSelect' +export {SidebarProjectList} from './SidebarProjectList' +export {ProjectSetSidebar} from './ProjectSetSidebar' diff --git a/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/(hooks)/index.tsx b/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/(hooks)/index.tsx new file mode 100644 index 0000000..5d5a665 --- /dev/null +++ b/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/(hooks)/index.tsx @@ -0,0 +1,2 @@ +export {useProjectSearchContext, ProjectSearchProvider} from './useProjectSearch' +export {useProjectsContext, ProjectsProvider} from './useProjects' diff --git a/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/(hooks)/useProjectSearch.tsx b/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/(hooks)/useProjectSearch.tsx new file mode 100644 index 0000000..b6eb44a --- /dev/null +++ b/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/(hooks)/useProjectSearch.tsx @@ -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({ + 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 + 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 = ({children}) => { + const projectSearch = useProjectSearch() + + return ( + + {children} + + ) +} + +export const useProjectSearchContext = () => useContext(ProjectSearchContext) diff --git a/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/(hooks)/useProjects.tsx b/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/(hooks)/useProjects.tsx new file mode 100644 index 0000000..dd3a02a --- /dev/null +++ b/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/(hooks)/useProjects.tsx @@ -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({ + projectSetId: "", + projects: [], + displayProjects: [], + setDisplayProjects: () => {}, + currentProject: undefined, + setCurrentProject: () => {}, +}) + +const useProjects = () => { + const {projectSetId} = useParams<{ projectSetId: string }>() + const [projects, setProjects] = useState([]) + const [displayProjects, setDisplayProjects] = useState([]) + const [currentProject, setCurrentProject] = useState() + + 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 = ({children}) => { + const projectSet = useProjects() + + return ( + + {children} + + ) +} + +export const useProjectsContext = () => useContext(ProjectSetContext) diff --git a/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/page.tsx b/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/page.tsx new file mode 100644 index 0000000..c6e177d --- /dev/null +++ b/src/app/(app)/course/[courseId]/project-sets/[projectSetId]/page.tsx @@ -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 => { + 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 + + + + + + +} + +export default ProjectSetPage diff --git a/src/app/(app)/course/[courseId]/project-sets/page.tsx b/src/app/(app)/course/[courseId]/project-sets/page.tsx index 6361364..0e45b24 100644 --- a/src/app/(app)/course/[courseId]/project-sets/page.tsx +++ b/src/app/(app)/course/[courseId]/project-sets/page.tsx @@ -7,7 +7,8 @@ import {redirect} from "next/navigation" import {columns} from "./columns" const getProjectSetsData = async (): Promise => { - 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.") } @@ -20,18 +21,24 @@ const getProjectSetsData = async (): Promise => { }) 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 ( diff --git a/src/components/SearchBar/index.tsx b/src/components/SearchBar/index.tsx index 7f0b8e6..251e967 100644 --- a/src/components/SearchBar/index.tsx +++ b/src/components/SearchBar/index.tsx @@ -9,8 +9,8 @@ interface SearchProps extends InputProps {} const SearchBar = React.forwardRef(({ className, type, ...props }, ref) => { return ( -
- +
+