mirror of
https://github.com/SrIzan10/featheroom.git
synced 2026-06-06 00:56:49 +00:00
feat: intial school work viewer
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
55
app/(app)/drawer/courses/courseWork/[...ids].tsx
Normal file
55
app/(app)/drawer/courses/courseWork/[...ids].tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
5
app/(app)/drawer/courses/courseWorkMaterial/[...ids].tsx
Normal file
5
app/(app)/drawer/courses/courseWorkMaterial/[...ids].tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { useLocalSearchParams } from 'expo-router'
|
||||
|
||||
export default function CourseWorkViewer() {
|
||||
const { ids } = useLocalSearchParams() as { ids: string[] }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
23
lib/ui/components/SimpleAttachment.tsx
Normal file
23
lib/ui/components/SimpleAttachment.tsx
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user