diff --git a/src/components/Global/MissingItem.tsx b/src/components/Global/MissingItem.tsx index 8f99b7954..c55b2ad25 100644 --- a/src/components/Global/MissingItem.tsx +++ b/src/components/Global/MissingItem.tsx @@ -1,10 +1,12 @@ import Reanimated, { AnimatedStyle, EntryExitTransition, FadeInUp, FadeOutDown, LinearTransition } from "react-native-reanimated"; import { type StyleProp, Text, type ViewStyle } from "react-native"; import { NativeText } from "./NativeComponents"; +import AnimatedEmoji from "../Grades/AnimatedEmoji"; interface MissingItemProps { style?: StyleProp>>; - emoji: string; + emoji?: string; + animatedEmoji?: boolean; title: string; description: string; entering?: EntryExitTransition; @@ -14,6 +16,7 @@ interface MissingItemProps { const MissingItem: React.FC = ({ style, emoji, + animatedEmoji, title, description, entering, @@ -31,9 +34,13 @@ const MissingItem: React.FC = ({ entering={entering ? entering : FadeInUp} exiting={exiting ? exiting : FadeOutDown} > - - {emoji} - + {!animatedEmoji ? ( + + {emoji} + + ) : ( + + )} {title} diff --git a/src/components/Grades/AnimatedEmoji.tsx b/src/components/Grades/AnimatedEmoji.tsx new file mode 100644 index 000000000..7e12350d4 --- /dev/null +++ b/src/components/Grades/AnimatedEmoji.tsx @@ -0,0 +1,93 @@ +import React, { useEffect, useState } from "react"; +import { View, Text } from "react-native"; +import Animated, { + useAnimatedStyle, + useSharedValue, + withTiming, + withSpring, + withSequence, + Easing, +} from "react-native-reanimated"; + +interface AnimatedEmojiProps { + initialScale?: number; + size?: number; // Taille de la police +} + +const AnimatedEmoji: React.FC = ({ initialScale = 1, size = 20 }) => { + const scale = useSharedValue(initialScale); + const opacity = useSharedValue(1); + const emojis = ["😍", "🙄", "😭", "🥳", "😱", "😳", "🤓", "🤡", "🤯", "😨", "🤔", "🫠"]; + const [currentEmoji, setCurrentEmoji] = useState(emojis[0]); + + const animatedStyle = useAnimatedStyle(() => { + return { + transform: [{ scale: scale.value }], + opacity: opacity.value, + }; + }); + + const changeEmoji = () => { + scale.value = withSequence( + withSpring(initialScale * 0.5, { + damping: 10, + stiffness: 100, + }), + withSpring(initialScale, { + damping: 12, + stiffness: 200, + }) + ); + + opacity.value = withSequence( + withTiming(0, { + duration: 100, + easing: Easing.inOut(Easing.ease), + }), + withTiming(1, { + duration: 200, + easing: Easing.inOut(Easing.ease), + }) + ); + + setTimeout(() => { + const nextIndex = (emojis.indexOf(currentEmoji) + 1) % emojis.length; + setCurrentEmoji(emojis[nextIndex]); + }, 100); + }; + + useEffect(() => { + const interval = setInterval(() => { + changeEmoji(); + }, 2000); + + return () => clearInterval(interval); + }, [currentEmoji]); + + return ( + + + {currentEmoji} + + + ); +}; + +export default AnimatedEmoji; diff --git a/src/components/Grades/GradeModal.tsx b/src/components/Grades/GradeModal.tsx new file mode 100644 index 000000000..a022b3822 --- /dev/null +++ b/src/components/Grades/GradeModal.tsx @@ -0,0 +1,238 @@ +import React from "react"; +import { + Modal, + View, + Image, + TouchableOpacity, + Text, + Platform, + Alert +} from "react-native"; +import { Download, Trash, Maximize2, Share, Delete } from "lucide-react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { BlurView } from "expo-blur"; +import { ScrollView } from "react-native-gesture-handler"; +import * as Sharing from "expo-sharing"; +import * as FileSystem from "expo-file-system"; +import * as MediaLibrary from "expo-media-library"; +interface GradeModalProps { + isVisible: boolean; + imageBase64: string; + onClose: () => void; + DeleteGrade: () => void; +} + +const GradeModal: React.FC = ({ + isVisible, + imageBase64, + onClose, + DeleteGrade, +}) => { + const insets = useSafeAreaInsets(); + + const shareImage = async () => { + try { + const fileUri = FileSystem.cacheDirectory + "image.jpg"; + await FileSystem.writeAsStringAsync(fileUri, imageBase64, { encoding: FileSystem.EncodingType.Base64 }); + await Sharing.shareAsync(fileUri); + } catch (error) { + console.error("Failed to share image:", error); + } + }; + + const saveimage = async () => { + try { + const fileUri = FileSystem.cacheDirectory + "image.jpg"; + await FileSystem.writeAsStringAsync(fileUri, imageBase64, { encoding: FileSystem.EncodingType.Base64 }); + const asset = await MediaLibrary.createAssetAsync(fileUri); + await MediaLibrary.createAlbumAsync("Download", asset, false); + Alert.alert("Image sauvegardée", "L'image a été sauvegardée dans votre galerie."); + } catch (error) { + console.error("Failed to save image:", error); + } + }; + + return ( + + + + + + + + + + + Télécharger + + + + + + + + Partager + + + + + + + + Supprimer + + + + + + + + Fermer + + + + + + + ); +}; + +export default GradeModal; \ No newline at end of file diff --git a/src/components/Settings/ReelGallery.tsx b/src/components/Settings/ReelGallery.tsx new file mode 100644 index 000000000..381eed1cb --- /dev/null +++ b/src/components/Settings/ReelGallery.tsx @@ -0,0 +1,172 @@ +import React, { useState } from "react"; +import { + ScrollView, + View, + TouchableOpacity, + StyleSheet, + Image, + Text, + Dimensions, +} from "react-native"; +import { useTheme } from "@react-navigation/native"; +import { useGradesStore } from "@/stores/grades"; +import GradeModal from "../Grades/GradeModal"; +import { Reel } from "@/services/shared/Reel"; + +interface ReelModalProps { + reel: Reel; + visible: boolean; + onClose: () => void; +} + +// Components +const GradeIndicator = ({ value, outOf, color }: { value: number; outOf: number; color: string }) => ( + + {value.toFixed(2)} + /{outOf} + +); + +const SubjectBadge = ({ emoji, color }: { emoji: string; color: string }) => ( + + {emoji} + +); + +const ReelThumbnail = ({ reel, onPress, width }: { reel: Reel; onPress: () => void; width: number }) => { + const { colors } = useTheme(); + + return ( + + + + + + + + ); +}; + +interface ReelGalleryProps { + reels: Reel[]; +} + +const ReelGallery = ({ reels }: ReelGalleryProps) => { + const [selectedReel, setSelectedReel] = useState(null); + const windowWidth = Dimensions.get("window").width; + const padding = 40; + const gap = 8; + const numColumns = 2; + + const itemWidth = (Math.min(500, windowWidth) - (padding * 2) - (gap * (numColumns - 1))) / numColumns; + + const deleteReel = (reelId: string) => { + useGradesStore.setState((store) => { + const updatedReels = { ...store.reels }; + delete updatedReels[reelId]; + return { reels: updatedReels }; + }); + setSelectedReel(null); + }; + + return ( + + + + {reels.map((reel) => ( + setSelectedReel(reel)} + /> + ))} + + + + {selectedReel && ( + setSelectedReel(null)} + DeleteGrade={() => deleteReel(selectedReel.id)} + /> + )} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: "center", + paddingHorizontal: 16, + paddingVertical: 16, + }, + galleryContent: { + width: "100%", + maxWidth: 500, + flexDirection: "row", + flexWrap: "wrap", + justifyContent: "flex-start", + alignContent: "flex-start", + }, + item: { + borderRadius: 12, + overflow: "hidden", + }, + thumbnail: { + width: "100%", + height: "100%", + transform: [{ scaleX: -1 }], + }, + infoContainer: { + position: "absolute", + bottom: 10, + left: 20, + right: 20, + padding: 5, + borderRadius: 100, + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + }, + subjectBadge: { + borderRadius: 100, + padding: 5, + }, + emojiText: { + fontSize: 20, + }, + scoreText: { + fontWeight: "700", + fontSize: 18, + }, + maxScoreText: { + fontWeight: "300", + }, +}); + +export default ReelGallery; \ No newline at end of file diff --git a/src/router/helpers/types.ts b/src/router/helpers/types.ts index ede29094a..f72bb2400 100644 --- a/src/router/helpers/types.ts +++ b/src/router/helpers/types.ts @@ -79,7 +79,6 @@ export type RouteParameters = { // account.index Home: undefined; HomeScreen?: { onboard: boolean }; - NoteReaction: undefined; Lessons?: { outsideNav?: boolean }; LessonsImportIcal: { @@ -101,6 +100,7 @@ export type RouteParameters = { grade: Grade; allGrades?: Grade[]; }; + GradeReaction: { grade: Grade }; Evaluation: { outsideNav?: boolean }; EvaluationDocument: { @@ -132,6 +132,7 @@ export type RouteParameters = { SettingsAddons: undefined; SettingsDevLogs: undefined; SettingsDonorsList: undefined; + SettingsReactions: undefined; SettingsApparence: undefined; Menu?: undefined; diff --git a/src/router/screens/settings/index.ts b/src/router/screens/settings/index.ts index 68a83b5cf..167f595b5 100644 --- a/src/router/screens/settings/index.ts +++ b/src/router/screens/settings/index.ts @@ -24,6 +24,7 @@ import PriceBeforeScan from "@/views/settings/ExternalAccount/PriceBeforeScan"; import SettingsFlagsInfos from "@/views/settings/SettingsFlagsInfos"; import ExternalIzlyLogin from "@/views/settings/ExternalAccount/Izly"; import IzlyActivation from "@/views/settings/ExternalAccount/IzlyActivation"; +import SettingsReactions from "@/views/settings/SettingsReactions"; import TurboselfAccountSelector from "@/views/settings/ExternalAccount/TurboselfAccountSelector"; import SettingsApparence from "@/views/settings/SettingsApparence"; import ExternalAliseLogin from "@/views/settings/ExternalAccount/Alise"; @@ -57,6 +58,9 @@ const settingsScreens = [ createScreen("SettingsSubjects", SettingsSubjects, { headerTitle: "Matières", }), + createScreen("SettingsReactions", SettingsReactions, { + headerTitle: "Mes réactions", + }), createScreen("SettingsExternalServices", SettingsExternalServices, { headerTitle: "Services externes", }), diff --git a/src/router/screens/views/index.ts b/src/router/screens/views/index.ts index 3f159c352..d2fe1374e 100644 --- a/src/router/screens/views/index.ts +++ b/src/router/screens/views/index.ts @@ -1,6 +1,5 @@ import createScreen from "@/router/helpers/create-screen"; -import NoteReaction from "@/views/account/NoteReaction"; import SettingsTabs from "@/views/settings/SettingsTabs"; import RestaurantQrCode from "@/views/account/Restaurant/Modals/QrCode"; import NewsItem from "@/views/account/News/Document"; @@ -16,13 +15,14 @@ import LessonsImportIcal from "@/views/account/Lessons/Options/LessonsImportIcal import LessonDocument from "@/views/account/Lessons/Document"; import BackgroundIUTLannion from "@/views/login/IdentityProvider/actions/BackgroundIUTLannion"; import { Platform } from "react-native"; +import GradeReaction from "@/views/account/Grades/Modals/GradeReaction"; import EvaluationDocument from "@/views/account/Evaluation/Document"; import BackgroundIdentityProvider from "@/views/login/IdentityProvider/BackgroundIdentityProvider"; import ChatDetails from "@/views/account/Chat/Modals/ChatDetails"; import ChatThemes from "@/views/account/Chat/Modals/ChatThemes"; export default [ - createScreen("NoteReaction", NoteReaction, { + createScreen("GradeReaction", GradeReaction, { headerTitle: "", headerTransparent: true, presentation: "modal", diff --git a/src/services/shared/Reel.ts b/src/services/shared/Reel.ts index 610c22459..97fc642e0 100644 --- a/src/services/shared/Reel.ts +++ b/src/services/shared/Reel.ts @@ -1,7 +1,19 @@ export interface Reel { id: string - message: string timestamp: number /** base64 encoded */ image: string + imagewithouteffect: string + + subjectdata: { + color: string, + pretty: string, + emoji: string, + } + + grade: { + value: string, + outOf: string, + coef: string, + } } \ No newline at end of file diff --git a/src/views/account/Grades/Document.tsx b/src/views/account/Grades/Document.tsx index 22d5260c7..cdd196654 100644 --- a/src/views/account/Grades/Document.tsx +++ b/src/views/account/Grades/Document.tsx @@ -6,14 +6,19 @@ import { } from "@/components/Global/NativeComponents"; import { getSubjectData } from "@/services/shared/Subject"; import { useTheme } from "@react-navigation/native"; -import React, { useEffect, useLayoutEffect, useState } from "react"; -import { Image, ScrollView, Text, View, Platform } from "react-native"; +import React, { useCallback, useEffect, useLayoutEffect, useState } from "react"; +import { Image, ScrollView, Text, View, Platform, TouchableOpacity, Modal } from "react-native"; import * as StoreReview from "expo-store-review"; import { Asterisk, Calculator, + Download, + Expand, + Maximize2, Scale, School, + SmilePlus, + Trash, UserMinus, UserPlus, Users, @@ -23,10 +28,19 @@ import type { AverageDiffGrade } from "@/utils/grades/getAverages"; import { Screen } from "@/router/helpers/types"; import InsetsBottomView from "@/components/Global/InsetsBottomView"; import AsyncStorage from "@react-native-async-storage/async-storage"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useGradesStore } from "@/stores/grades"; +import { LinearGradient } from "expo-linear-gradient"; +import AnimatedEmoji from "@/components/Grades/AnimatedEmoji"; +import GradeModal from "@/components/Grades/GradeModal"; + const GradeDocument: Screen<"GradeDocument"> = ({ route, navigation }) => { const { grade, allGrades = [] } = route.params; const theme = useTheme(); + const insets = useSafeAreaInsets(); + const [modalOpen, setModalOpen] = useState(false); + const [isReactionBeingTaken, setIsReactionBeingTaken] = useState(false); const [subjectData, setSubjectData] = useState({ color: "#888888", @@ -35,6 +49,8 @@ const GradeDocument: Screen<"GradeDocument"> = ({ route, navigation }) => { }); const [shouldShowReviewOnClose, setShouldShowReviewOnClose] = useState(false); + const currentReel = useGradesStore((state) => state.reels[grade.id]); + const reels = useGradesStore((state) => state.reels); const askForReview = async () => { StoreReview.isAvailableAsync().then((available) => { @@ -44,7 +60,6 @@ const GradeDocument: Screen<"GradeDocument"> = ({ route, navigation }) => { }); }; - // on modal closed useEffect(() => { navigation.addListener("beforeRemove", () => { if (shouldShowReviewOnClose) { @@ -123,15 +138,15 @@ const GradeDocument: Screen<"GradeDocument"> = ({ route, navigation }) => { value: "x" + grade.coefficient.toFixed(2), }, grade.outOf.value !== 20 && - !grade.student.disabled && { + !grade.student.disabled && { icon: , title: "Remis sur /20", description: "Valeur recalculée sur 20", value: - typeof grade.student.value === "number" && - typeof grade.outOf.value === "number" - ? ((grade.student.value / grade.outOf.value) * 20).toFixed(2) - : "??", + typeof grade.student.value === "number" && + typeof grade.outOf.value === "number" + ? ((grade.student.value / grade.outOf.value) * 20).toFixed(2) + : "??", bareme: "/20", }, ], @@ -158,10 +173,10 @@ const GradeDocument: Screen<"GradeDocument"> = ({ route, navigation }) => { title: "Note minimale", description: "Moins bonne note de la classe", value: - grade.min.value?.toFixed(2) && - grade.min.value.toFixed(2) !== "-1.00" - ? grade.min.value?.toFixed(2) - : "??", + grade.min.value?.toFixed(2) && + grade.min.value.toFixed(2) !== "-1.00" + ? grade.min.value?.toFixed(2) + : "??", bareme: "/" + grade.outOf.value, }, ], @@ -174,80 +189,140 @@ const GradeDocument: Screen<"GradeDocument"> = ({ route, navigation }) => { title: "Moyenne générale", description: "Impact estimé sur la moyenne générale", value: - gradeDiff.difference === undefined - ? "???" - : (gradeDiff.difference > 0 - ? "- " - : gradeDiff.difference === 0 - ? "+/- " - : "+ ") + - gradeDiff.difference.toFixed(2).replace("-", "") + - " pts", + gradeDiff.difference === undefined + ? "???" + : (gradeDiff.difference > 0 + ? "- " + : gradeDiff.difference === 0 + ? "+/- " + : "+ ") + + gradeDiff.difference.toFixed(2).replace("-", "") + + " pts", color: - gradeDiff.difference === undefined - ? void 0 - : gradeDiff.difference < 0 - ? "#4CAF50" - : gradeDiff.difference === 0 - ? theme.colors.text - : "#F44336", + gradeDiff.difference === undefined + ? void 0 + : gradeDiff.difference < 0 + ? "#4CAF50" + : gradeDiff.difference === 0 + ? theme.colors.text + : "#F44336", }, !grade.average.disabled && { icon: , title: "Moyenne de la classe", description: "Impact de la note sur la moyenne de la classe", value: - classDiff.difference === undefined - ? "???" - : (classDiff.difference > 0 - ? "- " - : gradeDiff.difference === 0 - ? "+/- " - : "+ ") + - classDiff.difference.toFixed(2).replace("-", "") + - " pts", + classDiff.difference === undefined + ? "???" + : (classDiff.difference > 0 + ? "- " + : gradeDiff.difference === 0 + ? "+/- " + : "+ ") + + classDiff.difference.toFixed(2).replace("-", "") + + " pts", }, ], }, ]; + const deleteReel = (reelId: string) => { + useGradesStore.setState((store) => { + const updatedReels = { ...store.reels }; + delete updatedReels[reelId]; + return { reels: updatedReels }; + }); + setModalOpen(false); + }; + + const handleFocus = useCallback(() => { + // Si on revient de la page de réaction et qu'on a un reel + if (currentReel && isReactionBeingTaken) { + setModalOpen(true); + setIsReactionBeingTaken(false); + } + }, [currentReel]); + + useEffect(() => { + const unsubscribe = navigation.addListener("focus", handleFocus); + return unsubscribe; + }, [navigation, handleFocus]); + return ( - - + + + setModalOpen(false)} + DeleteGrade={() => deleteReel(grade.id)} + /> + + + {currentReel ? ( + <> + + + + ) : null} + - - {Platform.OS === "ios" && + + {Platform.OS === "ios" && ( = ({ route, navigation }) => { marginVertical: 8, }} /> - } - - - - {subjectData.pretty} - - - {grade.description || "Note sans description"} - - - {new Date(grade.timestamp).toLocaleDateString("fr-FR", { - weekday: "long", - month: "long", - day: "numeric", - })} - - + )} + {!reels[grade.id] ? ( + { + setIsReactionBeingTaken(true); + navigation.navigate("GradeReaction", { grade }); + }} + > + + + RÉAGIR + + + ) : ( + setModalOpen(true)} + > + + + )} + {subjectData.pretty} + + - {grade.student.disabled ? "N. not" : grade.student.value?.toFixed(2)} + {grade.description || "Note sans description"} - /{grade.outOf.value} + {new Date(grade.timestamp).toLocaleDateString("fr-FR", { + weekday: "long", + month: "long", + day: "numeric", + })} + + + + {grade.student.disabled ? "N. not" : grade.student.value?.toFixed(2)} + + + /{grade.outOf.value} + + + + {/* Scrollable Content */} = ({ route, navigation }) => { width: "100%", }} > - - + {lists.map((list, index) => ( - {list.items.map( (item, index) => @@ -392,7 +506,7 @@ const GradeDocument: Screen<"GradeDocument"> = ({ route, navigation }) => { lineHeight: 22, fontFamily: "semibold", color: - "color" in item ? item.color : theme.colors.text, + "color" in item ? item.color : theme.colors.text, }} > {item.value} @@ -407,7 +521,6 @@ const GradeDocument: Screen<"GradeDocument"> = ({ route, navigation }) => { } > {item.title} - {item.description && ( {item.description} @@ -420,7 +533,6 @@ const GradeDocument: Screen<"GradeDocument"> = ({ route, navigation }) => { ))} - diff --git a/src/views/account/Grades/Modals/GradeReaction.tsx b/src/views/account/Grades/Modals/GradeReaction.tsx new file mode 100644 index 000000000..956bc4675 --- /dev/null +++ b/src/views/account/Grades/Modals/GradeReaction.tsx @@ -0,0 +1,323 @@ +import React, { useEffect, useLayoutEffect, useRef, useState } from "react"; +import { Text, View, StyleSheet, TouchableOpacity, Image, Alert } from "react-native"; +import { CameraView, useCameraPermissions, PermissionStatus } from "expo-camera"; +import * as MediaLibrary from "expo-media-library"; +import { X } from "lucide-react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { captureRef } from "react-native-view-shot"; +import { Screen } from "@/router/helpers/types"; +import { getSubjectData } from "@/services/shared/Subject"; +import { useGradesStore } from "@/stores/grades"; +import { Reel } from "@/services/shared/Reel"; +import PapillonSpinner from "@/components/Global/PapillonSpinner"; + +// Types +interface SubjectData { + color: string; + pretty: string; + emoji: string; +} + +interface Grade { + id: string; + student: { value: number | null }; + outOf: { value: number | null }; + coefficient: number | null; + subjectName: string; + timestamp: number; +} + +// Helper Functions +const convertToBase64 = async (uri: string): Promise => { + const response = await fetch(uri); + const blob = await response.blob(); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve((reader.result as string).split(",")[1]); + reader.onerror = reject; + reader.readAsDataURL(blob); + }); +}; + +const createReel = async ( + grade: Grade, + imageUri: string, + imageWithoutEffectUri: string +): Promise => { + const [image, imageWithoutEffect] = await Promise.all([ + convertToBase64(imageUri), + convertToBase64(imageWithoutEffectUri) + ]); + + return { + id: grade.id, + timestamp: Date.now(), + image, + imagewithouteffect: imageWithoutEffect, + subjectdata: getSubjectData(grade.subjectName), + grade: { + value: grade.student.value?.toString() ?? "", + outOf: grade.outOf.value?.toString() ?? "", + coef: grade.coefficient?.toString() ?? "", + } + }; +}; + +const GradeReaction: Screen<"GradeReaction"> = ({ navigation, route }) => { + const inset = useSafeAreaInsets(); + const [mediaLibraryPermission, requestMediaLibraryPermission] = MediaLibrary.usePermissions(); + const [cameraPermission, requestCameraPermission] = useCameraPermissions(); + const cameraRef = useRef(null); + const composerRef = useRef(null); + const [capturedImage, setCapturedImage] = useState(); + const [isLoading, setIsLoading] = useState(false); + const [grade] = useState(route.params.grade); + const [subjectData, setSubjectData] = useState({ + color: "#888888", + pretty: "Matière inconnue", + emoji: "❓", + }); + + // Setup permissions + useEffect(() => { + const setupPermissions = async () => { + if (mediaLibraryPermission?.status !== PermissionStatus.GRANTED) { + await requestMediaLibraryPermission(); + } + if (cameraPermission?.status !== PermissionStatus.GRANTED) { + await requestCameraPermission(); + } + }; + setupPermissions(); + }, [mediaLibraryPermission, cameraPermission, requestMediaLibraryPermission, requestCameraPermission]); + + // Fetch subject data + useEffect(() => { + setSubjectData(getSubjectData(grade.subjectName)); + }, [grade.subjectName]); + + useLayoutEffect(() => { + navigation.setOptions({ + headerRight: () => ( + navigation.goBack()} + > + + + ), + }); + }, [navigation]); + + const handleCapture = async () => { + if (cameraPermission?.status !== PermissionStatus.GRANTED) { + Alert.alert("Permission Error", "Camera permission not granted"); + return; + } + + try { + const photo = await cameraRef.current?.takePictureAsync({ + quality: 0.5, + skipProcessing: true, + }); + if (!photo?.uri) return; + setCapturedImage(photo.uri); + setIsLoading(true); + setTimeout(async () => { + try { + const compositeUri = await captureRef(composerRef, { + format: "png", + quality: 0.5, + }); + const reel = await createReel(grade, compositeUri, photo.uri); + useGradesStore.setState((state) => ({ + ...state, + reels: { + ...state.reels, + [grade.id]: reel + } + })); + navigation.goBack(); + } catch (error) { + console.error("Failed to save image:", error); + Alert.alert("Erreur", "Erreur lors de l'enregistrement de l'image"); + } finally { + setIsLoading(false); + } + }, 1000); + } catch (error) { + console.error("Failed to take picture:", error); + Alert.alert("Error", "Failed to capture image"); + } + }; + + return ( + + + + {capturedImage ? ( + + ) : ( + + )} + + + + {subjectData.emoji} + + + + {subjectData.pretty} + + + {new Date(grade.timestamp).toLocaleDateString()} + + + + {grade.student.value} + /{grade.outOf.value} + + + + + + {isLoading && ( + + + Enregistrement en cours... + + )} + + {!capturedImage && !isLoading && ( + + + + )} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "black", + }, + cameraContainer: { + alignSelf: "center", + width: "90%", + height: 550, + borderRadius: 16, + overflow: "hidden", + }, + logo: { + position: "absolute", + width: 100, + height: 60, + zIndex: 40, + right: 20, + }, + cameraView: { + width: "100%", + height: "100%", + }, + capturedImage: { + width: "100%", + height: "100%", + transform: [{ scaleX: -1 }], + }, + infoNoteContainer: { + position: "absolute", + bottom: 20, + left: 0, + right: 0, + alignItems: "center", + }, + infoNote: { + width: "90%", + height: 60, + backgroundColor: "#FFFFFF", + borderRadius: 30, + flexDirection: "row", + alignItems: "center", + paddingHorizontal: 20, + }, + emoji: { + fontSize: 25, + }, + emojiContainer: { + marginRight: 5, + marginLeft: -10, + backgroundColor: "#00000020", + padding: 5, + borderRadius: 20, + }, + subjectText: { + fontWeight: "600", + color: "#000000", + fontSize: 16, + maxWidth: 150, + }, + dateText: { + color: "#00000090" + }, + scoreContainer: { + marginLeft: "auto", + flexDirection: "row", + alignItems: "baseline", + }, + scoreText: { + fontWeight: "700", + color: "#000000", + fontSize: 20 + }, + maxScoreText: { + fontWeight: "300", + color: "#000000" + }, + captureButton: { + borderRadius: 37.5, + borderColor: "#FFFFFF", + borderWidth: 2, + width: 75, + height: 75, + justifyContent: "center", + alignItems: "center", + alignSelf: "center", + marginTop: 30, + }, + captureButtonInner: { + borderRadius: 30, + backgroundColor: "#FFFFFF", + width: 60, + height: 60, + }, + headerRight: { + padding: 5, + borderRadius: 50, + marginRight: 5, + backgroundColor: "#FFFFFF20", + }, + loadingContainer: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + justifyContent: "center", + alignItems: "center", + backgroundColor: "rgba(0, 0, 0, 0.9)", + zIndex: 50, + }, + loadingText: { + color: "#FFFFFF", + marginTop: 10, + fontSize: 16, + }, +}); + +export default GradeReaction; \ No newline at end of file diff --git a/src/views/account/NoteReaction.tsx b/src/views/account/NoteReaction.tsx deleted file mode 100644 index 8e2b8af7e..000000000 --- a/src/views/account/NoteReaction.tsx +++ /dev/null @@ -1,298 +0,0 @@ -import React, { useEffect, useRef, useState } from "react"; -import { - Text, - View, - StyleSheet, - TouchableOpacity, - Image, - Alert, -} from "react-native"; -import { CameraView, useCameraPermissions, PermissionStatus } from "expo-camera"; -import * as MediaLibrary from "expo-media-library"; -import { X, Share2, Save } from "lucide-react-native"; -import * as Sharing from "expo-sharing"; -import { useCurrentAccount } from "@/stores/account"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { captureRef } from "react-native-view-shot"; -import {Screen} from "@/router/helpers/types"; - -const NoteReaction: Screen<"NoteReaction"> = ({ navigation }) => { - const inset = useSafeAreaInsets(); - const [mediaLibraryPermission, requestMediaLibraryPermission] = MediaLibrary.usePermissions(); - const [cameraPermission, requestCameraPermission] = useCameraPermissions(); - const cameraRef = useRef(null); - const composerRef = useRef(null); - const [capturedImage, setCapturedImage] = useState(undefined); - const account = useCurrentAccount(store => store.account); - - useEffect(() => { - const setupPermissions = async () => { - if (mediaLibraryPermission?.status !== PermissionStatus.GRANTED) { - await requestMediaLibraryPermission(); - } - if (cameraPermission?.status !== PermissionStatus.GRANTED) { - await requestCameraPermission(); - } - }; - setupPermissions(); - }, [mediaLibraryPermission, cameraPermission, requestMediaLibraryPermission, requestCameraPermission]); - - const captureImage = async () => { - if (cameraPermission?.status !== PermissionStatus.GRANTED) { - Alert.alert("Permission Error", "Camera permission not granted"); - return; - } - try { - const photo = await cameraRef.current?.takePictureAsync({ - quality: 0.5, - skipProcessing: true, - }); - setCapturedImage(photo?.uri); - } catch (error) { - console.error("Failed to take picture:", error); - Alert.alert("Error", "Failed to capture image"); - } - }; - - const saveImage = async () => { - try { - const uri = await captureRef(composerRef, { - format: "png", - quality: 0.5, - }); - await MediaLibrary.saveToLibraryAsync(uri); - Alert.alert("Success", "Image saved to gallery"); - } catch (error) { - console.error("Failed to save image:", error); - Alert.alert("Error", "Failed to save image"); - } - }; - - const shareImage = async () => { - try { - const uri = await captureRef(composerRef, { - format: "png", - quality: 0.5, - }); - await Sharing.shareAsync(uri); - } catch (error) { - console.error("Failed to share image:", error); - Alert.alert("Error", "Failed to share image"); - } - }; - - React.useLayoutEffect(() => { - navigation.setOptions({ - headerRight: () => ( - navigation.goBack()} - > - - - ), - }); - }, [navigation]); - - return ( - - - - {capturedImage ? ( - - ) : ( - - )} - - - - 🇬🇧 - - - Oral d'anglais - 19 février 2024 - - - 0.00 - /20 - - - - - {!capturedImage && ( - <> - Une réaction ? - - Cette note était... quelque peu regrettable ? - - - Qu'as tu à dire {account?.studentName?.first || ""} ? - - - )} - {capturedImage ? ( - - - Enregistrer - - - - Partager - - - ) : ( - - - - )} - - sus - - - ); -}; - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: "black", - }, - cameraContainer: { - alignSelf: "center", - width: "90%", - height: 450, - borderRadius: 16, - overflow: "hidden", - }, - logo: { - position: "absolute", - width: 100, - height: 60, - zIndex: 40, - right: 20, - }, - cameraView: { - width: "100%", - height: "100%", - }, - capturedImage: { - width: "100%", - height: "100%", - transform: [{ scaleX: -1 }], - }, - infoNoteContainer: { - position: "absolute", - bottom: 20, - left: 0, - right: 0, - alignItems: "center", - }, - infoNote: { - width: "90%", - height: 60, - backgroundColor: "#FFFFFF", - borderRadius: 30, - flexDirection: "row", - alignItems: "center", - paddingHorizontal: 20, - }, - emoji: { - fontSize: 25, - }, - emojiContainer: { - marginRight: 5, - marginLeft: -10, - backgroundColor: "#00000020", - padding: 5, - borderRadius: 20, - - }, - subjectText: { fontWeight: "600", color: "#000000", fontSize: 16}, - dateText: { color: "#00000090" }, - scoreContainer: { - marginLeft: "auto", - flexDirection: "row", - alignItems: "baseline", - }, - scoreText: { fontWeight: "700", color: "#000000", fontSize: 20 }, - maxScoreText: { fontWeight: "300", color: "#000000" }, - titleText: { - fontSize: 20, - fontWeight: "600", - textAlign: "center", - marginTop: 20, - color: "#FFFFFF", - }, - descText: { - fontSize: 16, - textAlign: "center", - marginTop: 5, - color: "#FFFFFF90", - }, - captureButton: { - borderRadius: 37.5, - borderColor: "#FFFFFF", - borderWidth: 2, - width: 75, - height: 75, - justifyContent: "center", - alignItems: "center", - alignSelf: "center", - marginTop: 30, - }, - savebutton: { - alignItems: "center", - flexDirection: "row", - justifyContent: "center", - height: 70, - paddingHorizontal: 83, - backgroundColor: "#FFFFFF", - borderRadius: 200, - }, - captureButtonInner: { - borderRadius: 30, - backgroundColor: "#FFFFFF", - width: 60, - height: 60, - }, - actionButtons: { - flex: 1, - flexDirection: "row", - justifyContent: "center", - alignItems: "center", - alignSelf: "center", - marginBottom: 10, - gap: 10, - }, - actionButton: { - alignItems: "center", - gap: 5, - }, - buttonText: { - fontSize: 17, - fontFamily: "semibold", - color: "#FFFFFF", - }, - savebuttonText: { - fontSize: 17, - fontFamily: "semibold", - color: "#000000", - }, - headerRight: { - padding: 5, - borderRadius: 50, - marginRight: 5, - backgroundColor: "#FFFFFF20", - }, -}); - -export default NoteReaction; diff --git a/src/views/settings/Settings.tsx b/src/views/settings/Settings.tsx index 8d46498b0..f3dfbf5e8 100644 --- a/src/views/settings/Settings.tsx +++ b/src/views/settings/Settings.tsx @@ -28,7 +28,9 @@ import { Route, Scroll, Settings as SettingsLucide, - Sparkles, SunMoon, + Sparkles, + SunMoon, + Smile, SwatchBook, WandSparkles, X @@ -115,6 +117,12 @@ const Settings: Screen<"Settings"> = ({ route, navigation }) => { label: "Services externes", onPress: () => navigation.navigate("SettingsExternalServices"), }, + { + icon: , + color: "#136B00", + label: "Réactions", + onPress: () => navigation.navigate("SettingsReactions"), + }, ], }, { diff --git a/src/views/settings/SettingsReactions.tsx b/src/views/settings/SettingsReactions.tsx new file mode 100644 index 000000000..49400a566 --- /dev/null +++ b/src/views/settings/SettingsReactions.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { ScrollView, Text, View } from "react-native"; +import type { Screen } from "@/router/helpers/types"; +import { useTheme } from "@react-navigation/native"; +import { useGradesStore } from "@/stores/grades"; +import ReelGallery from "@/components/Settings/ReelGallery"; +import MissingItem from "@/components/Global/MissingItem"; +import AnimatedEmoji from "@/components/Grades/AnimatedEmoji"; + +const SettingsReactions: Screen<"SettingsReactions"> = () => { + const theme = useTheme(); + const reelsObject = useGradesStore((store) => store.reels); + const reels = Object.values(reelsObject); + + return ( + + {reels.length === 0 ? ( + + + + ) : ( + + )} + + ); +}; + +export default SettingsReactions; \ No newline at end of file