Skip to content

Commit 7a1b0ed

Browse files
authored
Merge pull request #10 from ls1intum/sidebar
Sidebar
2 parents 014c123 + b74ea8c commit 7a1b0ed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1486
-72
lines changed

clients/core/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"check-performance": "webpack --mode=production --env NODE_ENV=production --env BUNDLE_SIZE=true"
1212
},
1313
"dependencies": {
14-
"external-remotes-plugin": "^1.0.0"
14+
"external-remotes-plugin": "^1.0.0",
15+
"sha256": "^0.2.0"
1516
},
1617
"devDependencies": {
1718
"clean-webpack-plugin": "^4.0.0",

clients/core/src/App.tsx

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import React from 'react'
2-
import ErrorBoundary from './ErrorBoundary'
1+
import React, { Suspense } from 'react'
32
import { LandingPage } from './LandingPage/LandingPage'
43
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
5-
const TemplateComponent = React.lazy(() => import('template_component/App'))
4+
import { KeycloakProvider } from '@/keycloak/KeycloakProvider'
5+
import { BrowserRouter, Route, Routes } from 'react-router-dom'
6+
import { ManagementRoot } from './management/ManagementConsole'
7+
import { CourseOverview } from './Course/CourseOverview'
8+
import { TemplateRoutes } from './Router/TemplateRoutes'
69

710
const queryClient = new QueryClient({
811
defaultOptions: {
@@ -14,12 +17,35 @@ const queryClient = new QueryClient({
1417

1518
export const App = (): JSX.Element => {
1619
return (
17-
<QueryClientProvider client={queryClient}>
18-
<div>
19-
{/* add router here */}
20-
<LandingPage />
21-
</div>
22-
</QueryClientProvider>
20+
<KeycloakProvider>
21+
<QueryClientProvider client={queryClient}>
22+
<BrowserRouter>
23+
<Routes>
24+
<Route path='/' element={<LandingPage />} />
25+
<Route path='/management' element={<ManagementRoot />} />
26+
<Route path='/management/general' element={<ManagementRoot />} />
27+
<Route
28+
path='/management/course/:courseId'
29+
element={
30+
<ManagementRoot>
31+
<CourseOverview />
32+
</ManagementRoot>
33+
}
34+
/>
35+
<Route
36+
path='/management/course/:courseId/template/*'
37+
element={
38+
<ManagementRoot>
39+
<Suspense fallback={<div>Fallback</div>}>
40+
<TemplateRoutes />
41+
</Suspense>
42+
</ManagementRoot>
43+
}
44+
/>
45+
</Routes>
46+
</BrowserRouter>
47+
</QueryClientProvider>
48+
</KeycloakProvider>
2349
)
2450
}
2551

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Course } from '@/interfaces/course'
2+
import { useCourseStore } from '@/zustand/useCourseStore'
3+
import { useEffect, useState } from 'react'
4+
import { useParams } from 'react-router-dom'
5+
6+
export const CourseOverview = (): JSX.Element => {
7+
const { courses } = useCourseStore()
8+
const { courseId } = useParams<{ courseId: string }>()
9+
const [course, setCourse] = useState<Course | undefined>(undefined)
10+
11+
useEffect(() => {
12+
if (courseId) {
13+
const foundCourse = courses.find((c) => c.id === courseId)
14+
setCourse(foundCourse)
15+
}
16+
}, [courseId, courses])
17+
18+
if (!course) {
19+
return <div>Course not found</div>
20+
}
21+
22+
return (
23+
<div>
24+
<h1>{course.name}</h1>
25+
<p>{course.semester_tag}</p>
26+
{/* Weitere Kursdetails */}
27+
</div>
28+
)
29+
}

clients/core/src/LandingPage/components/CourseCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Button } from '@/components/ui/button'
22
import { Badge } from '@/components/ui/badge'
33
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
44
import { DeadlineInfo } from './DeadlineInfo'
5-
import { Course } from 'src/interfaces/open_course_applications'
5+
import { Course } from '@/interfaces/course'
66
import { Calendar } from 'lucide-react'
77
import { format } from 'date-fns'
88

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
import { Button } from '@/components/ui/button'
22
import { LogIn } from 'lucide-react'
3+
import { useNavigate } from 'react-router-dom'
34

4-
export const Header = (): JSX.Element => (
5-
<div className='flex flex-col sm:flex-row justify-between items-center mb-12 gap-4'>
6-
<div className='flex items-center space-x-4'>
7-
<img src='/ase.jpeg' alt='TUM Logo' className='h-12 w-12' />
8-
<h1 className='text-xl'>Applied Education Technologies</h1>
5+
export const Header = (): JSX.Element => {
6+
const navigate = useNavigate()
7+
return (
8+
<div className='flex flex-col sm:flex-row justify-between items-center mb-12 gap-4'>
9+
<div className='flex items-center space-x-4'>
10+
<img src='/ase.jpeg' alt='TUM Logo' className='h-12 w-12' />
11+
<h1 className='text-xl'>Applied Education Technologies</h1>
12+
</div>
13+
<Button
14+
variant='outline'
15+
className='flex items-center space-x-2'
16+
onClick={() => navigate('/management')}
17+
>
18+
<LogIn className='h-4 w-4' />
19+
<span>Chair Member Login</span>
20+
</Button>
921
</div>
10-
<Button variant='outline' className='flex items-center space-x-2'>
11-
<LogIn className='h-4 w-4' />
12-
<span>Chair Member Login</span>
13-
</Button>
14-
</div>
15-
)
22+
)
23+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert'
2+
import { AlertCircle } from 'lucide-react'
3+
import React from 'react'
4+
import { RouteObject, Routes, Route } from 'react-router-dom'
5+
import ErrorBoundary from '../ErrorBoundary'
6+
7+
export const TemplateRoutes = React.lazy(() =>
8+
import('template_component/routers')
9+
.then((module): { default: React.FC } => ({
10+
default: () => {
11+
const routes: RouteObject[] = module.default || []
12+
return (
13+
<Routes>
14+
{routes.map((route, index) => (
15+
<Route
16+
key={index}
17+
path={route.path}
18+
element={
19+
<ErrorBoundary fallback={<div>Route loading failed</div>}>
20+
{route.element}
21+
</ErrorBoundary>
22+
}
23+
/>
24+
))}
25+
</Routes>
26+
)
27+
},
28+
}))
29+
.catch((): { default: React.FC } => ({
30+
default: () => {
31+
console.warn('Failed to load template routes')
32+
return (
33+
<Alert variant='destructive'>
34+
<AlertCircle className='h-4 w-4' />
35+
<AlertTitle>Error</AlertTitle>
36+
<AlertDescription>
37+
We&apos;re sorry, but we couldn&apos;t load the template routes. Please try refreshing
38+
or contact support if the problem persists.
39+
</AlertDescription>
40+
</Alert>
41+
)
42+
},
43+
})),
44+
)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
'use client'
2+
3+
import * as React from 'react'
4+
5+
import { Sidebar } from '@/components/ui/sidebar'
6+
import { InsideSidebar } from './InsideSidebar/InsideSidebar'
7+
import { CourseSwitchSidebar } from './CourseSwitchSidebar/CourseSwitchSidebar'
8+
9+
interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
10+
onLogout: () => void
11+
}
12+
13+
export function AppSidebar({ onLogout, ...props }: AppSidebarProps): JSX.Element {
14+
return (
15+
<Sidebar
16+
collapsible='icon'
17+
className='overflow-hidden [&>[data-sidebar=sidebar]]:flex-row'
18+
{...props}
19+
>
20+
{/* This is the first sidebar */}
21+
<CourseSwitchSidebar onLogout={onLogout} />
22+
<InsideSidebar />
23+
</Sidebar>
24+
)
25+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {
2+
Sidebar,
3+
SidebarContent,
4+
SidebarFooter,
5+
SidebarGroup,
6+
SidebarGroupContent,
7+
SidebarMenu,
8+
} from '@/components/ui/sidebar'
9+
import { NavUserMenu } from './components/NavUserMenu'
10+
import { useCourseStore } from '@/zustand/useCourseStore'
11+
import SidebarHeaderComponent from './components/SidebarHeader'
12+
import { CourseSidebarItem } from './components/CourseSidebarItem'
13+
14+
interface CourseSwitchSidebarProps {
15+
onLogout: () => void
16+
}
17+
18+
export const CourseSwitchSidebar = ({ onLogout }: CourseSwitchSidebarProps): JSX.Element => {
19+
const { courses } = useCourseStore()
20+
21+
return (
22+
<Sidebar
23+
collapsible='none'
24+
className='!w-[calc(var(--sidebar-width-icon)_+_1px)] min-w-[calc(var(--sidebar-width-icon)_+_1px)] border-r'
25+
>
26+
<SidebarHeaderComponent />
27+
<SidebarContent>
28+
<SidebarGroup>
29+
<SidebarGroupContent className='px-0'>
30+
<SidebarMenu>
31+
{courses.map((course) => {
32+
return <CourseSidebarItem key={course.id} course={course} />
33+
})}
34+
</SidebarMenu>
35+
</SidebarGroupContent>
36+
</SidebarGroup>
37+
</SidebarContent>
38+
<SidebarFooter>
39+
<div className='relative flex aspect-square size-12 items-center justify-center'>
40+
<div className='flex aspect-square size-10 items-center justify-center'>
41+
<NavUserMenu onLogout={onLogout} />
42+
</div>
43+
</div>
44+
</SidebarFooter>
45+
</Sidebar>
46+
)
47+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/components/ui/sidebar'
2+
import { Course } from '@/interfaces/course'
3+
import DynamicIcon from '@/components/DynamicIcon'
4+
import { useMemo } from 'react'
5+
import { useNavigate, useParams } from 'react-router-dom'
6+
7+
// Todo move somewhere else
8+
const subtleColors = [
9+
'bg-red-100',
10+
'bg-yellow-100',
11+
'bg-green-100',
12+
'bg-blue-100',
13+
'bg-indigo-100',
14+
'bg-purple-100',
15+
'bg-pink-100',
16+
'bg-orange-100',
17+
'bg-teal-100',
18+
'bg-cyan-100',
19+
]
20+
21+
interface CourseSidebarItemProps {
22+
course: Course
23+
}
24+
25+
export const CourseSidebarItem = ({ course }: CourseSidebarItemProps): JSX.Element => {
26+
const { setOpen } = useSidebar()
27+
const navigate = useNavigate()
28+
const { courseId } = useParams<{ courseId: string }>()
29+
30+
const isActive = course.id === courseId
31+
const bgColor = course.meta_data?.['bg-color'] || subtleColors['bg-grey-100']
32+
const iconName = course.meta_data?.['icon'] || 'graduation-cap'
33+
34+
const MemoizedIcon = useMemo(() => {
35+
return (
36+
<div className='size-6'>
37+
<DynamicIcon name={iconName} />
38+
</div>
39+
)
40+
}, [iconName])
41+
42+
return (
43+
<SidebarMenuItem key={course.id}>
44+
<SidebarMenuButton
45+
size='lg'
46+
tooltip={{
47+
children: course.name,
48+
hidden: false,
49+
}}
50+
onClick={() => {
51+
setOpen(true)
52+
navigate(`/management/course/${course.id}`)
53+
}}
54+
isActive={isActive}
55+
className='min-w-12 min-h-12 p-0'
56+
>
57+
<div
58+
className={`relative flex aspect-square size-12 items-center justify-center ${
59+
isActive
60+
? 'after:absolute after:inset-0 after:rounded-lg after:border-2 after:border-primary'
61+
: ''
62+
}`}
63+
>
64+
<div
65+
className={`
66+
flex aspect-square items-center justify-center rounded-lg text-gray-800
67+
${isActive ? 'size-12' : 'size-10'}
68+
${bgColor}
69+
`}
70+
>
71+
<div className='size-6'>{MemoizedIcon}</div>
72+
</div>
73+
</div>
74+
</SidebarMenuButton>
75+
</SidebarMenuItem>
76+
)
77+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
2+
import { useAuthStore } from '@/zustand/useAuthStore'
3+
import { sha256 } from 'js-sha256'
4+
5+
const getGravatarUrl = (email) => {
6+
const hash = sha256(email.trim().toLowerCase())
7+
8+
return `https://www.gravatar.com/avatar/${hash}?d=identicon&d=404`
9+
}
10+
11+
export function NavAvatar(): JSX.Element {
12+
const { user } = useAuthStore()
13+
14+
const userName = user?.firstName + ' ' + user?.lastName || ' Unknown User'
15+
const userEmail = user?.email || 'Unknown Email'
16+
const initials = `${user?.firstName.charAt(0)}${user?.lastName.charAt(0)}` || '??'
17+
18+
return (
19+
<>
20+
<Avatar className='h-10 w-10 rounded-lg'>
21+
<AvatarImage src={getGravatarUrl(userEmail)} alt={userName} />
22+
<AvatarFallback className='rounded-lg'>{initials}</AvatarFallback>
23+
</Avatar>
24+
<div className='grid flex-1 text-left text-sm leading-tight'>
25+
<span className='truncate font-semibold'>{userName}</span>
26+
<span className='truncate text-xs'>{userEmail}</span>
27+
</div>
28+
</>
29+
)
30+
}

0 commit comments

Comments
 (0)