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/(mybusiness)/editStory.tsx b/app/(mybusiness)/editStory.tsx
new file mode 100644
index 0000000..a4a7e99
--- /dev/null
+++ b/app/(mybusiness)/editStory.tsx
@@ -0,0 +1,39 @@
+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 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';
+
+export default function EditStory() {
+ const [toggle, setToggle] = useState<'WRITE' | 'AUDIO'>('WRITE')
+ const [text, setText] = useState('')
+ const [audio, setAudio] = useState('')
+
+ const handlePress = ()=> {
+ router.navigate('/(mybusiness)/manageImages')
+ }
+
+ 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/(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 83d1b3c..a4dc312 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -9,12 +9,15 @@ 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/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/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 0000000..27beb40
Binary files /dev/null and b/assets/images/profile-robot.jpg differ
diff --git a/components/Home/completeProfile/completeProfile.tsx b/components/Home/completeProfile/completeProfile.tsx
new file mode 100644
index 0000000..0845dd7
--- /dev/null
+++ b/components/Home/completeProfile/completeProfile.tsx
@@ -0,0 +1,85 @@
+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);
+
+ 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();
+ }, []);
+
+ 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/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/editStory/audioBox.tsx b/components/editStory/audioBox.tsx
new file mode 100644
index 0000000..65a7c35
--- /dev/null
+++ b/components/editStory/audioBox.tsx
@@ -0,0 +1,58 @@
+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) {
+
+ audio
+ setAudio
+
+ 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..e125c04
--- /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..511fdb7
--- /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..f5ef1f1
--- /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..919abf8
--- /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/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/general/header.tsx b/components/general/header.tsx
index e1c8b9a..0ae4bc8 100644
--- a/components/general/header.tsx
+++ b/components/general/header.tsx
@@ -1,13 +1,22 @@
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?: IoniconName;
+ rightButtonColor?: string;
+ rightButtonBgColor?: string;
+ backButtonColor?: string;
+ backButtonBgColor?: string;
+ backButtonSize?: number;
};
export const Header = ({
@@ -16,6 +25,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 +38,38 @@ export const Header = ({
: () => router.navigate('/notifications');
return (
-
+
{showBackButton ? (
-
+
) : (
-
+
)}
{title}
{showNotificationButton ? (
-
+
) : (
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/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"
>
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/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 3ce2643..be048eb 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.`
: ''
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/index.ts b/types/index.ts
index ed5504d..28ea31c 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';
+};
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