finished assignment 3 - 100%

This commit is contained in:
Christopher Sanden
2026-03-16 17:42:22 +01:00
parent a4c2d55aea
commit ca915ec8e8
33 changed files with 2869 additions and 711 deletions

View File

@@ -1,19 +0,0 @@
import Animated from 'react-native-reanimated';
export function HelloWave() {
return (
<Animated.Text
style={{
fontSize: 28,
lineHeight: 32,
marginTop: -6,
animationName: {
'50%': { transform: [{ rotate: '25deg' }] },
},
animationIterationCount: 4,
animationDuration: '300ms',
}}>
👋
</Animated.Text>
);
}

View File

@@ -0,0 +1,173 @@
import { Image } from "expo-image"
import { useState } from "react"
import { Modal, Pressable, Text, useWindowDimensions, View } from "react-native"
import { formatBytes, StagedNoteImage } from "@/src/notes/image-utils"
import { noteImagePanelStyles as styles } from "@/src/styles/app-styles"
type Palette = {
surface: string
elevated: string
text: string
mutedText: string
border: string
accent: string
destructive: string
}
type NoteImagePanelProps = {
canEdit: boolean
isBusy?: boolean
currentImageUrl?: string | null
currentImageMimeType?: string | null
currentImageSizeBytes?: number | null
stagedImage?: StagedNoteImage | null
helperText?: string | null
palette: Palette
primaryTextColor: string
onTakePhoto?: () => void
onChooseFromLibrary?: () => void
onRemoveImage?: () => void
}
export default function NoteImagePanel({
canEdit,
isBusy = false,
currentImageUrl,
currentImageMimeType,
currentImageSizeBytes,
stagedImage,
helperText,
palette,
primaryTextColor,
onTakePhoto,
onChooseFromLibrary,
onRemoveImage,
}: NoteImagePanelProps) {
const [isFullscreenOpen, setIsFullscreenOpen] = useState(false)
const { width } = useWindowDimensions()
const previewUri = stagedImage?.uri ?? currentImageUrl ?? null
const mimeType = stagedImage?.mimeType ?? currentImageMimeType ?? null
const sizeBytes = stagedImage?.fileSize ?? currentImageSizeBytes ?? null
const useStackedLayout = width < 680
return (
<View style={styles.section}>
{previewUri ? (
<View
style={[
styles.previewCard,
styles.previewLayout,
useStackedLayout ? styles.previewLayoutStacked : null,
{ borderColor: palette.border, backgroundColor: palette.elevated },
]}
>
<View style={styles.previewDetails}>
<Text style={[styles.previewMetaLabel, { color: palette.text }]}>
{stagedImage ? "Staged image" : "Saved image"}
</Text>
<Text style={[styles.previewMeta, { color: palette.mutedText }]}>
{(mimeType ?? "Unknown type").toUpperCase()}
</Text>
<Text style={[styles.previewMeta, { color: palette.mutedText }]}>
{formatBytes(sizeBytes)}
</Text>
{!stagedImage && currentImageUrl ? (
<Text selectable numberOfLines={3} style={[styles.urlText, { color: palette.mutedText }]}>
{currentImageUrl}
</Text>
) : null}
</View>
<Pressable
disabled={!previewUri}
onPress={() => {
if (previewUri) {
setIsFullscreenOpen(true)
}
}}
style={styles.previewFrame}
>
<Image
source={{ uri: previewUri }}
style={styles.previewImage}
contentFit="contain"
/>
</Pressable>
</View>
) : (
<Text style={[styles.emptyText, { color: palette.mutedText }]}>
No image attached.
</Text>
)}
{helperText ? <Text style={[styles.helperText, { color: palette.mutedText }]}>{helperText}</Text> : null}
{canEdit ? (
<View style={styles.buttonRow}>
<Pressable
disabled={isBusy}
onPress={onTakePhoto}
style={[styles.actionButton, styles.enabledButtonShadow, isBusy ? styles.disabledButton : null, { backgroundColor: palette.accent }]}
>
<Text style={[styles.actionButtonText, { color: primaryTextColor }]}>Take photo</Text>
</Pressable>
<Pressable
disabled={isBusy}
onPress={onChooseFromLibrary}
style={[
styles.secondaryButton,
isBusy ? styles.disabledButton : null,
{ borderColor: palette.border, backgroundColor: palette.elevated },
]}
>
<Text style={[styles.secondaryButtonText, { color: palette.text }]}>Choose from gallery</Text>
</Pressable>
{previewUri ? (
<Pressable
disabled={isBusy}
onPress={onRemoveImage}
style={[
styles.secondaryButton,
isBusy ? styles.disabledButton : null,
{ borderColor: palette.destructive, backgroundColor: palette.surface },
]}
>
<Text style={[styles.removeButtonText, { color: palette.destructive }]}>Remove image</Text>
</Pressable>
) : null}
</View>
) : null}
<Modal
visible={isFullscreenOpen}
animationType="fade"
transparent
onRequestClose={() => {
setIsFullscreenOpen(false)
}}
>
<View style={styles.fullscreenOverlay}>
<Pressable
style={styles.fullscreenBackdrop}
onPress={() => {
setIsFullscreenOpen(false)
}}
/>
<View style={[styles.fullscreenCard, { backgroundColor: palette.surface, borderColor: palette.border }]}>
<Pressable
onPress={() => {
setIsFullscreenOpen(false)
}}
style={[styles.closeButton, { borderColor: palette.border, backgroundColor: palette.elevated }]}
>
<Text style={[styles.closeButtonText, { color: palette.text }]}>Close</Text>
</Pressable>
{previewUri ? (
<Image source={{ uri: previewUri }} style={styles.fullscreenImage} contentFit="contain" />
) : null}
</View>
</View>
</Modal>
</View>
)
}

