feat: intial school work viewer

This commit is contained in:
2024-11-17 00:36:33 +01:00
parent 6a5cf570ab
commit 518181eb6b
6 changed files with 202 additions and 62 deletions

View File

@@ -42,6 +42,20 @@ const DrawerLayout = () => {
title: 'Create Announcement',
}}
/>
<Drawer.Screen
name="courses/courseWork/[...ids]"
options={{
drawerLabel: 'Course Work Viewer',
title: 'Course Work Viewer',
}}
/>
<Drawer.Screen
name="courses/courseWorkMaterial/[...ids]"
options={{
drawerLabel: 'Course Work Material Viewer',
title: 'Course Work Material Viewer',
}}
/>
</Drawer>
</GestureHandlerRootView>
)

View File

@@ -0,0 +1,55 @@
import { useLocalSearchParams } from 'expo-router'
import { useEffect } from 'react'
import { View } from 'react-native'
import { Surface, Text } from 'react-native-paper'
import { useGetCourseWork } from '@/lib/clients/classroom'
import Loading from '@/lib/ui/components/Loading'
import SimpleAttachment from '@/lib/ui/components/SimpleAttachment'
export default function CourseWorkViewer() {
const { ids } = useLocalSearchParams() as { ids: string[] }
const courseId = ids[0]
const courseWorkId = ids[1]
const { data, isLoading } = useGetCourseWork(courseId, courseWorkId)
useEffect(() => {
console.log('get course work', data)
}, [data])
if (isLoading) return <Loading />
// TODO: Add support for non-assignment work types
if (data?.workType !== 'ASSIGNMENT')
return (
<Surface>
<Text>Non assignments are not supported temporarily.</Text>
</Surface>
)
return (
<Surface className="flex-1">
<View className="p-4">
<Text variant="headlineMedium" className="pb-3">
{data?.title}
</Text>
<Text variant="headlineSmall">Description</Text>
<Text>{data?.description}</Text>
{data?.materials && (
<>
<Text variant="headlineSmall">Attachments</Text>
{data.materials.map((material) => (
<SimpleAttachment
key={material.driveFile!.driveFile!.id}
title={material.driveFile!.driveFile!.title!}
link={material.driveFile!.driveFile!.alternateLink!}
/>
))}
</>
)}
</View>
</Surface>
)
}

View File

@@ -0,0 +1,5 @@
import { useLocalSearchParams } from 'expo-router'
export default function CourseWorkViewer() {
const { ids } = useLocalSearchParams() as { ids: string[] }
}

View File

