finished assignment 3 - 100%
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
173
FastNotes/components/note-image-panel.tsx
Normal file
173
FastNotes/components/note-image-panel.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
36
FastNotes/components/upload-progress-bar.tsx
Normal file
36
FastNotes/components/upload-progress-bar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user