├── .eslintrc.json ├── public ├── cart.png ├── favicon.ico ├── wallpaper.jpg └── loading-logo.png ├── .env.example ├── next.config.js ├── src ├── services │ ├── api.ts │ └── regex.ts ├── types │ └── index.ts ├── models │ ├── deleteAllItems.ts │ ├── getAllItems.ts │ ├── insertOneItem.ts │ ├── index.ts │ ├── database.ts │ └── updateOneItem.ts ├── pages │ ├── _document.tsx │ ├── checklist │ │ └── index.tsx │ ├── _app.tsx │ ├── api │ │ └── items.ts │ ├── components │ │ ├── LoadingScreen.tsx │ │ ├── DefaultContainer.tsx │ │ ├── Header.tsx │ │ ├── Message.tsx │ │ ├── DotsMenu.tsx │ │ ├── MessageInput.tsx │ │ ├── ItemsTable.tsx │ │ └── MessageContainer.tsx │ └── index.tsx └── styles │ └── globals.css ├── README.md ├── .gitignore ├── tsconfig.json └── package.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/cart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eliasnsz/lista-de-compras/HEAD/public/cart.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eliasnsz/lista-de-compras/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/wallpaper.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eliasnsz/lista-de-compras/HEAD/public/wallpaper.jpg -------------------------------------------------------------------------------- /public/loading-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eliasnsz/lista-de-compras/HEAD/public/loading-logo.png -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_URL=http://localhost:3000 2 | 3 | # MONGO DB 4 | MONGO_URI=your_mongo_uri 5 | MONGO_DB=your_db_name -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /src/services/api.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const appUrl = process.env.APP_URL as string 4 | 5 | const api = axios.create({ 6 | baseURL: appUrl + "/api", 7 | }) 8 | 9 | export default api -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from "mongodb"; 2 | 3 | export interface Item { 4 | _id: ObjectId; 5 | name: string; 6 | quantity: number; 7 | unit: "un" | "kg" | "g"; 8 | isChecked: boolean; 9 | created_at: string; 10 | } -------------------------------------------------------------------------------- /src/models/deleteAllItems.ts: -------------------------------------------------------------------------------- 1 | import connectToDatabase from "./database"; 2 | 3 | export const deleteAllItems = async () => { 4 | 5 | const db = await connectToDatabase(); 6 | const itemsCollection = db.collection("items"); 7 | 8 | await itemsCollection.deleteMany({}) 9 | } 10 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/models/getAllItems.ts: -------------------------------------------------------------------------------- 1 | import connectToDatabase from "./database"; 2 | 3 | export const getAllItems = async () => { 4 | 5 | const db = await connectToDatabase(); 6 | const itemsCollection = db.collection("items"); 7 | 8 | const findedItems = await itemsCollection.find({}).toArray(); 9 | return findedItems; 10 | } 11 | -------------------------------------------------------------------------------- /src/models/insertOneItem.ts: -------------------------------------------------------------------------------- 1 | import { Item } from "@/types"; 2 | import connectToDatabase from "./database"; 3 | 4 | export const insertOneItem = async (item: Item) => { 5 | const db = await connectToDatabase(); 6 | const itemsCollection = db.collection("items"); 7 | 8 | await itemsCollection.insertOne(item); 9 | return 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Lista de Compras 2 | 3 | Webapp que desenvolvi com objetivo de listar de compras mantendo a interface e a usabilidade do Whatsapp. 4 | 5 | O aplicativo usa regex para converter as mensagens enviadas em dados legíveis, para assim armazená-los em um banco de dados. 6 | 7 | 8 | ![enter image description here](https://i.imgur.com/qvzz85w.png) 9 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | import { getAllItems } from "./getAllItems"; 2 | import { insertOneItem } from "./insertOneItem"; 3 | import { updateOneItem } from "./updateOneItem"; 4 | import { deleteAllItems } from "./deleteAllItems"; 5 | 6 | const models = { 7 | getAllItems, 8 | insertOneItem, 9 | updateOneItem, 10 | deleteAllItems 11 | } 12 | 13 | export default models -------------------------------------------------------------------------------- /src/pages/checklist/index.tsx: -------------------------------------------------------------------------------- 1 | import DefaultContainer from "../components/DefaultContainer"; 2 | import ItemsTable from "../components/ItemsTable"; 3 | import Header from "../components/Header"; 4 | 5 | export default function Checklist() { 6 | 7 | return ( 8 | <> 9 | 10 |
11 | 12 | 13 | 14 | ) 15 | } -------------------------------------------------------------------------------- /src/models/database.ts: -------------------------------------------------------------------------------- 1 | import { Db, MongoClient } from "mongodb" 2 | 3 | const uri = process.env.MONGO_URI as string 4 | const databaseName = process.env.MONGO_DB as string 5 | 6 | let cachedDb: null | Db; 7 | 8 | export default async function connectToDatabase() { 9 | 10 | if (cachedDb) { 11 | return cachedDb; 12 | } 13 | 14 | const client = new MongoClient(uri) 15 | const db = client.db(databaseName) 16 | cachedDb = db 17 | 18 | return db 19 | } -------------------------------------------------------------------------------- /src/models/updateOneItem.ts: -------------------------------------------------------------------------------- 1 | import { Item } from "@/types"; 2 | import { ObjectId } from "mongodb"; 3 | import connectToDatabase from "./database"; 4 | 5 | export const updateOneItem = async (id: string) => { 6 | const db = await connectToDatabase(); 7 | const itemsCollection = db.collection("items"); 8 | 9 | const filter = { _id: new ObjectId(id) } 10 | 11 | await itemsCollection.findOneAndUpdate(filter, [{ $set: { isChecked: { $not: "$isChecked"} }}]) 12 | return 13 | } 14 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400;1,500;1,600;1,700;1,800&display=swap'); 2 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); 3 | 4 | * { 5 | margin: 0; 6 | padding: 0; 7 | scroll-behavior: smooth; 8 | font-family: 'Montserrat', sans-serif; 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | .vercel 40 | -------------------------------------------------------------------------------- /src/services/regex.ts: -------------------------------------------------------------------------------- 1 | import { Item } from "@/types"; 2 | 3 | export default function processInputWithRegex(text: string) { 4 | 5 | const regex = /^([1-9]\d*(?:\.\d+)?)\s*(k?g)?\s+(?:de\s+)?(.+)/i; 6 | const match = regex.exec(text); 7 | 8 | if (!match) { 9 | return { 10 | quantity: null, 11 | unit: 'un', 12 | name: text.trim(), 13 | isChecked: false 14 | }; 15 | } 16 | 17 | const quantity = parseFloat(match[1]); 18 | const unit = match[2]?.toLowerCase() || 'un'; 19 | const name = match[3].trim(); 20 | 21 | return { 22 | quantity, 23 | unit, 24 | name, 25 | } as Item; 26 | } -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { ChakraProvider } from "@chakra-ui/react" 2 | import type { AppProps } from 'next/app' 3 | import { QueryClientProvider, QueryClient } from "react-query" 4 | 5 | import '@/styles/globals.css' 6 | import "moment/locale/pt-br" 7 | 8 | import moment from 'moment' 9 | 10 | moment.locale("pt-br") 11 | 12 | const client = new QueryClient(); 13 | 14 | export default function App({ Component, pageProps }: AppProps) { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./src/*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/api/items.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | 3 | import models from '@/models' 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | 7 | if (req.method === 'GET') { 8 | const findedItems = await models.getAllItems() 9 | return res.status(200).json(findedItems) 10 | } 11 | 12 | if (req.method === 'POST') { 13 | const { item } = req.body 14 | await models.insertOneItem(item) 15 | return res.status(201).end(); 16 | } 17 | 18 | if (req.method === 'PUT') { 19 | const { id }: { id: string } = req.body 20 | await models.updateOneItem(id) 21 | return res.status(204).end(); 22 | } 23 | 24 | if (req.method === 'DELETE') { 25 | await models.deleteAllItems() 26 | return res.status(204).end(); 27 | } 28 | 29 | } 30 | 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "supermarket-list", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "test": "vitest", 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@chakra-ui/react": "^2.5.1", 14 | "@emotion/react": "^11.10.6", 15 | "@emotion/styled": "^11.10.6", 16 | "@fontsource/open-sans": "^4.5.14", 17 | "@types/node": "18.14.6", 18 | "@types/react": "18.0.28", 19 | "@types/react-dom": "18.0.11", 20 | "axios": "^1.3.4", 21 | "eslint": "8.35.0", 22 | "eslint-config-next": "13.2.3", 23 | "framer-motion": "^10.2.3", 24 | "moment": "^2.29.4", 25 | "mongodb": "^5.1.0", 26 | "next": "13.2.3", 27 | "react": "18.2.0", 28 | "react-dom": "18.2.0", 29 | "react-icons": "^4.8.0", 30 | "react-query": "^3.39.3", 31 | "typescript": "4.9.5" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/pages/components/LoadingScreen.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useState } from 'react' 2 | import { Center, Image } from '@chakra-ui/react' 3 | 4 | import DefaultContainer from './DefaultContainer' 5 | 6 | interface IProps {} 7 | 8 | const LoadingScreen:FC = (props) => { 9 | 10 | const [opacity, setOpacity] = useState(0) 11 | 12 | useEffect(() => { 13 | setTimeout(() => { 14 | setOpacity(1) 15 | }, 300); 16 | }, []) 17 | 18 | return ( 19 | 20 |
25 | loading-logo 33 |
34 |
35 | ) 36 | } 37 | 38 | export default LoadingScreen -------------------------------------------------------------------------------- /src/pages/components/DefaultContainer.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from '@chakra-ui/react' 2 | import Head from 'next/head' 3 | import React, { FC, ReactNode } from 'react' 4 | 5 | interface IProps { 6 | children: ReactNode 7 | } 8 | 9 | const DefaultContainer:FC = ({ children }) => { 10 | return ( 11 | <> 12 | 13 | Lista de Compras 14 | 15 | 16 | 17 | 18 | 31 | {children} 32 | 33 | 34 | ) 35 | } 36 | 37 | export default DefaultContainer -------------------------------------------------------------------------------- /src/pages/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, Box, Stack, Text } from '@chakra-ui/react' 2 | import { FC } from 'react' 3 | import DotsMenu from './DotsMenu' 4 | 5 | interface IProps { 6 | page: string 7 | } 8 | 9 | const Header:FC = ({ page }) => { 10 | 11 | return ( 12 | 23 | 28 | 33 | 37 | 38 | 41 | Lista de compras 42 | 43 | 44 | 45 | 46 | ) 47 | } 48 | 49 | export default Header -------------------------------------------------------------------------------- /src/pages/components/Message.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Icon, Stack, Text } from '@chakra-ui/react' 2 | import { FC, ReactNode } from 'react' 3 | import { BsCheckAll } from "react-icons/bs" 4 | 5 | import moment from 'moment' 6 | 7 | interface IProps { 8 | created_at: string 9 | children: ReactNode 10 | } 11 | 12 | const Message:FC = ({ children, created_at }) => { 13 | 14 | return ( 15 | 19 | 29 | 34 | 37 | {children} 38 | 39 | 45 | {moment(created_at).format("HH:mm")} 46 | 47 | 53 | 54 | 55 | 61 | 62 | ) 63 | } 64 | 65 | export default Message -------------------------------------------------------------------------------- /src/pages/components/DotsMenu.tsx: -------------------------------------------------------------------------------- 1 | import api from '@/services/api' 2 | import { Icon, IconButton, Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react' 3 | import Link from 'next/link' 4 | import React, { FC } from 'react' 5 | import { BiDotsVerticalRounded } from 'react-icons/bi' 6 | import { useQueryClient } from 'react-query' 7 | 8 | interface IProps { 9 | page: "main" | "checklist" 10 | } 11 | 12 | const DotsMenu:FC = ({ page }) => { 13 | 14 | const queryClient = useQueryClient() 15 | 16 | const handleDelete = async () => { 17 | await api.delete("/items") 18 | queryClient.invalidateQueries("items") 19 | } 20 | 21 | return ( 22 | 23 | } 30 | /> 31 | 37 | 44 | { page === "main" ? "Ir para a checagem" : "Ir para a listagem"} 45 | 46 | 52 | { page === "main" ? "Limpar lista" : "Concluir checagem"} 53 | 54 | 55 | 56 | ) 57 | } 58 | 59 | export default DotsMenu -------------------------------------------------------------------------------- /src/pages/components/MessageInput.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button, Center, ComponentWithAs, Icon, IconButton, Input, InputProps, Stack } from '@chakra-ui/react' 2 | import React, { Dispatch, FC, FormEvent, SetStateAction, useRef } from 'react' 3 | import { IoSendSharp } from 'react-icons/io5' 4 | import { MdEmojiEmotions } from 'react-icons/md' 5 | 6 | interface IProps { 7 | text: string, 8 | setText: Dispatch> 9 | isAllMatch: boolean 10 | handleSubmit: (e: FormEvent) => void 11 | } 12 | 13 | const MessageInput:FC = ({ setText, text, isAllMatch, handleSubmit }) => { 14 | 15 | return ( 16 |
17 |
20 | 25 | 35 | 42 | setText(e.target.value)} 49 | /> 50 | 51 | 69 | 70 |
71 |
72 | ) 73 | } 74 | 75 | export default MessageInput -------------------------------------------------------------------------------- /src/pages/components/ItemsTable.tsx: -------------------------------------------------------------------------------- 1 | import { TableContainer, Table, TableCaption, Thead, Tr, Th, Tbody, Td, Tfoot, Checkbox, Text, Center } from '@chakra-ui/react' 2 | import { useQueryClient, useQuery } from 'react-query' 3 | import { Item } from '@/types' 4 | import { FC, useState } from 'react' 5 | 6 | // import ItemsTableRow from './ItemsTableRow' 7 | import api from '@/services/api' 8 | 9 | interface IProps {} 10 | 11 | const ItemsTable:FC = (props) => { 12 | 13 | const { data: items, isLoading } = useQuery("items", async () => { 14 | const response = await api.get("/items") 15 | return response.data 16 | }, { 17 | staleTime: 1000 * 60, // 60s 18 | refetchOnWindowFocus: false 19 | }) 20 | 21 | if (!items?.length) { 22 | return ( 23 |
24 | Nenhum item encontrado 25 |
26 | ) 27 | } 28 | 29 | return ( 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | { 41 | items?.map((item, index) => { 42 | return 43 | }) 44 | } 45 | 46 |
QntProdutoChecagem
47 |
48 | ) 49 | } 50 | 51 | function ItemsTableRow({ item }: { item: Item}) { 52 | 53 | const [isChecked, setIsChecked] = useState(item?.isChecked) 54 | 55 | const queryClient = useQueryClient() 56 | 57 | const handleToggleCheck = async () => { 58 | await api.put("/items", { id: item?._id.toString() }) 59 | queryClient.invalidateQueries("items") 60 | } 61 | 62 | return ( 63 | {setIsChecked(state => !state), handleToggleCheck()}} 67 | > 68 | {item?.quantity} {item?.unit !== "un" && item?.unit} 69 | {item?.name} 70 | 71 | {setIsChecked(state => !state), handleToggleCheck()}} 76 | /> 77 | 78 | 79 | ) 80 | } 81 | 82 | export default ItemsTable -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery, useQueryClient } from 'react-query' 2 | import { useEffect, useState } from 'react' 3 | import { Item } from '@/types' 4 | 5 | import processInputWithRegex from '@/services/regex' 6 | import DefaultContainer from './components/DefaultContainer' 7 | import MessageContainer from './components/MessageContainer' 8 | import LoadingScreen from './components/LoadingScreen' 9 | import MessageInput from './components/MessageInput' 10 | import moment from 'moment' 11 | import Header from './components/Header' 12 | import api from '@/services/api' 13 | 14 | export default function Home() { 15 | 16 | const [idList, setIdList] = useState([]) 17 | 18 | const [text, setText] = useState("") 19 | const [isAllMatch, setIsAllMatch] = useState(false) 20 | const [loadingAnimation, setLoadingAnimation] = useState(true) 21 | 22 | const queryClient = useQueryClient() 23 | 24 | const { data: items, isLoading } = useQuery("items", async () => { 25 | const response = await api.get("/items") 26 | return response.data 27 | }, { 28 | staleTime: 1000 * 60, // 60s 29 | refetchOnWindowFocus: false 30 | }) 31 | 32 | useEffect(() => { 33 | const { name, quantity, unit } = processInputWithRegex(text) 34 | 35 | if ((!name || !unit || !quantity || !text)) { 36 | setIsAllMatch(false) 37 | } else { 38 | setIsAllMatch(true) 39 | } 40 | }, [text]) 41 | 42 | useEffect(() => { 43 | setTimeout(() => { 44 | setLoadingAnimation(false) 45 | }, 1500); 46 | }, []) 47 | 48 | const handleSubmit = async (e: any) => { 49 | e.preventDefault() 50 | if(!isAllMatch) return 51 | const input = e.target[0] as HTMLInputElement 52 | 53 | const { name, quantity, unit } = processInputWithRegex(text) 54 | const created_at = moment().toISOString() 55 | 56 | const newItem = { name, unit, quantity, created_at, isChecked: false } as Item 57 | 58 | await api.post("/items", { item: newItem }) 59 | queryClient.invalidateQueries("items") 60 | 61 | setText("") 62 | input.focus() 63 | } 64 | 65 | if (isLoading || loadingAnimation) { 66 | return 67 | } 68 | 69 | return ( 70 | <> 71 | 72 |
73 | 78 | 84 | 85 | 86 | ) 87 | } 88 | -------------------------------------------------------------------------------- /src/pages/components/MessageContainer.tsx: -------------------------------------------------------------------------------- 1 | import { Item } from '@/types' 2 | import { Box, Button, IconButton, Stack, Text } from '@chakra-ui/react' 3 | import React, { Dispatch, FC, ReactNode, RefObject, useRef, useState } from 'react' 4 | import { HiChevronDoubleDown } from "react-icons/hi" 5 | import Message from './Message' 6 | 7 | interface IProps { 8 | idList: string[] 9 | items: Item[] | undefined 10 | setIdList: Dispatch> 11 | } 12 | 13 | const MessageContainer:FC = ({ items, idList, setIdList }) => { 14 | 15 | const containerRef = useRef(null) 16 | const [isVisibleButton, setVisibleButton] = useState(false) 17 | 18 | const handleScroll = (event: any) => { 19 | const distanceToTop = Math.abs(event.target.scrollTop as number) 20 | if (distanceToTop >= 400) { 21 | setVisibleButton(true) 22 | } else { 23 | setVisibleButton(false) 24 | } 25 | } 26 | 27 | const scrollToBottom = () => { 28 | if (containerRef.current) { 29 | containerRef.current.scrollTop = 0 30 | } 31 | } 32 | 33 | console.log(idList); 34 | 35 | 36 | return ( 37 | <> 38 | handleScroll(event)} 48 | sx={{ "&::-webkit-scrollbar": { display: "none" } }} 49 | > 50 | { 51 | items?.map((item, index) => { 52 | return ( 53 | 57 | {`${item.quantity}${item.unit !== "un" ? item.unit : ""} ${item.name}`} 58 | 59 | ) 60 | }).reverse() 61 | } 62 | { isVisibleButton && 63 | ( 71 | 84 | ) 85 | } 86 | 87 | 88 | ) 89 | } 90 | 91 | export default MessageContainer --------------------------------------------------------------------------------