├── .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 |
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 | 
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 |
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 |
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 |
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 | | Qnt |
35 | Produto |
36 | Checagem |
37 |
38 |
39 |
40 | {
41 | items?.map((item, index) => {
42 | return
43 | })
44 | }
45 |
46 |
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
--------------------------------------------------------------------------------