feat: query classes and screen to show them

This commit is contained in:
2024-11-03 21:31:48 +01:00
parent e926345b20
commit 72a1570c55
20 changed files with 147 additions and 443 deletions

View File

@@ -20,45 +20,6 @@ const TabLayout = () => {
name="index"
options={{
title: 'Home',
headerRight: () => (
<>
<Tooltip title="Search">
<Appbar.Action
icon="magnify"
onPress={() => router.push('/search')}
/>
</Tooltip>
<Menu
statusBarHeight={48}
visible={visible}
onDismiss={() => setVisible(false)}
anchor={
<Tooltip title="Options">
<Appbar.Action
icon="dots-vertical"
onPress={() => setVisible(true)}
/>
</Tooltip>
}
>
<Menu.Item
title="Settings"
leadingIcon="cog"
onPress={() => router.push('/(tabs)/settings')}
/>
<Menu.Item
title="Stack Navigation"
leadingIcon="card-multiple-outline"
onPress={() => router.push('/modal')}
/>
<Menu.Item
title="Drawer Navigation"
leadingIcon="gesture-swipe"
onPress={() => router.push('/drawer')}
/>
</Menu>
</>
),
tabBarIcon: (props) => (
<MaterialCommunityIcons
{...props}
@@ -72,14 +33,6 @@ const TabLayout = () => {
name="settings"
options={{
title: 'Settings',
headerRight: () => (
<Tooltip title="Drawer Navigation">
<Appbar.Action
icon="gesture-swipe"
onPress={() => router.push('/drawer')}
/>
</Tooltip>
),
tabBarIcon: (props) => (
<MaterialCommunityIcons
{...props}

View File

@@ -1,12 +1,28 @@
import { List, Surface, Text } from 'react-native-paper'
import { useCourses } from '@/lib/clients/classroom'
import { View } from 'react-native'
import React from 'react'
import { Surface } from 'react-native-paper'
import { Link } from 'expo-router'
import Loading from '@/lib/ui/components/Loading'
import { ScreenInfo, styles } from '@/lib/ui'
function TabsHome() {
const { data, isLoading } = useCourses()
const TabsHome = () => (
<Surface style={styles.screen}>
<ScreenInfo title="Home" path="app/(tabs)/index.tsx" />
</Surface>
)
if (isLoading) {
return <Loading />
}
return (
<Surface className="flex-1">
<Text>My Courses</Text>
{data?.courses.map((course) => (
<Link href={`/drawer/courses/${course.id}`} key={course.id}>
{course.name}
</Link>
))}
</Surface>
)
}
export default TabsHome

View File

@@ -75,7 +75,6 @@ const RootLayoutNav = () => {
>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="drawer" options={{ headerShown: false }} />
<Stack.Screen name="search" options={{ title: 'Search' }} />
<Stack.Screen
name="modal"
options={{ title: 'Modal', presentation: 'modal' }}

View File

@@ -1,14 +1,11 @@
import { router } from 'expo-router'
import { Drawer } from 'expo-router/drawer'
import React from 'react'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { Appbar, Menu, Tooltip, useTheme } from 'react-native-paper'
import { useTheme } from 'react-native-paper'
import { DrawerContent, DrawerHeader } from '@/lib/ui'
import { DrawerContent, DrawerHeader } from '../../../lib/ui/components'
const DrawerLayout = () => {
const theme = useTheme()
const [visible, setVisible] = React.useState(false)
return (
<GestureHandlerRootView style={{ flex: 1 }}>
@@ -32,64 +29,10 @@ const DrawerLayout = () => {
}}
>
<Drawer.Screen
name="index"
name="courses/[id]"
options={{
drawerLabel: 'Home',
title: 'Home',
headerRight: () => (
<>
<Tooltip title="Search">
<Appbar.Action
icon="magnify"
onPress={() => router.push('/search')}
/>
</Tooltip>
<Menu
statusBarHeight={48}
visible={visible}
onDismiss={() => setVisible(false)}
anchor={
<Tooltip title="Options">
<Appbar.Action
icon="dots-vertical"
onPress={() => setVisible(true)}
/>
</Tooltip>
}
>
<Menu.Item
title="Settings"
leadingIcon="cog"
onPress={() => router.push('/drawer/settings')}
/>
<Menu.Item
title="Stack Navigation"
leadingIcon="card-multiple-outline"
onPress={() => router.push('/modal')}
/>
<Menu.Item
title="Drawer Navigation"
leadingIcon="gesture-swipe"
onPress={() => router.push('/drawer')}
/>
</Menu>
</>
),
}}
/>
<Drawer.Screen
name="settings"
options={{
drawerLabel: 'Settings',
title: 'Settings',
headerRight: () => (
<Tooltip title="Stack Navigation">
<Appbar.Action
icon="card-multiple-outline"
onPress={() => router.push('/modal')}
/>
</Tooltip>
),
drawerLabel: 'Courses',
title: 'Courses',
}}
/>
</Drawer>

View File

@@ -0,0 +1,20 @@
import { useCourse } from '@/lib/clients/classroom'
import Loading from '@/lib/ui/components/Loading'
import { useLocalSearchParams } from 'expo-router'
import { Surface, Text } from 'react-native-paper'
export default function Courses() {
const { id } = useLocalSearchParams() as { id: string }
const { data: course, isLoading } = useCourse(id)
if (isLoading) {
return <Loading />
}
return (
<Surface className="flex-1">
<Text>hi this is class with name {course?.name}</Text>
</Surface>
)
}

View File

@@ -0,0 +1,5 @@
import { Text } from 'react-native-paper'
export default function Index() {
return <Text>nuh uh</Text>
}

View File

@@ -1,12 +0,0 @@
import React from 'react'
import { Surface } from 'react-native-paper'
import { ScreenInfo, styles } from '@/lib/ui'
const DrawerHome = () => (
<Surface style={styles.screen}>
<ScreenInfo title="Home" path="app/drawer/index.tsx" />
</Surface>
)
export default DrawerHome

View File

@@ -1,254 +0,0 @@
import * as SecureStore from 'expo-secure-store'
import React from 'react'
import { Platform, useColorScheme } from 'react-native'
import {
Surface,
List,
Menu,
Button,
IconButton,
Snackbar,
Icon,
} from 'react-native-paper'
import { Color, Setting } from '@/lib/types'
import { Colors, LoadingIndicator, ScreenInfo, styles } from '@/lib/ui'
const Settings = () => {
const colorScheme = useColorScheme()
const [loading, setLoading] = React.useState<boolean>(false)
const [message, setMessage] = React.useState({ visible: false, content: '' })
const [settings, setSettings] = React.useState<Setting>({
color: 'default',
theme: 'auto',
})
const [display, setDisplay] = React.useState({
color: false,
theme: false,
})
React.useEffect(() => {
setLoading(true)
if (Platform.OS !== 'web') {
SecureStore.getItemAsync('settings')
.then((result) =>
setSettings(JSON.parse(result ?? JSON.stringify(settings))),
)
.catch((res) =>
setMessage({
visible: true,
content: res.message,
}),
)
}
setLoading(false)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const themeColors =
Colors[
settings.theme === 'auto' ? (colorScheme ?? 'light') : settings.theme
]
return (
<Surface style={{ flex: 1 }}>
{loading ? (
<LoadingIndicator />
) : (
<Surface elevation={0}>
<List.AccordionGroup>
<List.Accordion
id="1"
title="Appearance"
left={(props) => <List.Icon {...props} icon="palette" />}
>
<List.Item
title="Mode"
description="Change Mode"
left={(props) => (
<List.Icon
{...props}
icon={
settings.theme === 'auto'
? 'theme-light-dark'
: settings.theme === 'light'
? 'weather-sunny'
: 'weather-night'
}
/>
)}
right={(props) => (
<Menu
visible={display.theme}
onDismiss={() => setDisplay({ ...display, theme: false })}
anchor={
<IconButton
{...props}
icon="pencil"
onPress={() => setDisplay({ ...display, theme: true })}
/>
}
>
<Menu.Item
title="System"
leadingIcon="theme-light-dark"
trailingIcon={
settings.theme === 'auto' ? 'check' : undefined
}
onPress={() => {
setSettings({ ...settings, theme: 'auto' })
setDisplay({ ...display, theme: false })
}}
/>
<Menu.Item
title="Light Mode"
leadingIcon="weather-sunny"
trailingIcon={
settings.theme === 'light' ? 'check' : undefined
}
onPress={() => {
setSettings({ ...settings, theme: 'light' })
setDisplay({ ...display, theme: false })
}}
/>
<Menu.Item
title="Dark Mode"
leadingIcon="weather-night"
trailingIcon={
settings.theme === 'dark' ? 'check' : undefined
}
onPress={() => {
setSettings({ ...settings, theme: 'dark' })
setDisplay({ ...display, theme: false })
}}
/>
</Menu>
)}
/>
<List.Item
title="Color"
description="Change Color"
left={(props) => (
<List.Icon
{...props}
icon="palette-swatch-variant"
color={
Colors[
settings.theme === 'auto'
? (colorScheme ?? 'light')
: settings.theme
][settings.color]?.primary
}
/>
)}
right={(props) => (
<Menu
visible={display.color}
onDismiss={() => setDisplay({ ...display, color: false })}
anchor={
<IconButton
{...props}
icon="pencil"
onPress={() => setDisplay({ ...display, color: true })}
/>
}
>
{Object.keys(Colors.light).map((color) => (
<Surface
key={color}
elevation={0}
style={{
width: '100%',
flexDirection: 'row',
alignItems: 'center',
}}
>
<Surface
elevation={0}
style={{
padding: 4,
marginLeft: 8,
borderRadius: 16,
backgroundColor:
color !== settings.color
? undefined
: themeColors[color]?.primary,
}}
>
<Icon
size={24}
source="palette"
color={
color !== settings.color
? themeColors[color as Color]?.primary
: themeColors[color].onPrimary
}
/>
</Surface>
<Menu.Item
key={color}
title={color}
onPress={() => {
setSettings({
...settings,
color: color as Color,
})
setDisplay({ ...display, color: false })
}}
/>
</Surface>
))}
</Menu>
)}
/>
</List.Accordion>
</List.AccordionGroup>
</Surface>
)}
<Surface elevation={0} style={styles.screen}>
<ScreenInfo title="Settings" path="app/(tabs)/settings.tsx" />
</Surface>
<Button
mode="contained"
style={{ margin: 16 }}
onPress={() =>
Platform.OS !== 'web'
? SecureStore.setItemAsync('settings', JSON.stringify(settings))
.then(() =>
setMessage({
visible: true,
content: 'Please restart the app to apply changes.',
}),
)
.catch((res) =>
setMessage({
visible: true,
content: res.message,
}),
)
: setMessage({
visible: true,
content: 'This feature is not available on the web.',
})
}
>
Save
</Button>
<Snackbar
visible={message.visible}
onDismiss={() => setMessage({ ...message, visible: false })}
onIconPress={() => setMessage({ ...message, visible: false })}
>
{message.content}
</Snackbar>
</Surface>
)
}
export default Settings

View File

@@ -1,38 +0,0 @@
import React from 'react'
import { Searchbar, Surface } from 'react-native-paper'
import { ScreenInfo, styles } from '@/lib/ui'
const Search = () => {
const [query, setQuery] = React.useState('')
const [loading, setLoading] = React.useState(false)
// Search logic
React.useEffect(() => {
if (query !== '') {
setLoading(true)
}
setTimeout(() => {
setLoading(false)
}, 1000)
}, [query])
return (
<Surface style={{ flex: 1, gap: 16 }}>
<Searchbar
value={query}
loading={loading}
onChangeText={(v) => setQuery(v)}
placeholder="Type here to search..."
style={{ marginTop: 16, marginHorizontal: 16 }}
/>
<Surface style={styles.screen}>
<ScreenInfo title="Search" path="app/search.tsx" />
</Surface>
</Surface>
)
}
export default Search

View File

@@ -8,6 +8,9 @@ import { Setting } from '@/lib/types'
import { Themes } from '@/lib/ui'
import '../global.css'
import { QueryClientProvider } from '@tanstack/react-query'
import { queryClient } from '@/lib/clients/classroom'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
export default function Root() {
const colorScheme = useColorScheme()
@@ -39,9 +42,11 @@ export default function Root() {
][settings.color]
}
>
<AuthProvider>
<Slot />
</AuthProvider>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<Slot />
</AuthProvider>
</QueryClientProvider>
</PaperProvider>
)
}

View File

@@ -2,6 +2,7 @@ import { useRouter } from 'expo-router'
import { useEffect } from 'react'
import { useColorScheme, View } from 'react-native'
import { Button, Text } from 'react-native-paper'
import { verifyInstallation } from 'nativewind'
import { useAuth } from '@/lib/providers/auth'
@@ -9,6 +10,7 @@ function Login() {
const { signIn, user } = useAuth()
const router = useRouter()
const colorScheme = useColorScheme()
verifyInstallation()
useEffect(() => {
if (user) {

View File

@@ -1,4 +1,4 @@
module.exports = function (api: { cache: (arg0: boolean) => void }) {
module.exports = function (api) {
api.cache(true)
return {
presets: [

BIN
bun.lockb

Binary file not shown.

67
lib/clients/classroom.ts Normal file
View File

@@ -0,0 +1,67 @@
/* eslint-disable no-throw-literal */
import { type classroom_v1 } from '@googleapis/classroom'
import { GoogleSignin } from '@react-native-google-signin/google-signin'
import { QueryClient, useQuery } from '@tanstack/react-query'
type ApiError = {
message: string
status: number
}
// Query Client
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 minute
retry: 1,
},
},
})
// API Functions
const BASE_URL = 'https://classroom.googleapis.com'
async function fetchApi<T>(
endpoint: string,
options?: RequestInit,
): Promise<T> {
const token = (await GoogleSignin.getTokens()).accessToken
const response = await fetch(`${BASE_URL}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options?.headers,
},
})
if (!response.ok) {
throw { message: response.statusText, status: response.status } as ApiError
}
return response.json()
}
// Query Keys
export const keys = {
courses: {
all: ['courses'],
one: (id: string) => ['courses', id],
},
}
// Hooks
export function useCourses() {
return useQuery({
queryKey: keys.courses.all,
queryFn: () =>
fetchApi<{ courses: classroom_v1.Schema$Course[] }>('/v1/courses'),
})
}
export function useCourse(id: string) {
return useQuery({
queryKey: keys.courses.one(id),
queryFn: () => fetchApi<classroom_v1.Schema$Course>(`/v1/courses/${id}`),
})
}

View File

@@ -14,18 +14,6 @@ const DrawerContent = (props: DrawerContentProps) => (
icon="arrow-left"
onPress={() => router.replace('/')}
/>
<Drawer.Item
label="Home"
icon="home"
active={props.navProps.state.index === 0}
onPress={() => router.push('/drawer')}
/>
<Drawer.Item
label="Settings"
icon="cog"
active={props.navProps.state.index === 2}
onPress={() => router.push('/drawer/settings')}
/>
</Drawer.Section>
)

View File

@@ -0,0 +1,9 @@
import { ActivityIndicator, Surface } from 'react-native-paper'
export default function Loading() {
return (
<Surface className="flex-1 h-screen flex justify-center items-center">
<ActivityIndicator size="large" />
</Surface>
)
}

View File

@@ -2,6 +2,6 @@ const { getDefaultConfig } = require('expo/metro-config')
const { withNativeWind } = require('nativewind/metro')
// eslint-disable-next-line no-undef
const config = getDefaultConfig(__dirname)
const config = getDefaultConfig(__dirname, { isCSSEnabled: true })
module.exports = withNativeWind(config, { input: './global.css' })

View File

@@ -21,11 +21,13 @@
"@expo-google-fonts/jetbrains-mono": "^0.2.3",
"@expo-google-fonts/noto-sans": "^0.2.3",
"@expo/vector-icons": "^14.0.0",
"@googleapis/classroom": "^4.4.0",
"@react-native-google-signin/google-signin": "^13.1.0",
"@react-navigation/drawer": "^6.6.15",
"@react-navigation/native": "^6.0.2",
"@shopify/flash-list": "1.6.4",
"@shopify/react-native-skia": "1.2.3",
"@tanstack/react-query": "^5.59.16",
"expo": "~51.0.31",
"expo-font": "~12.0.5",
"expo-image": "~1.13.0",

View File

@@ -1,10 +1,10 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
// NOTE: Update this to include the paths to all of your component files.
content: ["./app/**/*.{js,jsx,ts,tsx}", "./lib/**/*.{js,jsx,ts,tsx}"],
presets: [require("nativewind/preset")],
content: ['./app/**/*.{js,jsx,ts,tsx}', './lib/**/*.{js,jsx,ts,tsx}'],
presets: [require('nativewind/preset')],
theme: {
extend: {},
},
plugins: [],
}
}

View File

@@ -14,7 +14,6 @@
"**/*.tsx",
".expo/types/**/*.ts",
"expo-env.d.ts",
"app/(app)/plushtml.old",
"nativewind-env.d.ts"
, "babel.config.js" ]
, "babel.config.js", "app/(app)/drawer/_layout.tsx" ]
}