@@ -36,24 +36,6 @@ async function getAuthToken(): Promise<string> {
return tokenPromise
}
async function fetchUserProfile(userId: string) {
const response = await fetch(
`https://classroom.googleapis.com/v1/userProfiles/${userId}`,
{
headers: {
Authorization: `Bearer ${await getAuthToken()}`,
},
},
)
if (!response.ok) {
console.log('Error fetching user profile:', response.status)
return null
}
return await response.json()
}
async function fetchApi<T>(
endpoint: string,
insideKey?: string,
@@ -76,26 +58,11 @@ async function fetchApi<T>(
}
const data = await response.json()
// this is getting out of control quickly
const key = insideKey ? (data[insideKey] ?? []) : data
const creatorProfileData = await enrichWithCreatorProfile<T>(key)
// check if the key has a creatorUserId, if so, fetch the user profile
if (Array.isArray(key)) {
const enrichedKey = await Promise.all(
key.map(async (item) => {
if (item.creatorUserId) {
const profile = await fetchUserProfile(item.creatorUserId)
return {
...item,
creator: profile,
}
}
return item
}),
)
return enrichedKey as T
}
return key
return creatorProfileData || key
}
// Query Keys
@@ -104,12 +71,27 @@ export const keys = {
all: ['courses'],
one: (id: string) => ['courses', id],
announcements: (courseId: string) => ['courses', courseId, 'announcements'],
courseWork: (courseId: string) => ['courses', courseId, 'courseWork'],
courseWorks: (courseId: string) => ['courses', courseId, 'courseWork'],
courseWork: (courseId: string, courseWorkId: string) => [
'courses',
courseId,
'courseWork',
courseWorkId,
],
courseWorkMaterials: (courseId: string) => [
'courses',
courseId,
'courseWorkMaterials',
],
courseWorkMaterial: (courseId: string, courseWorkMaterialId: string) => [
'courses',
courseId,
'courseWorkMaterials',
courseWorkMaterialId,
],
},
users: {
one: (id: string) => ['users', id],
},
}
@@ -142,7 +124,7 @@ export function useAnnouncements(courseId: string) {
export function useCourseWork(courseId: string) {
return useQuery({
queryKey: keys.courses.courseWork(courseId),
queryKey: keys.courses.courseWorks(courseId),
queryFn: () =>
fetchApi<classroom_v1.Schema$CourseWork[]>(
`/v1/courses/${courseId}/courseWork`,
@@ -186,12 +168,10 @@ async function postAnnouncement(courseId: string, text: string) {
return response.json()
}
// Mutation hook
export function usePostAnnouncement(courseId: string) {
return useMutation({
mutationFn: (text: string) => postAnnouncement(courseId, text),
onSuccess: () => {
// Invalidate announcements query to refetch
queryClient.invalidateQueries({
queryKey: keys.courses.announcements(courseId),
})
@@ -199,6 +179,16 @@ export function usePostAnnouncement(courseId: string) {
})
}
export function useGetCourseWork(courseId: string, courseWorkId: string) {
return useQuery({
queryKey: keys.courses.courseWork(courseId, courseWorkId),
queryFn: () =>
fetchApi<classroom_v1.Schema$CourseWork>(
`/v1/courses/${courseId}/courseWork/${courseWorkId}`,
),
})
}
// various api utils from now on
export function classroomDateTimeToISO(
date: classroom_v1.Schema$Date,
@@ -216,3 +206,46 @@ export function classroomDateTimeToISO(
).toISOString()
return dt
}
// TODO: refactor to cache user profiles inside the queryClient (users.one)
async function fetchUserProfile(userId: string) {
const response = await fetch(
`https://classroom.googleapis.com/v1/userProfiles/${userId}`,
{
headers: {
Authorization: `Bearer ${await getAuthToken()}`,
},
},
)
if (!response.ok) {
console.log('Error fetching user profile:', response.status)
return null
}
return await response.json()
}
async function enrichWithCreatorProfile<T>(data: any): Promise<T> {
if (Array.isArray(data)) {
const enrichedData = await Promise.all(
data.map(async (item) => {
if (item.creatorUserId) {
const profile = await fetchUserProfile(item.creatorUserId)
return {
...item,
creator: profile,
}
}
return item
}),
)
return enrichedData as T
} else if (data.creatorUserId) {
const profile = await fetchUserProfile(data.creatorUserId)
return {
...data,
creator: profile,
} as T
}
return data as T
}

View File

@@ -1,4 +1,4 @@
import { View } from 'react-native'
import { Pressable, View } from 'react-native'
import { Card, Avatar, Text } from 'react-native-paper'
import { classroomDateTimeToISO } from '@/lib/clients/classroom'
@@ -6,8 +6,10 @@ import {
CourseWorkMaterialUserProfile,
CourseWorkUserProfile,
} from '@/lib/types/Classroom'
import { useRouter } from 'expo-router'
export default function CourseWorkCard(props: Props) {
const router = useRouter()
const getIcon = () => {
if (props.isCWM) return 'file-document-outline'
return 'clipboard-text-outline'
@@ -24,27 +26,35 @@ export default function CourseWorkCard(props: Props) {
return (
<View className="m-2.5">
<Card className="border border-gray-200 dark:border-gray-700">
<Card.Title
title={props.data.title}
subtitle={!props.isCWM && getDateString()}
left={(ip) => (
<Avatar.Icon
{...ip}
icon={getIcon()}
className="bg-blue-100 dark:bg-blue-900"
/>
)}
right={(ip) =>
!props.isCWM &&
props.data.maxPoints && (
<Text {...ip} className="mr-4">
{props.data.maxPoints} points
</Text>
)
}
/>
</Card>
<Pressable
onPress={() =>
router.push(
`/drawer/courses/courseWork${props.isCWM ? 'Material' : ''}/${props.data.courseId}/${props.data.id}`,
)
}
>
<Card className="border border-gray-200 dark:border-gray-700">
<Card.Title
title={props.data.title}
subtitle={!props.isCWM && getDateString()}
left={(ip) => (
<Avatar.Icon
{...ip}
icon={getIcon()}
className="bg-blue-100 dark:bg-blue-900"
/>
)}
right={(ip) =>
!props.isCWM &&
props.data.maxPoints && (
<Text {...ip} className="mr-4">
{props.data.maxPoints} points
</Text>
)
}
/>
</Card>
</Pressable>
</View>
)
}

View File

@@ -0,0 +1,23 @@
import * as WebBrowser from 'expo-web-browser'
import { Pressable, View } from 'react-native'
import { Icon, Text } from 'react-native-paper'
export default function SimpleAttachment(props: Props) {
return (
<Pressable
onPress={async () => {
await WebBrowser.openBrowserAsync(props.link)
}}
>
<View className="flex flex-row gap-3 items-center pt-2">
<Icon source="attachment" size={20} />
<Text className="text-xl">{props.title}</Text>
</View>
</Pressable>
)
}
interface Props {
title: string
link: string
}