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