From 76fe9c4d298342e88663a584e39e1713a6f5c723 Mon Sep 17 00:00:00 2001 From: Daniel Santana Date: Mon, 6 Apr 2026 20:04:21 -0300 Subject: [PATCH 1/9] =?UTF-8?q?scrum-78=20tela=20de=20editar=20hist=C3=B3r?= =?UTF-8?q?ia=20e=20toggle=20de=20escrever=20ou=20gravar=20=C3=A1udio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(mybusiness)/editStory.tsx | 32 +++++++++++++ app/_layout.tsx | 1 + components/editStory/audioBox.tsx | 55 ++++++++++++++++++++++ components/editStory/checklist.tsx | 14 ++++++ components/editStory/checklistButton.tsx | 51 ++++++++++++++++++++ components/editStory/inputBox.tsx | 33 +++++++++++++ components/editStory/toggleWrite.tsx | 25 ++++++++++ components/editStory/toggleWriteButton.tsx | 42 +++++++++++++++++ components/overview/editButton.tsx | 4 +- 9 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 app/(mybusiness)/editStory.tsx create mode 100644 components/editStory/audioBox.tsx create mode 100644 components/editStory/checklist.tsx create mode 100644 components/editStory/checklistButton.tsx create mode 100644 components/editStory/inputBox.tsx create mode 100644 components/editStory/toggleWrite.tsx create mode 100644 components/editStory/toggleWriteButton.tsx diff --git a/app/(mybusiness)/editStory.tsx b/app/(mybusiness)/editStory.tsx new file mode 100644 index 0000000..1b1f514 --- /dev/null +++ b/app/(mybusiness)/editStory.tsx @@ -0,0 +1,32 @@ +import AudioBox from '@/components/editStory/audioBox'; +import Checklist from '@/components/editStory/checklist'; +import InputBox from '@/components/editStory/inputBox'; +import ToggleWrite from '@/components/editStory/toggleWrite'; +import { Container } from '@/components/general/container'; +import { Header } from '@/components/general/header'; +import { useState } from 'react'; +import { Text, View } from 'react-native'; + +export default function EditStory() { + const [toggle, setToggle] = useState<'WRITE' | 'AUDIO'>('WRITE') + const [text, setText] = useState('') + const [audio, setAudio] = useState('') + + return ( + + +
+ Conte sua história para nosso assistente, que vai resumir de uma forma objetiva e original. + + {toggle === 'WRITE' && + + } + { + toggle === 'AUDIO' && + + } + + + + ); +} diff --git a/app/_layout.tsx b/app/_layout.tsx index 83d1b3c..1f08e15 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -15,6 +15,7 @@ export default function RootLayout() { + diff --git a/components/editStory/audioBox.tsx b/components/editStory/audioBox.tsx new file mode 100644 index 0000000..2900148 --- /dev/null +++ b/components/editStory/audioBox.tsx @@ -0,0 +1,55 @@ +import Ionicons from "@expo/vector-icons/Ionicons" +import { Pressable, Text, View } from "react-native" +import Animated, { + useAnimatedStyle, + useSharedValue, + withRepeat, + withTiming +} from "react-native-reanimated" + +type Props = { + audio: string + setAudio: (s: string) => void +} + +export default function AudioBox({ audio, setAudio }: Props) { + + const scale = useSharedValue(1) + + const animatedStyle = useAnimatedStyle(() => { + return { + transform: [{ scale: scale.value }] + } + }) + + const handlePressIn = () => { + scale.value = withRepeat( + withTiming(1.2, { duration: 600 }), + -1, + true + ) + } + + const handlePressOut = () => { + scale.value = withTiming(1, { duration: 200 }) + } + + return ( + + + + + + + + + + Toque para começar a gravar + + + ) +} \ No newline at end of file diff --git a/components/editStory/checklist.tsx b/components/editStory/checklist.tsx new file mode 100644 index 0000000..d4719f1 --- /dev/null +++ b/components/editStory/checklist.tsx @@ -0,0 +1,14 @@ +import { Text, View } from 'react-native'; +import ChecklistButton from './checklistButton'; + +export default function Checklist() { + return ( + + Dicas para uma boa descrição + + + + + + ); +} diff --git a/components/editStory/checklistButton.tsx b/components/editStory/checklistButton.tsx new file mode 100644 index 0000000..b5bde8b --- /dev/null +++ b/components/editStory/checklistButton.tsx @@ -0,0 +1,51 @@ +import { useEffect } from "react" +import { Pressable, Text } from "react-native" +import Animated, { + interpolateColor, + useAnimatedStyle, + useSharedValue, + withTiming +} from "react-native-reanimated" + +type Props = { + text: string + check: boolean +} + +export default function ChecklistButton({ text, check }: Props) { + + const progress = useSharedValue(check ? 1 : 0) + + useEffect(() => { + progress.value = withTiming(check ? 1 : 0, { duration: 250 }) + }, [check]) + + const animatedStyle = useAnimatedStyle(() => { + return { + backgroundColor: interpolateColor( + progress.value, + [0, 1], + ['transparent', '#C34342'] // verde + ), + borderColor: interpolateColor( + progress.value, + [0, 1], + ['#000', '#C34342'] + ) + } + }) + + return ( + + + + + + {text} + + + ) +} \ No newline at end of file diff --git a/components/editStory/inputBox.tsx b/components/editStory/inputBox.tsx new file mode 100644 index 0000000..9e8061a --- /dev/null +++ b/components/editStory/inputBox.tsx @@ -0,0 +1,33 @@ +import { useState } from "react" +import { TextInput, View } from "react-native" + +type Props = { + text: string, + setText: (s: string) => void +} + +export default function InputBox({ text, setText }: Props) { + + const [isFocused, setIsFocused] = useState(false) + + return ( + + setIsFocused(true)} + onBlur={() => setIsFocused(false)} + className="text-base text-black" + /> + + ) +} \ No newline at end of file diff --git a/components/editStory/toggleWrite.tsx b/components/editStory/toggleWrite.tsx new file mode 100644 index 0000000..4f276c8 --- /dev/null +++ b/components/editStory/toggleWrite.tsx @@ -0,0 +1,25 @@ +import { View } from "react-native"; +import ToggleWriteButton from "./toggleWriteButton"; + +type Props = { + toggle: 'WRITE' | 'AUDIO' + setToggle: (s: 'WRITE' | 'AUDIO')=> void +} +export default function ToggleWrite ({toggle, setToggle}: Props) { + return( + + setToggle('WRITE')} + /> + setToggle("AUDIO")} + /> + + ) +} \ No newline at end of file diff --git a/components/editStory/toggleWriteButton.tsx b/components/editStory/toggleWriteButton.tsx new file mode 100644 index 0000000..4e1085a --- /dev/null +++ b/components/editStory/toggleWriteButton.tsx @@ -0,0 +1,42 @@ +import { Pressable, Text } from "react-native" +import Animated, { + useAnimatedStyle, + withTiming +} from "react-native-reanimated" + +type Props = { + text: string + tag: 'WRITE' | 'AUDIO' + toggle: 'WRITE' | 'AUDIO' + handlePress: (s: 'WRITE' | 'AUDIO') => void +} + +export default function ToggleWriteButton({ + text, + handlePress, + toggle, + tag +}: Props) { + + const animatedStyle = useAnimatedStyle(() => { + const isActive = toggle === tag + + return { + backgroundColor: withTiming( + isActive ? '#ffffff' : 'transparent', + { duration: 300 } + ) + } + }) + + return ( + + handlePress(tag)} + > + {text} + + + ) +} \ No newline at end of file diff --git a/components/overview/editButton.tsx b/components/overview/editButton.tsx index d225e81..bcec225 100644 --- a/components/overview/editButton.tsx +++ b/components/overview/editButton.tsx @@ -1,12 +1,12 @@ import Ionicons from '@expo/vector-icons/Ionicons'; +import { router } from 'expo-router'; import { Pressable, StyleSheet, Text, View } from 'react-native'; export default function EditButton() { - const handleEditButton = () => {}; return ( router.navigate('/(mybusiness)/editStory')} className="flex-row justify-between items-center" > From c718d6a468e4c5caf22804638e4af8ca7df2de2f Mon Sep 17 00:00:00 2001 From: Daniel Santana Date: Mon, 6 Apr 2026 22:45:40 -0300 Subject: [PATCH 2/9] =?UTF-8?q?scrum-78=20eslint=20corre=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/editStory/audioBox.tsx | 12 ++++++------ components/editStory/checklistButton.tsx | 14 +++++++------- components/editStory/inputBox.tsx | 4 ++-- components/editStory/toggleWrite.tsx | 6 +++--- components/editStory/toggleWriteButton.tsx | 12 ++++++------ 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/components/editStory/audioBox.tsx b/components/editStory/audioBox.tsx index 2900148..301510f 100644 --- a/components/editStory/audioBox.tsx +++ b/components/editStory/audioBox.tsx @@ -1,11 +1,11 @@ -import Ionicons from "@expo/vector-icons/Ionicons" -import { Pressable, Text, View } from "react-native" +import Ionicons from '@expo/vector-icons/Ionicons' +import { Pressable, Text, View } from 'react-native' import Animated, { useAnimatedStyle, useSharedValue, withRepeat, - withTiming -} from "react-native-reanimated" + withTiming, +} from 'react-native-reanimated' type Props = { audio: string @@ -18,7 +18,7 @@ export default function AudioBox({ audio, setAudio }: Props) { const animatedStyle = useAnimatedStyle(() => { return { - transform: [{ scale: scale.value }] + transform: [{ scale: scale.value }], } }) @@ -26,7 +26,7 @@ export default function AudioBox({ audio, setAudio }: Props) { scale.value = withRepeat( withTiming(1.2, { duration: 600 }), -1, - true + true, ) } diff --git a/components/editStory/checklistButton.tsx b/components/editStory/checklistButton.tsx index b5bde8b..e125c04 100644 --- a/components/editStory/checklistButton.tsx +++ b/components/editStory/checklistButton.tsx @@ -1,11 +1,11 @@ -import { useEffect } from "react" -import { Pressable, Text } from "react-native" +import { useEffect } from 'react' +import { Pressable, Text } from 'react-native' import Animated, { interpolateColor, useAnimatedStyle, useSharedValue, - withTiming -} from "react-native-reanimated" + withTiming, +} from 'react-native-reanimated' type Props = { text: string @@ -25,13 +25,13 @@ export default function ChecklistButton({ text, check }: Props) { backgroundColor: interpolateColor( progress.value, [0, 1], - ['transparent', '#C34342'] // verde + ['transparent', '#C34342'], // verde ), borderColor: interpolateColor( progress.value, [0, 1], - ['#000', '#C34342'] - ) + ['#000', '#C34342'], + ), } }) diff --git a/components/editStory/inputBox.tsx b/components/editStory/inputBox.tsx index 9e8061a..511fdb7 100644 --- a/components/editStory/inputBox.tsx +++ b/components/editStory/inputBox.tsx @@ -1,5 +1,5 @@ -import { useState } from "react" -import { TextInput, View } from "react-native" +import { useState } from 'react' +import { TextInput, View } from 'react-native' type Props = { text: string, diff --git a/components/editStory/toggleWrite.tsx b/components/editStory/toggleWrite.tsx index 4f276c8..f5ef1f1 100644 --- a/components/editStory/toggleWrite.tsx +++ b/components/editStory/toggleWrite.tsx @@ -1,5 +1,5 @@ -import { View } from "react-native"; -import ToggleWriteButton from "./toggleWriteButton"; +import { View } from 'react-native'; +import ToggleWriteButton from './toggleWriteButton'; type Props = { toggle: 'WRITE' | 'AUDIO' @@ -18,7 +18,7 @@ export default function ToggleWrite ({toggle, setToggle}: Props) { text='Gravar Áudio' tag='AUDIO' toggle={toggle} - handlePress={()=> setToggle("AUDIO")} + handlePress={()=> setToggle('AUDIO')} /> ) diff --git a/components/editStory/toggleWriteButton.tsx b/components/editStory/toggleWriteButton.tsx index 4e1085a..919abf8 100644 --- a/components/editStory/toggleWriteButton.tsx +++ b/components/editStory/toggleWriteButton.tsx @@ -1,8 +1,8 @@ -import { Pressable, Text } from "react-native" +import { Pressable, Text } from 'react-native' import Animated, { useAnimatedStyle, - withTiming -} from "react-native-reanimated" + withTiming, +} from 'react-native-reanimated' type Props = { text: string @@ -15,7 +15,7 @@ export default function ToggleWriteButton({ text, handlePress, toggle, - tag + tag, }: Props) { const animatedStyle = useAnimatedStyle(() => { @@ -24,8 +24,8 @@ export default function ToggleWriteButton({ return { backgroundColor: withTiming( isActive ? '#ffffff' : 'transparent', - { duration: 300 } - ) + { duration: 300 }, + ), } }) From 011d249b100050bd9aa61d6346dea0ca9c741c9d Mon Sep 17 00:00:00 2001 From: Daniel Santana Date: Mon, 6 Apr 2026 22:49:23 -0300 Subject: [PATCH 3/9] scrum-78 lint fix 2 --- components/editStory/audioBox.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/editStory/audioBox.tsx b/components/editStory/audioBox.tsx index 301510f..65a7c35 100644 --- a/components/editStory/audioBox.tsx +++ b/components/editStory/audioBox.tsx @@ -14,6 +14,9 @@ type Props = { export default function AudioBox({ audio, setAudio }: Props) { + audio + setAudio + const scale = useSharedValue(1) const animatedStyle = useAnimatedStyle(() => { From 33d808a4b2d9d96eac10a7004ad8069aca63be99 Mon Sep 17 00:00:00 2001 From: Daniel Santana Date: Tue, 7 Apr 2026 19:35:29 -0300 Subject: [PATCH 4/9] =?UTF-8?q?scrum-80=20tela=20de=20edi=C3=A7=C3=A3o=20e?= =?UTF-8?q?=20upload=20de=20imagens=20do=20estabelecimento?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(mybusiness)/editStory.tsx | 7 +++ app/(mybusiness)/manageImages.tsx | 17 ++++++ app/_layout.tsx | 1 + components/general/generalButton.tsx | 18 ++++++ components/manageImages/fileUpload.tsx | 63 +++++++++++++++++++++ components/manageImages/imageItem.tsx | 76 ++++++++++++++++++++++++++ components/manageImages/imagesList.tsx | 51 +++++++++++++++++ package-lock.json | 40 ++++++++++++-- package.json | 4 +- scripts/reset-project.js | 10 +++- 10 files changed, 279 insertions(+), 8 deletions(-) create mode 100644 app/(mybusiness)/manageImages.tsx create mode 100644 components/general/generalButton.tsx create mode 100644 components/manageImages/fileUpload.tsx create mode 100644 components/manageImages/imageItem.tsx create mode 100644 components/manageImages/imagesList.tsx diff --git a/app/(mybusiness)/editStory.tsx b/app/(mybusiness)/editStory.tsx index 1b1f514..a4a7e99 100644 --- a/app/(mybusiness)/editStory.tsx +++ b/app/(mybusiness)/editStory.tsx @@ -3,7 +3,9 @@ import Checklist from '@/components/editStory/checklist'; import InputBox from '@/components/editStory/inputBox'; import ToggleWrite from '@/components/editStory/toggleWrite'; import { Container } from '@/components/general/container'; +import GeneralButton from '@/components/general/generalButton'; import { Header } from '@/components/general/header'; +import { router } from 'expo-router'; import { useState } from 'react'; import { Text, View } from 'react-native'; @@ -12,6 +14,10 @@ export default function EditStory() { const [text, setText] = useState('') const [audio, setAudio] = useState('') + const handlePress = ()=> { + router.navigate('/(mybusiness)/manageImages') + } + return ( @@ -26,6 +32,7 @@ export default function EditStory() { } + ); diff --git a/app/(mybusiness)/manageImages.tsx b/app/(mybusiness)/manageImages.tsx new file mode 100644 index 0000000..69a4e3e --- /dev/null +++ b/app/(mybusiness)/manageImages.tsx @@ -0,0 +1,17 @@ +import { Container } from '@/components/general/container'; +import { Header } from '@/components/general/header'; +import FileUpload from '@/components/manageImages/fileUpload'; +import ImagesList from '@/components/manageImages/imagesList'; +import { View } from 'react-native'; + +export default function ManageImages () { + return( + + +
+ + + + + ) +} \ No newline at end of file diff --git a/app/_layout.tsx b/app/_layout.tsx index 1f08e15..f983bd7 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -16,6 +16,7 @@ export default function RootLayout() { + diff --git a/components/general/generalButton.tsx b/components/general/generalButton.tsx new file mode 100644 index 0000000..946627d --- /dev/null +++ b/components/general/generalButton.tsx @@ -0,0 +1,18 @@ +import { Pressable, Text } from 'react-native'; + +type Props = { + text: string + handlePress: ()=> void +} +export default function GeneralButton ({text, handlePress}: Props) { + return( + + + {text} + + + ) +} \ No newline at end of file diff --git a/components/manageImages/fileUpload.tsx b/components/manageImages/fileUpload.tsx new file mode 100644 index 0000000..ffd904a --- /dev/null +++ b/components/manageImages/fileUpload.tsx @@ -0,0 +1,63 @@ +import Ionicons from '@expo/vector-icons/Ionicons'; +import * as ImagePicker from 'expo-image-picker'; +import { Component } from 'react'; +import { Image, Pressable, Text, View } from 'react-native'; + +type State = { + image: string | null; +}; + +export default class FileUpload extends Component<{}, State> { + state: State = { + image: null, + }; + + pickImage = async () => { + const permission = await ImagePicker.requestMediaLibraryPermissionsAsync(); + + if (!permission.granted) { + alert('Permissão para acessar a galeria é necessária!'); + return; + } + + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ['images'], + quality: 1, + }); + + if (!result.canceled) { + this.setState({ image: result.assets[0].uri }); + } + }; + + render() { + const { image } = this.state; + + return ( + + + Imagens ou mídias + + + + + {image ? ( + + ) : ( + <> + + + Adicionar Imagem + + + )} + + + + ); + } +} \ No newline at end of file diff --git a/components/manageImages/imageItem.tsx b/components/manageImages/imageItem.tsx new file mode 100644 index 0000000..7da65a0 --- /dev/null +++ b/components/manageImages/imageItem.tsx @@ -0,0 +1,76 @@ +import * as ImagePicker from 'expo-image-picker'; +import { ActivityIndicator, Alert, Image, Pressable, Text, View } from 'react-native'; + +type Props = { + id: string; + uri: string; + onDelete: () => void; + onReplace: (newUri: string) => void; + isLoading?: boolean; +}; + +export default function ImageItem({ + uri, + onDelete, + onReplace, + isLoading = false, +}: Props) { + + async function handlePickImage() { + const permission = await ImagePicker.requestMediaLibraryPermissionsAsync(); + + if (!permission.granted) { + Alert.alert('Permissão necessária', 'Você precisa permitir acesso à galeria.'); + return; + } + + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ['images'], // novo padrão + quality: 1, + }); + + if (!result.canceled) { + const newUri = result.assets[0].uri; + + onReplace(newUri); + } + } + + return ( + + + + + {/* 🔄 Substituir */} + + {isLoading ? ( + + ) : ( + + Substituir + + )} + + + {/* 🗑️ Deletar */} + + + Deletar + + + + + ); +} \ No newline at end of file diff --git a/components/manageImages/imagesList.tsx b/components/manageImages/imagesList.tsx new file mode 100644 index 0000000..af3de5e --- /dev/null +++ b/components/manageImages/imagesList.tsx @@ -0,0 +1,51 @@ +import { useState } from 'react'; +import { Text, View } from 'react-native'; +import ImageItem from './imageItem'; + +type ImageType = { + id: string; + uri: string; +}; + +export default function ImagesList() { + const [images, setImages] = useState([ + { id: '1', uri: 'https://picsum.photos/id/1018/800/400' }, + { id: '2', uri: 'https://picsum.photos/id/1015/800/400' }, + { id: '3', uri: 'https://picsum.photos/id/1019/800/400' }, + { id: '4', uri: 'https://picsum.photos/id/1020/800/400' }, + { id: '5', uri: 'https://picsum.photos/id/1024/800/400' }, + ]); + + // DELETE (simulando API) + function handleDelete(id: string) { + setImages(prev => prev.filter(img => img.id !== id)); + } + + // UPDATE / REPLACE (simulando API) + function handleReplace(id: string) { + const newImage = `https://picsum.photos/800/400?random=${Math.random()}`; + + setImages(prev => + prev.map(img => + img.id === id ? { ...img, uri: newImage } : img, + ), + ); + } + + return ( + + + Imagens cadastradas + + {images.map((img) => ( + handleDelete(img.id)} + onReplace={() => handleReplace(img.id)} + /> + ))} + + ); +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e4c14b9..90ea7cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "expo-font": "~55.0.4", "expo-haptics": "~55.0.9", "expo-image": "~55.0.6", + "expo-image-picker": "~55.0.17", "expo-linking": "~55.0.9", "expo-router": "~55.0.8", "expo-splash-screen": "~55.0.13", @@ -39,7 +40,8 @@ "react-native-snap-carousel": "^3.9.1", "react-native-web": "~0.21.0", "react-native-worklets": "0.7.2", - "tailwindcss": "^3.4.17" + "tailwindcss": "^3.4.17", + "zod": "^4.3.6" }, "devDependencies": { "@types/react": "~19.2.10", @@ -6220,6 +6222,27 @@ } } }, + "node_modules/expo-image-loader": { + "version": "55.0.0", + "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-55.0.0.tgz", + "integrity": "sha512-NOjp56wDrfuA5aiNAybBIjqIn1IxKeGJ8CECWZncQ/GzjZfyTYAHTCyeApYkdKkMBLHINzI4BbTGSlbCa0fXXQ==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-image-picker": { + "version": "55.0.17", + "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-55.0.17.tgz", + "integrity": "sha512-oCayiw6ZMKDnUGVPFhQ1j0Cg0ZvzSDWwuVm0QSX+AkdqBuRv/n3SB3ZTVW2M+lR6zU/aTtVTduqlNnVyv4CrhA==", + "license": "MIT", + "dependencies": { + "expo-image-loader": "~55.0.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-keep-awake": { "version": "55.0.4", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-55.0.4.tgz", @@ -6784,6 +6807,15 @@ } } }, + "node_modules/expo/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -13690,9 +13722,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index e458a17..e7421b8 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "expo-font": "~55.0.4", "expo-haptics": "~55.0.9", "expo-image": "~55.0.6", + "expo-image-picker": "~55.0.17", "expo-linking": "~55.0.9", "expo-router": "~55.0.8", "expo-splash-screen": "~55.0.13", @@ -44,7 +45,8 @@ "react-native-snap-carousel": "^3.9.1", "react-native-web": "~0.21.0", "react-native-worklets": "0.7.2", - "tailwindcss": "^3.4.17" + "tailwindcss": "^3.4.17", + "zod": "^4.3.6" }, "devDependencies": { "@types/react": "~19.2.10", diff --git a/scripts/reset-project.js b/scripts/reset-project.js index 0abde93..8edba94 100644 --- a/scripts/reset-project.js +++ b/scripts/reset-project.js @@ -2,8 +2,11 @@ /** * This script is used to reset the project to a blank state. - * It deletes or moves the /app, /components, /hooks, /scripts, and /constants directories to /app-example based on user input and creates a new /app directory with an index.tsx and _layout.tsx file. - * You can remove the `reset-project` script from package.json and safely delete this file after running it. + * It deletes or moves the /app, /components, /hooks, /scripts, + * and /constants directories to /app-example based on user inpu + * and creates a new /app directory with an index.tsx and _layout.tsx file. + * You can remove the `reset-project` script from package.json + * and safely delete this file after running it. */ const fs = require('fs'); @@ -87,7 +90,8 @@ const moveDirectories = async (userInput) => { console.log('\n✅ Project reset complete. Next steps:'); console.log( - `1. Run \`npx expo start\` to start a development server.\n2. Edit app/index.tsx to edit the main screen.${ + `1. Run \`npx expo start\` to start a development server.\n2. + Edit app/index.tsx to edit the main screen.${ userInput === 'y' ? `\n3. Delete the /${exampleDir} directory when you're done referencing it.` : '' From 4b842ec2e9af9a2c77bbfc548be3f2bf94adf163 Mon Sep 17 00:00:00 2001 From: Daniel Santana Date: Tue, 7 Apr 2026 23:29:33 -0300 Subject: [PATCH 5/9] scrum-27 implementando a api no header e no progresso da home --- app/home.tsx | 6 +- app/index.tsx | 17 +++- .../Home/completeProfile/completeProfile.tsx | 79 +++++++++++++++ components/Home/completeProfile/main.tsx | 47 --------- components/Home/header/header.tsx | 96 +++++++++++++++++++ components/Home/header/main.tsx | 46 --------- types/enterprise.ts | 7 ++ types/user.ts | 8 ++ 8 files changed, 205 insertions(+), 101 deletions(-) create mode 100644 components/Home/completeProfile/completeProfile.tsx delete mode 100644 components/Home/completeProfile/main.tsx create mode 100644 components/Home/header/header.tsx delete mode 100644 components/Home/header/main.tsx create mode 100644 types/enterprise.ts create mode 100644 types/user.ts diff --git a/app/home.tsx b/app/home.tsx index bcab6c4..b143082 100644 --- a/app/home.tsx +++ b/app/home.tsx @@ -1,13 +1,13 @@ import { Container } from '@/components/general/container'; -import { CompleteProfile } from '@/components/Home/completeProfile/main'; -import { Header } from '@/components/Home/header/main'; +import { CompleteProfile } from '@/components/Home/completeProfile/completeProfile'; +import { Header } from '@/components/Home/header/header'; import { RouteGrid } from '@/components/Home/routeGrid/main'; export default function Home() { return (
- + ); diff --git a/app/index.tsx b/app/index.tsx index 8b87602..a50b026 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -1,17 +1,24 @@ -import { router } from 'expo-router'; +import { useRouter } from 'expo-router'; import { Pressable, Text, View } from 'react-native'; import '../global.css'; export default function App() { + const router = useRouter(); // ✅ correto + return ( - Hello Mandacá! + + Hello Mandacá! + + router.navigate('/home')} + onPress={() => router.push('/home')} // ✅ usar push > - ir para Home + + ir para Home + ); -} +} \ No newline at end of file diff --git a/components/Home/completeProfile/completeProfile.tsx b/components/Home/completeProfile/completeProfile.tsx new file mode 100644 index 0000000..8ec3206 --- /dev/null +++ b/components/Home/completeProfile/completeProfile.tsx @@ -0,0 +1,79 @@ +import { Enterprise } from '@/types/enterprise'; +import axios from 'axios'; +import { useEffect, useState } from 'react'; +import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'; + +export const CompleteProfile = () => { + const [enterprise, setEnterprise] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let isMounted = true; + + const getEnterprise = async () => { + try { + const enterpriseId = 'caa68f64-b68e-4327-90f0-264ca1bb73e2'; + const response = await axios.get( + `https://mandaca-backend.onrender.com/enterprises/percentage/${enterpriseId}`, + ); + if (isMounted) setEnterprise(response.data); + } catch (error) { + console.error('Erro ao buscar empresa:', error); + } finally { + if (isMounted) setLoading(false); + } + }; + + getEnterprise(); + return () => { isMounted = false; }; // Limpa o efeito para evitar erros de memória + }, []); + + if (loading) { + return ( + + + + ); + } + + const porcentagem = enterprise?.porcentagem ?? 0; + + return ( + + + Complete seu Perfil + {porcentagem}% + + + + Preencha as informações para atrair mais turistas + + + + + + + ); +}; + +const style = StyleSheet.create({ + card: { + padding: 24, + backgroundColor: '#FFF', // Substitua 'light' pela cor real para testar + borderRadius: 24, + gap: 16, + }, + row: { flexDirection: 'row', justifyContent: 'space-between' }, + title: { fontWeight: 'bold', fontSize: 24 }, + percentageText: { fontWeight: 'bold', fontSize: 24, color: '#C34342' }, // Substitua pela cor do seu primary + subtitle: { fontSize: 18 }, + progressBg: { width: '100%', height: 8, backgroundColor: '#EEE', borderRadius: 99 }, + progressBar: { height: '100%', backgroundColor: '#C34342', borderRadius: 99 }, + cardShadow: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 3.8, + elevation: 5, + }, +}); \ No newline at end of file diff --git a/components/Home/completeProfile/main.tsx b/components/Home/completeProfile/main.tsx deleted file mode 100644 index ead29e6..0000000 --- a/components/Home/completeProfile/main.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { StyleSheet, Text, View } from 'react-native'; -import Animated, { useSharedValue } from 'react-native-reanimated'; - -type Props = { - prifileProgress: number; -}; - -export const CompleteProfile = ({ prifileProgress }: Props) => { - const profileProgress = useSharedValue(prifileProgress); - - return ( - - - Complete seu Perfil - 75% - - - - Preencha as informações para atrair mais turistas - - - - - - - ); -}; - -const style = StyleSheet.create({ - cardShadow: { - shadowColor: '#000', - shadowOffset: { - width: 0, - height: 2, - }, - shadowOpacity: 1, - shadowRadius: 3.8, - - elevation: 5, - }, -}); diff --git a/components/Home/header/header.tsx b/components/Home/header/header.tsx new file mode 100644 index 0000000..53b3177 --- /dev/null +++ b/components/Home/header/header.tsx @@ -0,0 +1,96 @@ +import { User } from '@/types/user'; +import Ionicons from '@expo/vector-icons/Ionicons'; +import axios from 'axios'; +import { router } from 'expo-router'; +import { useEffect, useState } from 'react'; +import { + ActivityIndicator, + Image, + Pressable, + StyleSheet, + Text, + View, +} from 'react-native'; + + + +export const Header = () => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + const getUser = async () => { + try { + const userId = '453df15b-61ce-4571-8bdb-cdbedf0ff041'; + + const responseUser = await axios.get( + `https://mandaca-backend.onrender.com/users/${userId}`, + ); + + setUser(responseUser.data); + } catch (error) { + console.error('Erro ao buscar usuário:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + getUser(); + }, []); + + if (loading) { + return ; + } + + const userName = user?.nome || 'Usuário'; + const userInitial = userName.charAt(0).toUpperCase(); + + return ( + + router.push('/profile')} + > + {user?.url_foto_usuario ? ( + + ) : ( + {userInitial} + )} + + + + + Bem-vindo de volta, + + + {userName} + + + + router.push('/notifications')} + > + + + + ); +}; + +const style = StyleSheet.create({ + cardShadow: { + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 1, + shadowRadius: 3.8, + elevation: 5, + }, +}); \ No newline at end of file diff --git a/components/Home/header/main.tsx b/components/Home/header/main.tsx deleted file mode 100644 index c887bf6..0000000 --- a/components/Home/header/main.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import Ionicons from '@expo/vector-icons/Ionicons'; -import { router } from 'expo-router'; -import { Pressable, StyleSheet, Text, View } from 'react-native'; - -export const Header = () => { - const userName = 'Maria da Silva'; // requisição para receber nome do usuário - - return ( - - router.navigate('/profile')} - > - M - - - - Bem-vinda de volta, - {userName} - - - router.navigate('/notifications')} - > - - - - ); -}; - -const style = StyleSheet.create({ - cardShadow: { - shadowColor: '#000', - shadowOffset: { - width: 0, - height: 2, - }, - shadowOpacity: 1, - shadowRadius: 3.8, - - elevation: 5, - }, -}); diff --git a/types/enterprise.ts b/types/enterprise.ts new file mode 100644 index 0000000..312b97c --- /dev/null +++ b/types/enterprise.ts @@ -0,0 +1,7 @@ +export type Enterprise = { + id_empresa: string; + nome: string; + porcentagem: number; + campos_preenchidos: string[]; + campos_faltando: string[]; +}; \ No newline at end of file diff --git a/types/user.ts b/types/user.ts new file mode 100644 index 0000000..a9f6ca3 --- /dev/null +++ b/types/user.ts @@ -0,0 +1,8 @@ +export type User = { + id_usuario: string; + tipo_usuario: string; + nome: string; + cpf: string; + url_foto_usuario: string; + empresa_id: string; +}; \ No newline at end of file From 2d17b58563fbdee267ec112eda0a607dbff64baf Mon Sep 17 00:00:00 2001 From: Daniel Santana Date: Wed, 8 Apr 2026 00:49:15 -0300 Subject: [PATCH 6/9] scrum-27 completeProfile useEffect fix --- .../Home/completeProfile/completeProfile.tsx | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/components/Home/completeProfile/completeProfile.tsx b/components/Home/completeProfile/completeProfile.tsx index 8ec3206..8f75ba4 100644 --- a/components/Home/completeProfile/completeProfile.tsx +++ b/components/Home/completeProfile/completeProfile.tsx @@ -7,25 +7,31 @@ export const CompleteProfile = () => { const [enterprise, setEnterprise] = useState(null); const [loading, setLoading] = useState(true); - useEffect(() => { - let isMounted = true; - - const getEnterprise = async () => { - try { - const enterpriseId = 'caa68f64-b68e-4327-90f0-264ca1bb73e2'; - const response = await axios.get( - `https://mandaca-backend.onrender.com/enterprises/percentage/${enterpriseId}`, - ); - if (isMounted) setEnterprise(response.data); - } catch (error) { - console.error('Erro ao buscar empresa:', error); - } finally { - if (isMounted) setLoading(false); + const getEnterprise = async () => { + try { + const enterpriseId = + 'caa68f64-b68e-4327-90f0-264ca1bb73e2'; + + const response = await axios.get( + `https://mandaca-backend.onrender.com/enterprises/percentage/${enterpriseId}` + ); + + // 🔥 validação extra (evita crash) + if (response.data && typeof response.data.porcentagem === 'number') { + setEnterprise(response.data); + } else { + console.warn('Resposta inesperada:', response.data); } - }; + } catch (error) { + console.error('Erro ao buscar empresa:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { getEnterprise(); - return () => { isMounted = false; }; // Limpa o efeito para evitar erros de memória }, []); if (loading) { @@ -61,7 +67,7 @@ const style = StyleSheet.create({ padding: 24, backgroundColor: '#FFF', // Substitua 'light' pela cor real para testar borderRadius: 24, - gap: 16, + gap: 16 }, row: { flexDirection: 'row', justifyContent: 'space-between' }, title: { fontWeight: 'bold', fontSize: 24 }, From 6332438ba56e2c5f411c19a3d6deb5703029f261 Mon Sep 17 00:00:00 2001 From: Daniel Santana Date: Wed, 8 Apr 2026 00:51:40 -0300 Subject: [PATCH 7/9] scrum-27 lint fix --- components/Home/completeProfile/completeProfile.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/Home/completeProfile/completeProfile.tsx b/components/Home/completeProfile/completeProfile.tsx index 8f75ba4..0845dd7 100644 --- a/components/Home/completeProfile/completeProfile.tsx +++ b/components/Home/completeProfile/completeProfile.tsx @@ -13,7 +13,7 @@ export const CompleteProfile = () => { 'caa68f64-b68e-4327-90f0-264ca1bb73e2'; const response = await axios.get( - `https://mandaca-backend.onrender.com/enterprises/percentage/${enterpriseId}` + `https://mandaca-backend.onrender.com/enterprises/percentage/${enterpriseId}`, ); // 🔥 validação extra (evita crash) @@ -67,7 +67,7 @@ const style = StyleSheet.create({ padding: 24, backgroundColor: '#FFF', // Substitua 'light' pela cor real para testar borderRadius: 24, - gap: 16 + gap: 16, }, row: { flexDirection: 'row', justifyContent: 'space-between' }, title: { fontWeight: 'bold', fontSize: 24 }, From 5a9454786be7e333dc00c836db275a5fc747f248 Mon Sep 17 00:00:00 2001 From: Ronaldo Ribeiro Date: Wed, 22 Apr 2026 07:13:02 -0300 Subject: [PATCH 8/9] Scrum-47 Interface do chatbot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Consultor virtual com chat temporário, header e ajuste de teclado - Ainda falta integrar com o back! --- app.json | 3 +- app/_layout.tsx | 1 + app/consultant.tsx | 196 +++++++++++++++++++++++++++ app/report.tsx | 20 ++- assets/images/profile-robot.jpg | Bin 0 -> 8220 bytes components/consultant/ChatBubble.tsx | 72 ++++++++++ components/consultant/ChatInput.tsx | 90 ++++++++++++ components/general/header.tsx | 38 ++++-- constants/theme.ts | 22 +++ types/index.ts | 8 ++ 10 files changed, 439 insertions(+), 11 deletions(-) create mode 100644 app/consultant.tsx create mode 100644 assets/images/profile-robot.jpg create mode 100644 components/consultant/ChatBubble.tsx create mode 100644 components/consultant/ChatInput.tsx diff --git a/app.json b/app.json index 4713a73..6606d6d 100644 --- a/app.json +++ b/app.json @@ -19,6 +19,7 @@ "monochromeImage": "./assets/images/android-icon-monochrome.png" }, "edgeToEdgeEnabled": true, + "softwareKeyboardLayoutMode": "resize", "predictiveBackGestureEnabled": false }, "web": { @@ -49,4 +50,4 @@ "reactCompiler": true } } -} +} \ No newline at end of file diff --git a/app/_layout.tsx b/app/_layout.tsx index f983bd7..a4dc312 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -9,6 +9,7 @@ export default function RootLayout() { + diff --git a/app/consultant.tsx b/app/consultant.tsx new file mode 100644 index 0000000..6c91c97 --- /dev/null +++ b/app/consultant.tsx @@ -0,0 +1,196 @@ +import { ChatBubble } from '@/components/consultant/ChatBubble'; +import { ChatInput } from '@/components/consultant/ChatInput'; +import { CHATBOT_THEME } from '@/constants/theme'; +import { ChatMessage } from '@/types'; +import Ionicons from '@expo/vector-icons/Ionicons'; +import axios from 'axios'; +import { useEffect, useRef, useState } from 'react'; +import { + ActivityIndicator, + FlatList, + KeyboardAvoidingView, + Platform, + Pressable, + Text, + View, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +const INITIAL_BOT_MESSAGE = `Olá! Sou seu consultor virtual. +Como posso ajudar com a gestão +do seu restaurante hoje?`; + +export default function Consultant() { + const [messages, setMessages] = useState([ + { + id: '1', + type: 'bot', + content: INITIAL_BOT_MESSAGE, + timestamp: new Date(), + contentType: 'text', + }, + ]); + const [isAwaitingResponse, setIsAwaitingResponse] = useState(false); + const [user, setUser] = useState(null); + const [showScrollButton, setShowScrollButton] = useState(false); + const flatListRef = useRef(null); + + // Carrega dados do usuário sem bloquear a renderização inicial do chat + useEffect(() => { + const loadUser = async () => { + try { + const userId = '453df15b-61ce-4571-8bdb-cdbedf0ff041'; + + const responseUser = await axios.get( + `https://mandaca-backend.onrender.com/users/${userId}`, + { timeout: 4000 }, + ); + + setUser(responseUser.data); + } catch (error) { + console.error('Erro ao carregar usuário:', error); + } + }; + + loadUser(); + }, []); + + // Scroll automático para a última mensagem quando nova mensagem chega + useEffect(() => { + if (messages.length > 1) { + setTimeout(() => { + flatListRef.current?.scrollToEnd({ animated: true }); + }, 100); + } + }, [messages]); + + + const handleSendMessage = async (content: string) => { + // Adiciona mensagem do usuário + const userMessage: ChatMessage = { + id: Date.now().toString(), + type: 'user', + content: content, + timestamp: new Date(), + contentType: 'text', + }; + + setMessages((prev) => [...prev, userMessage]); + setIsAwaitingResponse(true); + + try { + // Requisição para buscar resposta do backend: + + // Simulando delay de resposta (temporario) + await new Promise((resolve) => setTimeout(resolve, 800)); + + const botResponse: ChatMessage = { + id: (Date.now() + 1).toString(), + type: 'bot', + content: + 'Entendo. Este é um exemplo de resposta do bot. Em produção, isso virá do backend com orientações específicas para seu restaurante.', + timestamp: new Date(), + contentType: 'text', + }; + + setMessages((prev) => [...prev, botResponse]); + } catch (error) { + console.error('Erro ao enviar mensagem:', error); + + const errorMessage: ChatMessage = { + id: (Date.now() + 1).toString(), + type: 'bot', + content: + 'Desculpe, houve um erro ao processar sua mensagem. Tente novamente.', + timestamp: new Date(), + contentType: 'text', + }; + + setMessages((prev) => [...prev, errorMessage]); + } finally { + setIsAwaitingResponse(false); + } + }; + + const handleScrollToBottom = () => { + flatListRef.current?.scrollToEnd({ animated: true }); + setShowScrollButton(false); + }; + + const handleScroll = (event: any) => { + const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent; + // Mostra botão se o usuário não está no final da lista + const isAtBottom = + contentOffset.y + layoutMeasurement.height >= contentSize.height - 100; + setShowScrollButton(!isAtBottom && messages.length > 3); + }; + + return ( + + {/* Main Content Container */} + + {/* Header */} + + + Consultor Virtual + + + + {/* Messages List */} + ( + + )} + keyExtractor={(item) => item.id} + contentContainerStyle={{ + paddingVertical: 16, + paddingHorizontal: 12, + flexGrow: 1, + }} + keyboardDismissMode={Platform.OS === 'ios' ? 'interactive' : 'on-drag'} + showsVerticalScrollIndicator={false} + keyboardShouldPersistTaps="handled" + onScroll={handleScroll} + scrollEventThrottle={16} + ListFooterComponent={ + isAwaitingResponse ? ( + + + + ) : null + } + /> + + {/* Scroll to Bottom Button */} + {showScrollButton && ( + + + + )} + + + + + + + ); +} diff --git a/app/report.tsx b/app/report.tsx index fd1e843..6ac9523 100644 --- a/app/report.tsx +++ b/app/report.tsx @@ -1,11 +1,27 @@ import { Container } from '@/components/general/container'; +import { Header } from '@/components/general/header'; +import { router } from 'expo-router'; import { Text, View } from 'react-native'; export default function Report() { + const handleConsultorPress = () => { + router.push('/consultant'); + }; + return ( - - Relatórios +
+ + Conteúdo dos Relatórios + {/* Implementar conteúdo de relatórios */} ); diff --git a/assets/images/profile-robot.jpg b/assets/images/profile-robot.jpg new file mode 100644 index 0000000000000000000000000000000000000000..27beb400b2a77c2b48e8eeab2a00113e0a2532cd GIT binary patch literal 8220 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 literal 0 HcmV?d00001 diff --git a/components/consultant/ChatBubble.tsx b/components/consultant/ChatBubble.tsx new file mode 100644 index 0000000..0946fb5 --- /dev/null +++ b/components/consultant/ChatBubble.tsx @@ -0,0 +1,72 @@ +import { CHATBOT_THEME } from '@/constants/theme'; +import { ChatMessage } from '@/types'; +import { Image, Text, View } from 'react-native'; + +type Props = { + message: ChatMessage; + userProfileUri?: string; + userName?: string; +}; + +export const ChatBubble = ({ message, userProfileUri, userName }: Props) => { + const isBot = message.type === 'bot'; + const theme = isBot ? CHATBOT_THEME.bot : CHATBOT_THEME.user; + + return ( + + {/* Label acima da bolha */} + + {isBot ? 'Consultor IA' : (userName || 'Você')} + + + {/* Container da bolha com foto */} + + {/* Foto do Bot (esquerda) */} + {isBot && ( + + )} + + {/* Bolha de mensagem */} + + + {message.content} + + + + {/* Foto do Usuário (direita) */} + {!isBot && ( + + )} + + + ); +}; diff --git a/components/consultant/ChatInput.tsx b/components/consultant/ChatInput.tsx new file mode 100644 index 0000000..9204ef5 --- /dev/null +++ b/components/consultant/ChatInput.tsx @@ -0,0 +1,90 @@ +import { CHATBOT_THEME } from '@/constants/theme'; +import Ionicons from '@expo/vector-icons/Ionicons'; +import { useState } from 'react'; +import { + Pressable, + TextInput, + View, +} from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +type Props = { + onSendMessage: (message: string) => void; + isLoading?: boolean; +}; + +export const ChatInput = ({ onSendMessage, isLoading = false }: Props) => { + const [message, setMessage] = useState(''); + const insets = useSafeAreaInsets(); + + const handleSend = () => { + if (message.trim()) { + onSendMessage(message.trim()); + setMessage(''); + } + }; + + return ( + + + {/* Input Field Container */} + + {/* Microphone Button */} + + + + + {/* Text Input */} + + + + {/* Send Button */} + + + + + + ); +}; diff --git a/components/general/header.tsx b/components/general/header.tsx index e1c8b9a..1c9f592 100644 --- a/components/general/header.tsx +++ b/components/general/header.tsx @@ -8,6 +8,12 @@ type Props = { showNotificationButton?: boolean; onBackPress?: () => void; onNotificationPress?: () => void; + rightButtonIcon?: string; + rightButtonColor?: string; + rightButtonBgColor?: string; + backButtonColor?: string; + backButtonBgColor?: string; + backButtonSize?: number; }; export const Header = ({ @@ -16,6 +22,12 @@ export const Header = ({ showNotificationButton = true, onBackPress, onNotificationPress, + rightButtonIcon, + rightButtonColor = '#2C2C2C', + rightButtonBgColor = '#FFFFFF', + backButtonColor = '#2C2C2C', + backButtonBgColor = '#FFFFFF', + backButtonSize = 10, }: Props) => { const handleBack = onBackPress ? onBackPress : () => router.back(); const handleNotification = onNotificationPress @@ -23,28 +35,38 @@ export const Header = ({ : () => router.navigate('/notifications'); return ( - + {showBackButton ? ( - + ) : ( - + )} {title} {showNotificationButton ? ( - + ) : ( diff --git a/constants/theme.ts b/constants/theme.ts index 6ac358e..a58a1bd 100644 --- a/constants/theme.ts +++ b/constants/theme.ts @@ -70,3 +70,25 @@ export const SENTIMENT_CONFIG = { textColor: '#004BDA', }, }; + +export const CHATBOT_THEME = { + bot: { + bgColor: '#EAEAEA', + textColor: '#000000', + labelColor: '#816F6A', + }, + user: { + bgColor: '#C34342', + textColor: '#FFFFFF', + }, + input: { + bgColor: '#EAEAEA', + textColor: '#6B7280', + placeholderColor: '#6B7280', + }, + sendButton: { + bgColor: '#C34342', + iconColor: '#FFFFFF', + }, + divider: '#D1D5DB', +}; diff --git a/types/index.ts b/types/index.ts index ed5504d..08d840d 100644 --- a/types/index.ts +++ b/types/index.ts @@ -1 +1,9 @@ export type ReviewSentiment = 'elogios' | 'dicas' | 'duvidas'; + +export type ChatMessage = { + id: string; + type: 'user' | 'bot'; + content: string; + timestamp: Date; + contentType?: 'text' | 'audio'; // Para futuras integrações com áudio +}; From ac60037369f69082bf3d10e7665b308ae4102231 Mon Sep 17 00:00:00 2001 From: Ronaldo Ribeiro Date: Wed, 22 Apr 2026 07:20:45 -0300 Subject: [PATCH 9/9] Scrum-47 fix: corrigi a checagem de tipo do typescript --- components/general/header.tsx | 5 ++++- types/index.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/components/general/header.tsx b/components/general/header.tsx index 1c9f592..0ae4bc8 100644 --- a/components/general/header.tsx +++ b/components/general/header.tsx @@ -1,14 +1,17 @@ import Ionicons from '@expo/vector-icons/Ionicons'; import { router } from 'expo-router'; +import type { ComponentProps } from 'react'; import { Pressable, StyleSheet, Text, View } from 'react-native'; +type IoniconName = ComponentProps['name']; + type Props = { title: string; showBackButton?: boolean; showNotificationButton?: boolean; onBackPress?: () => void; onNotificationPress?: () => void; - rightButtonIcon?: string; + rightButtonIcon?: IoniconName; rightButtonColor?: string; rightButtonBgColor?: string; backButtonColor?: string; diff --git a/types/index.ts b/types/index.ts index 08d840d..28ea31c 100644 --- a/types/index.ts +++ b/types/index.ts @@ -5,5 +5,5 @@ export type ChatMessage = { type: 'user' | 'bot'; content: string; timestamp: Date; - contentType?: 'text' | 'audio'; // Para futuras integrações com áudio + contentType?: 'text' | 'audio'; };