View File

@@ -1,5 +1,4 @@
import type { PropsWithChildren, ReactElement } from 'react';
import { StyleSheet } from 'react-native';
import Animated, {
interpolate,
useAnimatedRef,
@@ -10,6 +9,7 @@ import Animated, {
import { ThemedView } from '@/components/themed-view';
import { useColorScheme } from '@/hooks/use-color-scheme';
import { useThemeColor } from '@/hooks/use-theme-color';
import { parallaxScrollViewStyles as styles } from '@/src/styles/app-styles';
const HEADER_HEIGHT = 250;
@@ -61,19 +61,3 @@ export default function ParallaxScrollView({
</Animated.ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
height: HEADER_HEIGHT,
overflow: 'hidden',
},
content: {
flex: 1,
padding: 32,
gap: 16,
overflow: 'hidden',
},
});

View File

@@ -1,10 +1,27 @@
import { supabase } from '@/libs/supabase'
import { router } from 'expo-router'
import Constants from "expo-constants"
import React from 'react'
import { Pressable, StyleSheet, Text } from 'react-native'
import { Platform, Pressable, Text } from 'react-native'
import { useAppTheme } from '@/src/theme/AppThemeProvider'
import { signOutButtonStyles as styles } from '@/src/styles/app-styles'
async function onSignOutButtonPress() {
const {
data: { user },
} = await supabase.auth.getUser()
const isAndroidExpoGo = Platform.OS === "android" && Constants.executionEnvironment === "storeClient"
if (user?.id && !isAndroidExpoGo) {
const { unregisterPushNotifications } = await import('@/src/notifications/push-notifications')
const removed = await unregisterPushNotifications(user.id)
if (!removed) {
console.error('Failed to unregister push notifications before sign out.')
}
}
const { error } = await supabase.auth.signOut()
if (error) {
@@ -27,16 +44,3 @@ export default function SignOutButton() {
</Pressable>
)
}
const styles = StyleSheet.create({
button: {
borderWidth: 1,
borderRadius: 999,
paddingHorizontal: 14,
paddingVertical: 8,
},
text: {
fontSize: 14,
fontWeight: '600',
},
})

View File

@@ -1,6 +1,7 @@
import { StyleSheet, Text, type TextProps } from 'react-native'
import { Text, type TextProps } from 'react-native'
import { useThemeColor } from '@/hooks/use-theme-color'
import { themedTextStyles as styles } from '@/src/styles/app-styles'
export type ThemedTextProps = TextProps & {
lightColor?: string
@@ -32,29 +33,3 @@ export function ThemedText({
/>
)
}
const styles = StyleSheet.create({
default: {
fontSize: 16,
lineHeight: 24,
},
defaultSemiBold: {
fontSize: 16,
lineHeight: 24,
fontWeight: '600',
},
title: {
fontSize: 32,
fontWeight: 'bold',
lineHeight: 32,
},
subtitle: {
fontSize: 20,
fontWeight: 'bold',
},
link: {
lineHeight: 30,
fontSize: 16,
color: '#0a7ea4',
},
})

View File

@@ -1,11 +1,12 @@
import { PropsWithChildren, useState } from 'react';
import { StyleSheet, TouchableOpacity } from 'react-native';
import { TouchableOpacity } from 'react-native';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';
import { collapsibleStyles as styles } from '@/src/styles/app-styles';
export function Collapsible({ children, title }: PropsWithChildren & { title: string }) {
const [isOpen, setIsOpen] = useState(false);
@@ -31,15 +32,3 @@ export function Collapsible({ children, title }: PropsWithChildren & { title: st
</ThemedView>
);
}
const styles = StyleSheet.create({
heading: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
content: {
marginTop: 6,
marginLeft: 24,
},
});

View File

@@ -0,0 +1,36 @@
import { Text, View } from "react-native"
import { uploadProgressBarStyles as styles } from "@/src/styles/app-styles"
type Palette = {
accent: string
border: string
elevated: string
mutedText: string
text: string
}
type UploadProgressBarProps = {
progress: number
label?: string
palette: Palette
}
export default function UploadProgressBar({
progress,
label = "Uploading image...",
palette,
}: UploadProgressBarProps) {
const clampedProgress = Math.max(0, Math.min(100, progress))
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={[styles.label, { color: palette.text }]}>{label}</Text>
<Text style={[styles.percentage, { color: palette.mutedText }]}>{clampedProgress}%</Text>
</View>
<View style={[styles.track, { borderColor: palette.border, backgroundColor: palette.elevated }]}>
<View style={[styles.fill, { width: `${clampedProgress}%`, backgroundColor: palette.accent }]} />
</View>
</View>
)
}