diff --git a/package.json b/package.json
index b9e13f2..14d41a8 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
"lucide-react": "^0.306.0",
"next": "14.0.4",
"react": "^18",
+ "react-circular-progressbar": "^2.1.0",
"react-dom": "^18",
"tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7",
diff --git a/src/_temp_types/homeAttributes.ts b/src/_temp_types/homeAttributes.ts
new file mode 100644
index 0000000..94fd01d
--- /dev/null
+++ b/src/_temp_types/homeAttributes.ts
@@ -0,0 +1,8 @@
+export interface Attribute {
+ name: string;
+}
+
+export interface InfoItem {
+ label: string;
+ value: string | number;
+}
diff --git a/src/_temp_types/user.ts b/src/_temp_types/user.ts
index 00da8b1..715cc7b 100644
--- a/src/_temp_types/user.ts
+++ b/src/_temp_types/user.ts
@@ -5,7 +5,7 @@ export interface AuthUser {
last_name: string;
email: string;
is_staff: boolean;
- course_memberships: Array<{ course: number }>;
+ course_memberships: Array<{ id: number, name: string } >;
}
export interface AnonUser {
diff --git a/src/app/(app)/course/[courseId]/home/(components)/AttributesUsed.tsx b/src/app/(app)/course/[courseId]/home/(components)/AttributesUsed.tsx
new file mode 100644
index 0000000..8c7b3be
--- /dev/null
+++ b/src/app/(app)/course/[courseId]/home/(components)/AttributesUsed.tsx
@@ -0,0 +1,40 @@
+import { Attribute } from "@/_temp_types/homeAttributes"
+
+const AttributesUsed = ({ attributes }: { attributes: Attribute[] }) => {
+ const displayedAttributes = attributes.slice(0, 15)
+ const hasMoreAttributes = attributes.length > 15
+
+ return (
+
+
Attributes Used
+
+
0 ? "grid-cols-1 sm:grid-cols-4" : ""}`}>
+ {attributes.length > 0 ? (
+ <>
+ {displayedAttributes.map((attribute, index) => (
+
+ ))}
+ {hasMoreAttributes && (
+
+ )}
+ >
+ ) : (
+
+ )}
+
+
+
+ )
+}
+
+AttributesUsed.defaultProps = {
+ attributes: [],
+}
+
+export default AttributesUsed
diff --git a/src/app/(app)/course/[courseId]/home/(components)/InfoSection.tsx b/src/app/(app)/course/[courseId]/home/(components)/InfoSection.tsx
new file mode 100644
index 0000000..84f3e4f
--- /dev/null
+++ b/src/app/(app)/course/[courseId]/home/(components)/InfoSection.tsx
@@ -0,0 +1,36 @@
+import { InfoItem } from "@/_temp_types/homeAttributes"
+
+const InfoSection = ({
+ title,
+ items = [],
+}: {
+ title: string;
+ items?: InfoItem[];
+}) => (
+
+
{title}
+
+ {items.length > 0 ? (
+
+ {items.map((item, index) => (
+ -
+ {item.label}:
+ {item.value}
+
+ ))}
+
+ ) : (
+
No data available
+ )}
+
+
+)
+
+InfoSection.defaultProps = {
+ items: [],
+}
+
+export default InfoSection
diff --git a/src/app/(app)/course/[courseId]/home/(components)/OnboardingProgress.tsx b/src/app/(app)/course/[courseId]/home/(components)/OnboardingProgress.tsx
new file mode 100644
index 0000000..95ec8c3
--- /dev/null
+++ b/src/app/(app)/course/[courseId]/home/(components)/OnboardingProgress.tsx
@@ -0,0 +1,60 @@
+import CircularProgressBar from "@/components/ui/circular-progress-bar"
+
+interface OnboardingProgressProps {
+ completionPercentage: number;
+ nextStepTitle: string;
+ courseId: string | number;
+}
+
+const OnboardingProgress = ({
+ completionPercentage,
+ nextStepTitle,
+ courseId,
+}: OnboardingProgressProps) => {
+ return (
+ <>
+
+ Welcome back!{" "}
+ {completionPercentage === 100 ? (
+
+ Wanting to start a new team formation?
+
+ ) : (
+
+ Your onboarding process is incomplete...
+
+ )}
+
+
+ {completionPercentage !== 100 && (
+
+
+
+
+
+
+ You are {completionPercentage}% done with your current onboarding process.
+
+
+ Next step:
+
+ {nextStepTitle}
+
+
+
+
+ )}
+ >
+ )
+}
+
+export default OnboardingProgress
diff --git a/src/app/(app)/course/[courseId]/home/(hooks)/calculateOnboardingCompletion.tsx b/src/app/(app)/course/[courseId]/home/(hooks)/calculateOnboardingCompletion.tsx
new file mode 100644
index 0000000..2ea0cfb
--- /dev/null
+++ b/src/app/(app)/course/[courseId]/home/(hooks)/calculateOnboardingCompletion.tsx
@@ -0,0 +1,18 @@
+import { useSetupSteps } from "@/app/(app)/course/[courseId]/setup/(hooks)/useSetupSteps"
+
+export const CalculateOnboardingCompletion = () => {
+ const { steps, isLoading } = useSetupSteps()
+ const enabledSteps = steps.filter((step) => step.enabled)
+
+ const completedSteps = enabledSteps.filter((step) => step.completed)
+
+ const completionPercentage = Math.round((completedSteps.length / enabledSteps.length) * 100)
+
+ const firstIncompleteStep = enabledSteps.find((step) => !step.completed)
+
+ return {
+ completionPercentage,
+ nextStepTitle: firstIncompleteStep ? firstIncompleteStep.title : null,
+ isLoading,
+ }
+}
diff --git a/src/app/(app)/course/[courseId]/home/(hooks)/useAttributes.tsx b/src/app/(app)/course/[courseId]/home/(hooks)/useAttributes.tsx
new file mode 100644
index 0000000..afb57c4
--- /dev/null
+++ b/src/app/(app)/course/[courseId]/home/(hooks)/useAttributes.tsx
@@ -0,0 +1,46 @@
+"use client"
+
+import { useCourse } from "@/app/(app)/course/[courseId]/(hooks)/useCourse"
+import { useQuery } from "@tanstack/react-query"
+
+interface PastAttributesResponse {
+ team_set_name: string;
+ formation_date: string;
+ total_attributes_used: number;
+ attributes: { name: string }[];
+}
+
+const usePastAttributesQuery = ({ courseId }: { courseId: number }) => {
+ const attributeQuery = useQuery({
+ queryKey: [`courses/${courseId}/previous-attributes`],
+ })
+
+ return {
+ getPastAttributes: attributeQuery.refetch,
+ ...attributeQuery,
+ }
+}
+
+export const usePastAttributes = () => {
+ const { courseId } = useCourse()
+ const { data, isLoading, error, getPastAttributes } = usePastAttributesQuery({
+ courseId: Number(courseId),
+ })
+
+ const formattedData = data
+ ? {
+ ...data,
+ formation_date: new Date(data.formation_date)
+ .toISOString()
+ .split("T")[0]
+ .replace(/-/g, "/"),
+ }
+ : undefined
+
+ return {
+ data: formattedData,
+ isLoading,
+ error,
+ refetch: getPastAttributes,
+ }
+}
diff --git a/src/app/(app)/course/[courseId]/home/(hooks)/useHandleErrors.ts b/src/app/(app)/course/[courseId]/home/(hooks)/useHandleErrors.ts
new file mode 100644
index 0000000..56fd2df
--- /dev/null
+++ b/src/app/(app)/course/[courseId]/home/(hooks)/useHandleErrors.ts
@@ -0,0 +1,26 @@
+import { useToast } from "@/hooks/use-toast"
+import { useEffect } from "react"
+
+interface ErrorState {
+ totalStudentsError?: any;
+ pastAttributesError?: any;
+}
+
+export const useHandleErrors = ({ totalStudentsError, pastAttributesError }: ErrorState) => {
+ const { toast } = useToast()
+
+ useEffect(() => {
+ if (totalStudentsError) {
+ toast({
+ title: "Error fetching students",
+ description: "There was an error fetching the number of students enrolled on your LMS.",
+ })
+ }
+ if (pastAttributesError) {
+ toast({
+ title: "Error fetching attributes",
+ description: "There was an error fetching the attributes used in previous team formation.",
+ })
+ }
+ }, [totalStudentsError, pastAttributesError, toast])
+}
diff --git a/src/app/(app)/course/[courseId]/home/(hooks)/useTotalStudents.tsx b/src/app/(app)/course/[courseId]/home/(hooks)/useTotalStudents.tsx
new file mode 100644
index 0000000..3968b75
--- /dev/null
+++ b/src/app/(app)/course/[courseId]/home/(hooks)/useTotalStudents.tsx
@@ -0,0 +1,34 @@
+"use client"
+
+import { useCourse } from "@/app/(app)/course/[courseId]/(hooks)/useCourse"
+import { useQuery } from "@tanstack/react-query"
+
+const useTotalStudentsQuery = ({ courseId }: { courseId: number }) => {
+ const studentQuery = useQuery<
+ unknown,
+ unknown,
+ { total_students: number; opted_in_students: number }
+ >({
+ queryKey: [`courses/${courseId}/student-counts`],
+ })
+
+ return {
+ getTotalStudentsAsync: studentQuery.refetch,
+ ...studentQuery,
+ }
+}
+
+export const useTotalStudents = () => {
+ const { courseId } = useCourse()
+ const { data, isLoading, error, getTotalStudentsAsync } = useTotalStudentsQuery({
+ courseId: Number(courseId),
+ })
+
+ return {
+ totalStudents: data?.total_students ?? 0,
+ optedInStudents: data?.opted_in_students ?? 0,
+ isLoading,
+ error,
+ refetch: getTotalStudentsAsync,
+ }
+}
diff --git a/src/app/(app)/course/[courseId]/home/page.tsx b/src/app/(app)/course/[courseId]/home/page.tsx
new file mode 100644
index 0000000..6cc629a
--- /dev/null
+++ b/src/app/(app)/course/[courseId]/home/page.tsx
@@ -0,0 +1,56 @@
+"use client"
+
+import { formatDate } from "@/../utils/format-date"
+import { useCourse } from "@/app/(app)/course/[courseId]/(hooks)/useCourse"
+import PageView from "@/components/views/Page"
+import { ReloadIcon } from "@radix-ui/react-icons"
+import AttributesUsed from "./(components)/AttributesUsed"
+import InfoSection from "./(components)/InfoSection"
+import OnboardingProgress from "./(components)/OnboardingProgress"
+import { CalculateOnboardingCompletion } from "./(hooks)/calculateOnboardingCompletion"
+import { usePastAttributes } from "./(hooks)/useAttributes"
+import { useHandleErrors } from "./(hooks)/useHandleErrors"
+import { useTotalStudents } from "./(hooks)/useTotalStudents"
+
+const HomePage = () => {
+ const { courseId } = useCourse()
+ const { completionPercentage, nextStepTitle, isLoading: isLoadingOnboarding } = CalculateOnboardingCompletion()
+ const { totalStudents, optedInStudents, isLoading: isLoadingStudents, error: totalStudentsError } = useTotalStudents()
+ const { data: pastAttributes, isLoading: isLoadingAttributes, error: pastAttributesError } = usePastAttributes()
+ useHandleErrors({ totalStudentsError, pastAttributesError })
+
+ const isLoading = isLoadingOnboarding || isLoadingStudents || isLoadingAttributes
+
+ const signUpStats = [
+ { label: "Students Enrolled on Your LMS", value: totalStudents },
+ { label: "Total Team Formation Acceptions", value: optedInStudents },
+ ]
+
+ const previousTeamFormation = [
+ { label: "Number of Adopted Attributes", value: pastAttributes?.total_attributes_used || 0 },
+ { label: "Team Formation Date", value: pastAttributes ? formatDate(pastAttributes.formation_date) : "N/A" },
+ ]
+
+ return (
+
+ {isLoading ? (
+
+
+
+ ) : (
+ <>
+
+
+
+
+ >
+ )}
+
+ )
+}
+
+export default HomePage
diff --git a/src/app/(app)/course/[courseId]/page.tsx b/src/app/(app)/course/[courseId]/page.tsx
index becb379..d3ecb63 100644
--- a/src/app/(app)/course/[courseId]/page.tsx
+++ b/src/app/(app)/course/[courseId]/page.tsx
@@ -1,7 +1,7 @@
import { redirect } from "next/navigation"
const CourseHomepage = async ({ params }: { params: { courseId: string } }) => {
- redirect(`/course/${params.courseId}/setup`)
+ redirect(`/course/${params.courseId}/home`)
}
export default CourseHomepage
diff --git a/src/app/(app)/course/[courseId]/setup/page.tsx b/src/app/(app)/course/[courseId]/setup/page.tsx
index 30b3dd2..2d51fba 100644
--- a/src/app/(app)/course/[courseId]/setup/page.tsx
+++ b/src/app/(app)/course/[courseId]/setup/page.tsx
@@ -1,10 +1,10 @@
"use client"
-import React from "react"
-import PageView from "@/components/views/Page"
+import { useCourse } from "@/app/(app)/course/[courseId]/(hooks)/useCourse"
import { SetupStepDetailCard } from "@/app/(app)/course/[courseId]/setup/(components)/SetupStepDetailCard"
import { useSetupSteps } from "@/app/(app)/course/[courseId]/setup/(hooks)/useSetupSteps"
-import { useCourse } from "@/app/(app)/course/[courseId]/(hooks)/useCourse"
+import PageView from "@/components/views/Page"
+
const SetupPage = () => {
const { steps, addedComponents } = useSetupSteps()
@@ -13,7 +13,7 @@ const SetupPage = () => {
diff --git a/src/app/(app)/course/[courseId]/students/page.tsx b/src/app/(app)/course/[courseId]/students/page.tsx
index c0dd196..ef2f091 100644
--- a/src/app/(app)/course/[courseId]/students/page.tsx
+++ b/src/app/(app)/course/[courseId]/students/page.tsx
@@ -1,5 +1,6 @@
"use client"
+import { useCourse } from "@/app/(app)/course/[courseId]/(hooks)/useCourse"
import PageView from "@/components/views/Page"
import { useImportStudentGradebookData } from "@/hooks/use-import-student-gradebook-data"
import { useImportStudentsFromLms } from "@/hooks/use-import-students-from-lms"
@@ -15,6 +16,7 @@ export default function StudentsPage() {
}
const StudentsPageView = () => {
+ const { courseId } = useCourse()
const { refetch } = useStudents()
const {
@@ -32,7 +34,7 @@ const StudentsPageView = () => {
{
+ if (authUser?.course_memberships) {
+ setLoading(false)
+ }
+ }, [authUser])
+
+ return (
+ <>
+
+
+
+
+
+ |
+
Teamable
+
+
+
+
+ {authUser ? (
+
+
+ {authUser.username[0].toUpperCase()}
+
+
+ ) : (
+
+
+
+ )}
+
+
+ Logout
+
+
+
+
+
+
+
+
+
Your Courses
+ {loading ? (
+
+
+
+ ) : (
+
+ {authUser?.course_memberships.map((membership) => (
+ router.push(`/course/${membership.id}/home`)}
+ >
+
+
+ {membership.name}
+
+
+
+ ))}
+
+ )}
+
+
+
+
+
Your Courses
+ {loading ? (
+
+
+
+ ) : (
+
+ {authUser?.course_memberships.map((membership) => (
+ router.push(`/course/${membership.id}/home`)}
+ >
+
+
+ {membership.name}
+
+
+
+ ))}
+
+ )}
+
+
+ >
+ )
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index f463bc5..dff0144 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,6 +1,6 @@
-import { redirect } from "next/navigation"
-import { ROUTES } from "@/routes"
import { authUserQueryFn } from "@/hooks/use-auth-user-query"
+import { ROUTES } from "@/routes"
+import { redirect } from "next/navigation"
import { getTokenAuthHeaderServer } from "../../utils/auth-server"
export default async function Home() {
@@ -10,5 +10,5 @@ export default async function Home() {
if (!user) redirect(ROUTES.SIGN_UP)
if (!user.course_memberships.length) redirect(ROUTES.AUTH_ERROR)
- redirect(`/course/${user.course_memberships[0].course}/setup`)
+ redirect(`/courses`)
}
diff --git a/src/components/ui/circular-progress-bar.tsx b/src/components/ui/circular-progress-bar.tsx
new file mode 100644
index 0000000..e363047
--- /dev/null
+++ b/src/components/ui/circular-progress-bar.tsx
@@ -0,0 +1,27 @@
+"use client"
+
+import React from "react"
+import { CircularProgressbar, buildStyles } from "react-circular-progressbar"
+import "react-circular-progressbar/dist/styles.css"
+
+const progressBarStyles = buildStyles({
+ rotation: -0.5,
+ strokeLinecap: "round",
+ textSize: "24px",
+ pathColor: "#000",
+ textColor: "#000",
+ trailColor: "#e6e6e6",
+})
+
+interface CircularProgressProps {
+ value: number;
+ text: string;
+}
+
+const CircularProgressBar: React.FC = ({ value, text }) => {
+ return (
+
+ )
+}
+
+export default CircularProgressBar
diff --git a/src/hooks/use-auth-user-query.ts b/src/hooks/use-auth-user-query.ts
index 7eee83d..ea987a7 100644
--- a/src/hooks/use-auth-user-query.ts
+++ b/src/hooks/use-auth-user-query.ts
@@ -1,5 +1,5 @@
-import { useQuery } from "@tanstack/react-query"
import { AuthUser } from "@/_temp_types/user"
+import { useQuery } from "@tanstack/react-query"
import { getTokenAuthHeader } from "../../utils/auth"
export const useAuthUserQuery = () => {
@@ -25,6 +25,7 @@ export const authUserQueryFn = async ({
},
},
)
+
if (res.status === 401) return null
const data = await res.json()
if (!res.ok) throw data
diff --git a/utils/format-date.ts b/utils/format-date.ts
new file mode 100644
index 0000000..c716e74
--- /dev/null
+++ b/utils/format-date.ts
@@ -0,0 +1,2 @@
+export const formatDate = (dateString: string) =>
+ dateString.split("T")[0].replace(/-/g, "/")
diff --git a/yarn.lock b/yarn.lock
index d42542f..bd4890e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6381,6 +6381,11 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+react-circular-progressbar@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/react-circular-progressbar/-/react-circular-progressbar-2.1.0.tgz#99e5ae499c21de82223b498289e96f66adb8fa3a"
+ integrity sha512-xp4THTrod4aLpGy68FX/k1Q3nzrfHUjUe5v6FsdwXBl3YVMwgeXYQKDrku7n/D6qsJA9CuunarAboC2xCiKs1g==
+
react-dom@^18:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"