diff --git a/.DS_Store b/.DS_Store index 7246d77..3ebfd57 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.github/.DS_Store b/.github/.DS_Store new file mode 100644 index 0000000..0d33471 Binary files /dev/null and b/.github/.DS_Store differ diff --git a/.gitignore b/.gitignore index c6176a5..82d3769 100644 --- a/.gitignore +++ b/.gitignore @@ -133,6 +133,8 @@ dmypy.json # Pyre type checker .pyre/ -shopsync-se-firebase-adminsdk-nkzuw-e871ea65d4.json -shopsync-se-firebase-adminsdk-nkzuw-5c1cd78bc9.json -shopsync-se-firebase-adminsdk-nkzuw-ca6838f54f.json \ No newline at end of file +# shopsync-se-firebase-adminsdk-nkzuw-e871ea65d4.json +# shopsync-se-firebase-adminsdk-nkzuw-5c1cd78bc9.json +# shopsync-se-firebase-adminsdk-nkzuw-ca6838f54f.json +# src/frontend/shopsync-9ecdc-firebase-adminsdk-60nyc-7e5a173fe8.json +shopsync-9ecdc-firebase-adminsdk-60nyc-7e5a173fe8.json \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index cc9f0a3..f140dea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -91,6 +91,7 @@ sniffio==1.3.1 soupsieve==2.6 starlette==0.38.6 streamlit==1.38.0 +streamlit-cookies-controller==0.0.4 streamlit-option-menu==0.4.0 tenacity==8.5.0 toml==0.10.2 diff --git a/src/.DS_Store b/src/.DS_Store index 403f523..541aa35 100644 Binary files a/src/.DS_Store and b/src/.DS_Store differ diff --git a/src/configs.py b/src/configs.py index 585928e..438b7d8 100644 --- a/src/configs.py +++ b/src/configs.py @@ -59,6 +59,8 @@ 'title_indicator': 'h4.sku-header a', 'price_indicator': 'div.priceView-customer-price span', 'link_indicator': 'a.image-link', + 'image_indicator': 'image.primary-image', + 'review_indicator': 'span.ugc-c-review-average' } diff --git a/src/configs_mt.py b/src/configs_mt.py index 6d620a4..d89592e 100644 --- a/src/configs_mt.py +++ b/src/configs_mt.py @@ -23,34 +23,54 @@ 'data-item-id': True }, 'title_indicator': 'span.lh-title', - 'price_indicator': 'div.lh-copy', - 'link_indicator': 'a' + 'price_indicator': 'div[data-automation-id="product-price"] span.w_iUH7', + 'link_indicator': 'a', + 'image_indicator': 'img.absolute.top-0.left-0', + 'review_indicator': 'span.stars-container' } -AMAZON = { - 'site': 'amazon', - 'url': 'https://www.amazon.com/s?k=', - 'item_component': 'div', - 'item_indicator': { - 'data-component-type': 's-search-result' - }, - 'title_indicator': 'h2 a span', - 'price_indicator': 'span.a-price span', - 'link_indicator': 'h2 a.a-link-normal' -} +# AMAZON = { +# 'site': 'amazon', +# 'url': 'https://www.amazon.com/s?k=', +# 'item_component': 'div', +# 'item_indicator': { +# 'data-component-type': 's-search-result' +# }, +# 'title_indicator': 'h2 a span', +# 'price_indicator': 'span.a-price span', +# 'link_indicator': 'h2 a.a-link-normal', +# 'image_indicator': 'img.s-image', +# 'review_indicator': 'span.a-declarative a i span' +# } COSTCO = { 'site': 'costco', 'url': 'https://www.costco.com/CatalogSearch?dept=All&keyword=', 'item_component': 'div', 'item_indicator': { - 'class': 'product-tile-set' + 'data-testid': 'Grid' }, - 'title_indicator': 'span a', - 'price_indicator': 'div.price', - 'link_indicator': 'span.description a', + 'title_indicator': 'div[data-testid^="Text_ProductTile_"]', # Extract the title from the span + 'price_indicator': 'div.MuiTypography-root', # Extract the price element + 'link_indicator': 'a', # Anchor element for the product link + 'image_indicator': 'img', # Use `img` tag for the product image + 'review_indicator': 'div.product-rating' # Review container (verify its presence) } +# TARGET = { +# 'site': 'target', +# 'url': 'https://www.target.com/s?searchTerm=', +# 'item_component': 'div', +# 'item_indicator': { +# 'data-test': '@web/ProductCard/ProductCardVariantDefault' +# }, +# 'title_indicator': 'a[data-test="product-title"]', +# 'price_indicator': 'span[data-test="product-price"]', +# 'link_indicator': 'a[data-test="product-title"]', +# 'image_indicator': 'img[data-test="product-image"]', +# 'review_indicator': 'div[data-test="average-rating"]' +# } + BESTBUY = { 'site': 'bestbuy', 'url': 'https://www.bestbuy.com/site/searchpage.jsp?st=', @@ -61,6 +81,8 @@ 'title_indicator': 'h4.sku-title a', 'price_indicator': 'div.priceView-customer-price span', 'link_indicator': 'a.image-link', + 'image_indicator': 'img.product-image', + 'review_indicator': 'div.c-ratings-reviews p' } @@ -85,43 +107,9 @@ def run(self): List of items from the dict """ - # api_url = 'https://redsky.target.com/redsky_aggregations/v1/web/plp_search_v1' - - # page = '/s/' + self.query - # params = { - # 'key': '5938CFDFD3FB4A7DB7C060583C86663C', - # 'channel': 'WEB', - # 'count': '24', - # 'default_purchasability_filter': 'false', - # 'include_sponsored': 'true', - # 'keyword': self.query, - # 'offset': '0', - # 'page': page, - # 'platform': 'desktop', - # 'pricing_store_id': '3991', - # 'useragent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0', - # 'visitor_id': 'AAA', - # } - - # data = requests.get(api_url, params=params).json() - # items = [] - # if 'search' in data['data']: - # for p in data['data']['search']['products']: - # item = { - # 'timestamp': datetime.now().strftime("%d/%m/%Y %H:%M:%S"), - # 'title': html.unescape(p['item']['product_description']['title']), - # 'price': '$' + str(p['price']['current_retail']), - # 'website': 'target', - # #'link': shorten_url(p['item']['enrichment']['buy_url']) - # 'link': p['item']['enrichment']['buy_url'] - # } - # items.append(item) - - # self.result = items - # set up the request parameters params = { - 'api_key': '5938CFDFD3FB4A7DB7C060583C86663C', - 'search_term': 'Iphone', + 'api_key': 'A6D50D2FA74944AEB91D4B18CBFDE4B0', + 'search_term': self.query, 'type': 'search', 'sort_by': 'best_match' } @@ -178,7 +166,7 @@ def run(self): self.result = [] data = response.dict() - + print(data) items = [] for p in data['searchResult']['item']: item = { @@ -187,11 +175,77 @@ def run(self): 'price': '$' + p['sellingStatus']['currentPrice']['value'], 'website': 'ebay', #'link': shorten_url(p['viewItemURL']) - 'link': p['viewItemURL'] + 'link': p['viewItemURL'], + 'image_url': p['galleryURL'] if 'galleryURL' in p else 'https://via.placeholder.com/150', + 'review': p['sellerInfo']['positiveFeedbackPercent'] + '%' if 'sellerInfo' in p and 'positiveFeedbackPercent' in p['sellerInfo'] else 'No Reviews' } items.append(item) self.result = items -CONFIGS = [WALMART, AMAZON, COSTCO, BESTBUY] + + +class scrape_amazon(Thread): + def __init__(self, query): + self.result = [] + self.query = query + super(scrape_amazon, self).__init__() + + def run(self): + """Scrape Amazon product data using Rainforest API + + Parameters + ---------- + query: str + Item to look for in the API + + Returns + ---------- + items: list + List of items from the API response + """ + API_KEY = '34C1B2689C074F3BB120B22625C22F72' + BASE_URL = 'https://api.rainforestapi.com/request' + + params = { + 'api_key': API_KEY, + 'type': 'search', + 'amazon_domain': 'amazon.com', + 'search_term': self.query, + } + + try: + # Send request to Rainforest API + response = requests.get(BASE_URL, params=params) + data = response.json() + + # Extract items from response + items = [] + if "search_results" in data: + for product in data["search_results"]: + try: + item = { + 'timestamp': datetime.now().strftime("%d/%m/%Y %H:%M:%S"), + 'title': product['title'] if 'title' in product else 'No Title', + 'price': f"${product['price']['value']}" if 'price' in product and 'value' in product['price'] else 'No Price', + 'currency': product['price']['currency'] if 'price' in product and 'currency' in product['price'] else 'USD', + 'website': 'amazon', + 'link': product['link'] if 'link' in product else 'No Link', + 'review': f"{product['rating']} stars" if 'rating' in product else 'No Rating', + 'image_url': product['image'] if 'image' in product else 'https://via.placeholder.com/150' + + } + items.append(item) + except Exception as e: + print(f"Error processing product: {e}") + continue + + self.result = items + + except Exception as e: + print(f"Error fetching data from Amazon API: {e}") + self.result = [] + + +CONFIGS = [WALMART, COSTCO, BESTBUY] diff --git a/src/formattr.py b/src/formattr.py index c165bd6..76de949 100644 --- a/src/formattr.py +++ b/src/formattr.py @@ -15,7 +15,7 @@ """ -def formatResult(website, titles, prices, links): +def formatResult(website, titles, prices, links, image_url=None, review=None): """ The formatResult function takes the scraped HTML as input, and extracts the necessary values from the HTML code. Ex. extracting a price '$19.99' from @@ -34,8 +34,10 @@ def formatResult(website, titles, prices, links): "price": price, "link": f'www.{website}.com{link}', "website": website, + "image_url": image_url or "https://via.placeholder.com/150", + "review": review or "No Reviews" } - print(product['title']) + #print(product['title']) if website=='walmart': if link[0:4]=='http': product['link']=f'{link}' diff --git a/src/frontend/.DS_Store b/src/frontend/.DS_Store new file mode 100644 index 0000000..2c4eff5 Binary files /dev/null and b/src/frontend/.DS_Store differ diff --git a/src/frontend/account.py b/src/frontend/account.py index acb488b..e55c1b0 100644 --- a/src/frontend/account.py +++ b/src/frontend/account.py @@ -11,6 +11,10 @@ from PIL import Image from PIL import Image from dotenv import load_dotenv +import json +import slash_user_interface as slash_user_interface +from streamlit_cookies_controller import CookieController +import time # Load environment variables from .env file load_dotenv() @@ -27,8 +31,8 @@ def initialize_firebase(mock=False): if not firebase_admin._apps: firebase_admin.initialize_app() return True + json_path = os.path.join(os.path.dirname(__file__), 'shopsync-9ecdc-firebase-adminsdk-60nyc-a335ead1ea.json') - json_path = os.path.join(os.path.dirname(__file__), 'shopsync-se-firebase-adminsdk-nkzuw-e871ea65d4.json') try: # Path to Firebase service account key cred = credentials.Certificate(json_path) @@ -209,6 +213,25 @@ def app(): # If login successful, update session state st.session_state.logged_in = True st.session_state.user_email = email + + with open('tokens.json') as openfile: + json_object = json.load(openfile) + + controller = CookieController() + token = controller.get("csrftoken") + + tokenData = { + "email": email, + "expiration": time.time() + 70000 + } + + json_object["tokens"][token] = tokenData + + json_output = json.dumps(json_object, indent=4) + + with open('tokens.json', 'w') as openfile: + openfile.write(json_output) + st.success('Logged in successfully!') st.rerun() # Rerun the app to reflect login state @@ -299,6 +322,19 @@ def signup(username, email, password): raise ValueError("An unexpected error occurred.") def logout(): + # Delete token on logout + with open('tokens.json', 'r') as openfile: + json_object = json.load(openfile) + controller = CookieController() + token = controller.get("csrftoken") + + del json_object["tokens"][token] + + json_output = json.dumps(json_object, indent=4) + + with open('tokens.json', 'w') as openfile: + openfile.write(json_output) + # Clear session state on logout st.session_state.logged_in = False st.session_state.user_email = None diff --git a/src/frontend/app.py b/src/frontend/app.py index 7d62a4f..e9bb35f 100644 --- a/src/frontend/app.py +++ b/src/frontend/app.py @@ -5,6 +5,11 @@ import slash_user_interface as slash_user_interface import favourites import logout # Import the logout module +import json +import history +import slash_user_interface as slash_user_interface +import time +from streamlit_cookies_controller import CookieController import warnings warnings.filterwarnings("ignore", category=UserWarning, module='streamlit') @@ -19,14 +24,40 @@ def add_app(self, title, function): }) def run(self): + # See if already logged in + with open('tokens.json', 'r') as openfile: + json_object = json.load(openfile) + controller = CookieController() + token = controller.get("csrftoken") + + if token not in json_object["tokens"]: + if 'logged_in' not in st.session_state: + st.session_state.logged_in = False + if 'user_email' not in st.session_state: + st.session_state.user_email = None + else: + if time.time() > json_object["tokens"][token]["expiration"]: + del json_object["tokens"][token] + + json_output = json.dumps(json_object, indent=4) + + with open('tokens.json', 'w') as openfile: + openfile.write(json_output) + + st.session_state.logged_in = False + st.session_state.user_email = None + else: + st.session_state.logged_in = True + st.session_state.user_email = json_object["tokens"][token]["email"] + # Determine the default selected app based on login status default_index = 0 if not st.session_state.get('logged_in') else 1 with st.sidebar: app = option_menu( menu_title='ShopSync', - options=['Account', 'Home', 'Favourites', 'Logout'], - icons=['person-circle', 'house-fill', 'star-fill', 'box-arrow-right'], + options=['Account', 'Home', 'Favourites', 'History', 'Logout'], + icons=['person-circle', 'house-fill', 'star-fill', 'box-arrow-right', 'box-arrow-right'], menu_icon='shop', default_index=default_index, styles={ @@ -52,7 +83,13 @@ def run(self): favourites.app() else: st.warning("You need to log in to access the favourites page.") - + + elif app == "History": + if st.session_state.get('logged_in'): + history.app() + else: + st.warning("You need to log in to access the history page.") + elif app == "Logout": logout.app() # Call the logout function @@ -62,4 +99,5 @@ def run(self): app.add_app("Account", account.app) app.add_app("Home", slash_user_interface.app) app.add_app("Favourites", favourites.app) + app.add_app("History", history.app) app.run() \ No newline at end of file diff --git a/src/frontend/favourites.py b/src/frontend/favourites.py index 4559a87..1a6642c 100644 --- a/src/frontend/favourites.py +++ b/src/frontend/favourites.py @@ -9,14 +9,12 @@ def fetch_title(): def initialize_firebase(mock=False): if mock: - # Mock initialization for testing purposes if not firebase_admin._apps: firebase_admin.initialize_app() return True - json_path = os.path.join(os.path.dirname(__file__), 'shopsync-se-firebase-adminsdk-nkzuw-e871ea65d4.json') + json_path = os.path.join(os.path.dirname(__file__), 'shopsync-9ecdc-firebase-adminsdk-60nyc-7e5a173fe8.json') try: - # Path to Firebase service account key cred = credentials.Certificate(json_path) firebase_admin.initialize_app(cred) return True @@ -24,19 +22,15 @@ def initialize_firebase(mock=False): print(f"Error initializing Firebase: {e}") raise e -# db = firestore.client() def app(firestore_client=None): - # Allow mock initialization if not firebase_admin._apps: initialize_firebase(mock=False) - - # If firestore_client is None, initialize it + if firestore_client is None: firestore_client = firestore.client() st.title("Favorites") - # Fetch the user ID (UID) from Firebase Authentication user_email = st.session_state.user_email # Ensure this is set user = auth.get_user_by_email(user_email) # Get the user by email uid = user.uid @@ -48,21 +42,37 @@ def app(firestore_client=None): if user_fav_doc.exists: user_fav_data = user_fav_doc.to_dict() - # Create a DataFrame from the user's favorites + # Convert user's favorites into a DataFrame favorites_df = pd.DataFrame({ + "Image_URL": user_fav_data["Image_URL"], "Description": user_fav_data["Description"], - "Link": user_fav_data["Link"], "Price": user_fav_data["Price"], "Product": user_fav_data["Product"], "Website": user_fav_data["Website"], }) # Display the favorites DataFrame - st.dataframe(favorites_df.style, column_config={ - "Link": st.column_config.LinkColumn("URL to Website"), - "Button": st.column_config.LinkColumn("Add to fav"), - }) + print("there") + for index, row in favorites_df.iterrows(): + st.write(f"### {row['Product']}") + st.write(f"**Description:** {row['Description']}") + st.write(f"**Price:** {row['Price']}") + st.write(f"**Website:** {row['Website']}") + st.markdown(f"[View Product]({row['Link']})", unsafe_allow_html=True) + + # Add a "Remove from Favorites" button + print("here") + if st.button(f"Remove {row['Product']}", key=f"remove_{index}"): + # Remove the product from Firestore + updated_favorites = { + "Description": [d for i, d in enumerate(user_fav_data["Description"]) if i != index], + "Link": [l for i, l in enumerate(user_fav_data["Link"]) if i != index], + "Price": [p for i, p in enumerate(user_fav_data["Price"]) if i != index], + "Product": [p for i, p in enumerate(user_fav_data["Product"]) if i != index], + "Website": [w for i, w in enumerate(user_fav_data["Website"]) if i != index], + } + user_fav_ref.set(updated_favorites) + st.success(f"{row['Product']} removed from favorites.") + #st.experimental_rerun() # Refresh the page after removal else: st.write("You have no favorites yet.") - -# In your main app file, call favourites.app() to use this diff --git a/src/frontend/firebase.py b/src/frontend/firebase.py index fa3dc03..828c98e 100644 --- a/src/frontend/firebase.py +++ b/src/frontend/firebase.py @@ -4,7 +4,7 @@ def initialize_firebase(): # Check if Firebase has already been initialized if not firebase_admin._apps: - cred = credentials.Certificate('shopsync-se-firebase-adminsdk-nkzuw-e871ea65d4.json') + cred = credentials.Certificate('shopsync-9ecdc-firebase-adminsdk-60nyc-7e5a173fe8.json') # cred = credentials.Certificate('shopsync-se-firebase-adminsdk-nkzuw-ca6838f54f.json') firebase_admin.initialize_app(cred) \ No newline at end of file diff --git a/src/frontend/history.py b/src/frontend/history.py new file mode 100644 index 0000000..f59f99e --- /dev/null +++ b/src/frontend/history.py @@ -0,0 +1,71 @@ +import streamlit as st +import firebase_admin +from firebase_admin import credentials, firestore, auth +import pandas as pd +import os + +def fetch_title(): + return "History" + +def initialize_firebase(mock=False): + if mock: + # Mock initialization for testing purposes + if not firebase_admin._apps: + firebase_admin.initialize_app() + return True + + json_path = os.path.join(os.path.dirname(__file__), 'shopsync-se-firebase-adminsdk-nkzuw-e871ea65d4.json') + try: + # Path to Firebase service account key + cred = credentials.Certificate(json_path) + firebase_admin.initialize_app(cred) + return True + except Exception as e: + print(f"Error initializing Firebase: {e}") + raise e + +# db = firestore.client() +def app(firestore_client=None): + # Allow mock initialization + if not firebase_admin._apps: + initialize_firebase(mock=False) + + # If firestore_client is None, initialize it + if firestore_client is None: + firestore_client = firestore.client() + + st.title("History") + + # Fetch the user ID (UID) from Firebase Authentication + user_email = st.session_state.user_email # Ensure this is set + user = auth.get_user_by_email(user_email) # Get the user by email + uid = user.uid + + # Reference to the user's document in the "history" collection + user_his_ref = firestore_client.collection("history").document(uid) + user_his_doc = user_his_ref.get() + + if user_his_doc.exists: + user_his_data = user_his_doc.to_dict() + + timestamps = list() + for stamp in user_his_data["Timestamp"]: + temp = pd.Timestamp(stamp, unit='s', tz='US/Eastern') + # temp.floor(freq='h') + timestamps.append(temp.floor(freq='s')) + + # Create a DataFrame from the user's history + history_df = pd.DataFrame({ + "Search": user_his_data["Search"], + "Timestamp": timestamps + }) + + # Display the history DataFrame + st.dataframe(history_df.style, column_config={ + "Link": st.column_config.LinkColumn("URL to Website"), + "Button": st.column_config.LinkColumn("Add to history"), + }) + else: + st.write("You have no history yet.") + +# In your main app file, call history.app() to use this diff --git a/src/frontend/logger.txt b/src/frontend/logger.txt index 2cef09f..d239385 100644 --- a/src/frontend/logger.txt +++ b/src/frontend/logger.txt @@ -49,3 +49,162 @@ amazon query:iphone amazon query:iphone amazon query:iphone amazon query:ginger +amazon query:watch +amazon query:bag +amazon query:shirt +amazon query:bag +amazon query:iphone +amazon query:dunks +amazon query:banana +amazon query:t shirt +amazon query:apple +amazon query:banana +amazon query:apple +amazon query:water +amazon query:glass +amazon query:popcorn +amazon query:plate +amazon query:banana +amazon query:wine +amazon query:water +amazon query:mango +amazon query:bananna +amazon query:coffee +amazon query:cocoa +amazon query:apple +amazon query:shoes +amazon query:Icecream +amazon query:Icecream +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Apple +amazon query:Apple +amazon query:Banana +amazon query:Milk +amazon query:Milk +amazon query:Apple +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:milk +amazon query:Milk +amazon query:Apple +amazon query:Apple +amazon query:Apple +amazon query:Airpod +amazon query:Airpod +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Apple +amazon query:Apple +amazon query:Milk +amazon query:Milk +amazon query:Iphone +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Apple +amazon query:Apple +amazon query:Apple +amazon query:Apple +amazon query:Milk +amazon query:Milk +amazon query:Phone +amazon query:Apple +amazon query:Apples +amazon query:Apples +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:Milk +amazon query:milk +amazon query:milk +amazon query:milk +amazon query:milk +amazon query:milk +amazon query:milk +amazon query:milk +amazon query:Apple +amazon query:Apples +amazon query:Apples +amazon query:Apples +amazon query:Apples +amazon query:milk +amazon query:milk +amazon query:Apples +amazon query:Apples +amazon query:Apples diff --git a/src/frontend/shopsync-9ecdc-firebase-adminsdk-60nyc-05d8e88f22.json b/src/frontend/shopsync-9ecdc-firebase-adminsdk-60nyc-05d8e88f22.json new file mode 100644 index 0000000..d563401 --- /dev/null +++ b/src/frontend/shopsync-9ecdc-firebase-adminsdk-60nyc-05d8e88f22.json @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "shopsync-9ecdc", + "private_key_id": "05d8e88f22ac83c91b58b295a781a55ed4c4990e", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5R1i2UbzZ77zD\nOhrxbWA1VMtX2sZuplbpyZGHevDVBw3zeZBtQsdc8xTy/ooF8Z7WIejWH1nVGKYe\nmEMvl9g2B8mYrd1UNdEEYL0X/GRl6BVsog5n0TGyV4wwiUkZmFM9RClr0R4hVxhs\nCuAyQUpcr2Srqmd3SNllsFkWGpwvOKsKTSeCG/n2Ott7FqxyXR4OnHTrEjCO2brb\nGU5/J7d92XFUc/m9GGWALSyxel6qQ5moAinPX/oeCkiV8FVsUQsStUDkl7HjzsWg\n7eneBYiqzSacILCmqCqxdCe32M5MHWdi/yzSNSUW2xY2Bv81XOETOxmUBP3RAQeO\ng8ELG1NxAgMBAAECggEAGLpCR2hL9soy5zCvZ96xIxeuC+D8RXLt0UXq+6nsz9b2\nhbLeaQwAj5HhueHpieGK0V61NIlP8/YriunHYxK2SH/BksmhlcZDnydKFWlvUip8\nsYLvaUECNksjlVa9QC0+7sOqGORP9nh+n0IpqeC3i/yHSGH1wnXL40Z7R/fv4F1K\nz1UwTCwAFRApSRKylAX7ZM9CbDu3r4ZQD5g98CwDhRgvC+FOVPT5L0ogC5HFs/ZO\nLkWqQpqw9Oo5RNhlkjqVEqPNSKytb7esYp1zsA7jfWyspktoLHGGvbQvjDeCSCtC\nF3Fo+bgB2B/WTmb9D5WGCjgpxC1uRcwLw8OeZPhmAQKBgQDr87Xq7qoHq2xu64zH\nIpTQjywjRklwMlqsv0g0lsNHU+2BSzLn+KNtlu5Ad92wCXwWCbZ+twoTfcHf6XqL\nuVzkOACDzqamJtHH1YZqW+vC7Y+SKP91LoQS1QyYfZOFebZJEoHaogPYDqCcFsgu\nRHh7tS4YYdFmr3GNL7uWSJluowKBgQDJBWuKRdEel8FcNl5Q0n9sFtqQ1soym55J\naK+cyJHBJV1Mo5/VfI4njV0RrYoRvP5l02mLoi5N0XBEVIdHM+LEcCTRozsDahtj\nbNVcxXWcK0jWFHR6JZf7AWRco+Xz6guoOcC+MbmMP3AQftsz3t2E6Bf4j842GZwT\ngewBnGB62wKBgQDRZIac5xCdndOs2/0i+910+JoC8+1YVFeD1n0Nrn2+Xwz3IPUc\nR9tA9iCZtcZW4xPrutLpwSaABap4O4s1VFrLbaeHUW0zJmAlJ6kR2mFvq8MtwpRy\nOcWbsNZsvYSdf1X1oyb6D625n8GIw+8CoGEL583wdV6P8kKjOSkQRX1kYQKBgQC6\nRgx95+489BDYWwUQzc4HojHMj0x0kuGdUqWQmgb+PJp1HxZIJJAxtHvumqnbgA8Y\n2kvueU2BDLeEifOFFl5m+ygTHrfblSJmAn6/5bXzDeUDg5bfbSClFogilDnMyS8e\nJs4lMDyo6kv07ShAq58Hvm4gBVnnpdmL9hN09qwsiwKBgAecKahDlhqyLyrVky9U\nJkcvoiFCHdfaq9nVsBvGKPrhcgpRHcsdJW5DQWvgVuHxldX5oVcsWTK928t0g7NT\nK2lJUKPzWFjloTtVjwitLSeRzZwiQyuXCdcyQue6nbYFdBj81itYrHyxFUsEdNtO\nVh2hCgwDliUj3b0sgwVqtj7B\n-----END PRIVATE KEY-----\n", + "client_email": "firebase-adminsdk-60nyc@shopsync-9ecdc.iam.gserviceaccount.com", + "client_id": "111221445997110214622", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-60nyc%40shopsync-9ecdc.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +} diff --git a/src/frontend/shopsync-9ecdc-firebase-adminsdk-60nyc-a335ead1ea.json b/src/frontend/shopsync-9ecdc-firebase-adminsdk-60nyc-a335ead1ea.json new file mode 100644 index 0000000..3c795ac --- /dev/null +++ b/src/frontend/shopsync-9ecdc-firebase-adminsdk-60nyc-a335ead1ea.json @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "shopsync-9ecdc", + "private_key_id": "a335ead1ea5c35451f12689886264cd6c6e1a059", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQDcWezpDyRYN8JX\nxMyY0k+N/bTV3WWT6+Z3VT3RYkRMoPWtQ3HHXF9pnYubsLl/q640nEvA1dLZIUZI\nKdAxRdeHjdEV91olvAAsy7WLAMJhJjDrP3vIfMfricsYc/9niFBfivZJP5AgwN3K\nPq6VH77KQrB9DTTGsTk6wPAtH6Doa2tAmxGct6FGgY5nG6Fs8BxXSCzvcfPCLvDv\nngPJaFeloXlA4kDmXPwT66cgC1nUjATN49DC7nw1o9Y8xJ9Jc5v8MxjHTy7C/7Pi\nmK6dyIcUb76zAwa+hc6MINaR4jlAnQWhXvtXm2jKGAZCZ22gyEuvfQLqLX/MgMrL\nW4pkfLBrAgMBAAECggEAAn8tTtRcQqlys4I6embSItpjlsROVWsn/bp/tBUGzMKf\n9IBhEULftsCgp2n9lEmk1fJk2+twVPenStAVhm2YGGQ70YlaSGfLcJ4e9f395p5t\n/2zIIWcgXdeEUPjg18ZoqchLGWjytZXTBGSfk0Z5Z8u7m1BH+HPplUNxTygPR+j8\nxuc8j+RGcri7BzuSyTUybsCI1x0cfq5uqp32Hqk6CfXBg6fxhpYO7yVk7N6wT6ec\nKHF6ngRHXQe1dzDHMGi+iHAT01kkx/nofmKdV+HqHyxPm4rbgQOD0pn1ZEADY1PG\n5cvR8cQjWkV1yVrQ9vF6LDoEkHzHPqcfF4GAsNJYBQKBgQD2TZk1h9nY4isqg40i\n00Vp6PqGPqABCRBm0msGMh+CsVy8byDAnLaIRDlgo+JI/FvzfIwZuoeCyFPTkdgN\nKFNZhykOm43FBLXR9PxGlk7CuUK5ASYmJfxufnDenatU+XfdeaL5xFMRSODexrSq\nFmAS5TEU2e+YLqdyc/r1C2Q7DQKBgQDlBsR73Sg6MGayCrs2ruHW3g8ERtahXODH\nACXf5sACUjsV6jr3aKGR31vnABTcZEjrof0Ni/8LbWtBG6J+bmcjsC5VyMLOC4jh\nmvwIewmtQutzOVLaBkvF3/p7wIt+UteYeSPvbjQAnwi540oEktk9N8qkv2GgzAH/\nRfGVjmtbVwKBgHF45JnN4aZS5FIs0yv1K6iUhj6swWhYta65SEdNdkjuz2ucwvkZ\n+dojnE+SkSDQ6sftXFpKHj45bq0tJt1A881uQJMTRSg8eEunU0Zt3xFE6qFzDxFK\nNNbu968H8rQuTnPBozzwnth6u+bGotstfcuWvZr+oKx66fgHyNl2CxJNAoGAYSda\nyFSL0QthNRu6STsskHKImj7Wo4L7008rwexn/VQWvngrZXKcP34pxTdSoh9kk5iW\n+V0u5xEWk3r+lnWNCSWeskNE6BUajuGpEovnEfm2WZ2ymMxc7mbSIhcO1Zqc3JBe\n/x2Xr7/G+twBNSl6QC7fpr2M06JXIovwLIpK3mcCfxLaw55CjnkF8QdPW22RE53g\n5tRzEHHkrCiQBeDnZAP7mCO8I7bd+7qcsuHi4aVAiRp3aeniUnAehW8W6oLgJiNk\nOLRpPU6IWkjFSyyfyvtYHGOkFJxqmQM6vIMkpQx9BfzjjuZXPk4HGhNq3lfW6lfc\nhZPy6tWfQYc4xwDQrEA=\n-----END PRIVATE KEY-----\n", + "client_email": "firebase-adminsdk-60nyc@shopsync-9ecdc.iam.gserviceaccount.com", + "client_id": "111221445997110214622", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-60nyc%40shopsync-9ecdc.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +} diff --git a/src/frontend/slash_user_interface.py b/src/frontend/slash_user_interface.py index df0c7ca..fe3c703 100644 --- a/src/frontend/slash_user_interface.py +++ b/src/frontend/slash_user_interface.py @@ -7,6 +7,7 @@ # Import Libraries import os import sys +import time sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) import streamlit.components.v1 as components import re @@ -16,6 +17,8 @@ from main_streamlit import search_items_API import streamlit as st from firebase_admin import firestore, auth +from bs4 import BeautifulSoup +import requests # sys.path.append('../') # st.set_page_config(layout= "wide") # st.title("ShopSync") @@ -32,8 +35,144 @@ def create_app(db_client=None): db_client = get_firestore_client() def search_product(website, product_name): - return search_items_API(website, product_name) + results = search_items_API(website, product_name) + #print(results) + return results + + +def search_walmart_product(query): + api_key = "YOUR_WALMART_API_KEY" + url = f"https://api.walmartlabs.com/v1/search" + params = { + "query": query, + "format": "json", + "apiKey": api_key + } + response = requests.get(url, params=params) + if response.status_code == 200: + data = response.json() + return [{ + "title": item.get("name"), + "price": f"${item.get('salePrice', 'N/A')}", + "link": item.get("productUrl"), + "image_url": item.get("thumbnailImage", "https://via.placeholder.com/150") + } for item in data.get("items", [])] + return [] + +def search_amazon_product(query): + url = "https://amazon24.p.rapidapi.com/api/product" + headers = { + "X-RapidAPI-Key": "YOUR_RAPIDAPI_KEY", + "X-RapidAPI-Host": "amazon24.p.rapidapi.com" + } + params = {"country": "US", "keyword": query} + response = requests.get(url, headers=headers, params=params) + if response.status_code == 200: + data = response.json() + return [{ + "title": product.get("title"), + "price": product.get("price"), + "link": product.get("link"), + "image_url": product.get("thumbnail") + } for product in data.get("docs", [])] + return [] + +def search_ebay_product(query): + app_id = "YOUR_EBAY_APP_ID" + url = "https://svcs.ebay.com/services/search/FindingService/v1" + params = { + "OPERATION-NAME": "findItemsByKeywords", + "SERVICE-VERSION": "1.0.0", + "SECURITY-APPNAME": app_id, + "RESPONSE-DATA-FORMAT": "JSON", + "keywords": query + } + response = requests.get(url, params=params) + if response.status_code == 200: + data = response.json() + return [{ + "title": item.get("title")[0], + "price": item.get("sellingStatus", [{}])[0].get("currentPrice", [{}])[0].get("__value__"), + "link": item.get("viewItemURL", ["#"])[0], + "image_url": item.get("galleryURL", ["https://via.placeholder.com/150"])[0] + } for item in data.get("findItemsByKeywordsResponse", [{}])[0].get("searchResult", [{}])[0].get("item", [])] + return [] + +def search_bestbuy_product(query): + api_key = "YOUR_BESTBUY_API_KEY" + url = f"https://api.bestbuy.com/v1/products((search={query}))" + params = { + "format": "json", + "apiKey": api_key + } + response = requests.get(url, params=params) + if response.status_code == 200: + data = response.json() + return [{ + "title": product.get("name"), + "price": product.get("salePrice"), + "link": product.get("url"), + "image_url": product.get("image") + } for product in data.get("products", [])] + return [] + +def search_target_product(query): + url = "https://target1.p.rapidapi.com/products/v3/search" + headers = { + "X-RapidAPI-Key": "YOUR_RAPIDAPI_KEY", + "X-RapidAPI-Host": "target1.p.rapidapi.com" + } + params = {"keyword": query, "count": 10} + + response = requests.get(url, headers=headers, params=params) + if response.status_code == 200: + data = response.json() + return [{ + "title": item.get("title"), + "price": item.get("price", {}).get("formatted_current_price"), + "link": item.get("url"), + "image_url": item.get("images", [{}])[0].get("base_url") + item.get("images", [{}])[0].get("primary") + } for item in data.get("data", {}).get("search", {}).get("products", [])] + return [] + +def search_costco_product(query): + url = "https://costco4.p.rapidapi.com/search" + headers = { + "X-RapidAPI-Key": "YOUR_RAPIDAPI_KEY", + "X-RapidAPI-Host": "costco4.p.rapidapi.com" + } + params = {"q": query} + + response = requests.get(url, headers=headers, params=params) + if response.status_code == 200: + data = response.json() + return [{ + "title": item.get("name"), + "price": item.get("price"), + "link": item.get("url"), + "image_url": item.get("image") + } for item in data.get("products", [])] + return [] + + +def fetch_image_from_bing(product_name): + api_key = "YOUR_BING_API_KEY" + search_url = "https://api.bing.microsoft.com/v7.0/images/search" + headers = {"Ocp-Apim-Subscription-Key": api_key} + params = {"q": product_name, "count": 1} # Limit to 1 image for speed + + try: + response = requests.get(search_url, headers=headers, params=params) + if response.status_code == 200: + data = response.json() + if "value" in data and len(data["value"]) > 0: + return data["value"][0]["contentUrl"] # Return the first image URL + except Exception as e: + print(f"Error fetching image: {e}") + + return "https://via.placeholder.com/150" # Default image if no result + def check_product_input(product): """Check if the product input is valid based on multiple criteria.""" # Check for non-empty input @@ -137,15 +276,32 @@ def split_description(description): margin-top: -40px; } /* Adjust the select box width */ + # .stSelectbox { + # font-size: 400px !important; + # width: 100% !important; + # max-width: 500px; + # margin-top: -40px; + # } .stSelectbox { font-size: 400px !important; width: 100% !important; max-width: 500px; margin-top: -40px; } + + /* Align checkboxes to the right */ + div[data-testid="stHorizontalBlock"] { + display: flex; + justify-content: right; /* Align checkboxes to the right */ + } + + label { + margin-left: auto; /* Push label text to the left */ + text-align: right; /* Align text to the right */ + } .stSlider{ margin-top: -30px; - margin-left: 5px; + margin-left: -65px; width: 400px !important;} """, @@ -245,13 +401,39 @@ def reset_button(): company_list = conf.getCompanies() # results = search_product(website, product) results = search_product(website_dict[website], product) + + user = auth.get_user_by_email(st.session_state.user_email) # Replace with actual user email + uid = user.uid + + # Reference to the user's document in "hisourites" collection + user_his_ref = db.collection("history").document(uid) + + # Get the user's current hisorites data, or create a new structure if it doesn't exist + user_his_doc = user_his_ref.get() + + if user_his_doc.exists: + # If the document exists, retrieve the current data + user_his_data = user_his_doc.to_dict() + else: + # Initialize empty arrays if document doesn't exist + user_his_data = { + "Search": [], + "Timestamp": [] + } + + user_his_data["Search"].append(product) # Access the actual value + user_his_data["Timestamp"].append(time.time()) # Access the actual value + + # Update the user's document in Firestore with the new data + user_his_ref.set(user_his_data) + # Use st.columns based on return values description = [] url = [] price = [] site = [] - # rakuten = [] rating = [] + image = [] import random @@ -278,17 +460,20 @@ def get_random_value_from_list(lst): description.append(result['title']) url.append(result['link']) price_str = result['price'] - rating_value = get_random_value_from_list(my_list) - + rating_value = result.get('review', '0') # Safely access 'review' + print(rating_value) + image.append(result['image_url']) # Clean and extract price clean_price_str = re.sub(r'[^\d\.\,]', '', price_str) # Remove unwanted characters match = re.search(r'(\d{1,3}(?:,\d{3})*(?:\.\d{1,2})?)', clean_price_str) - + rating_matches = re.findall(r"\d+(?:\.\d+)?", rating_value) + print(rating_matches) + rating_float = [float(match) for match in rating_matches[:1]] if match: price_str = match.group(0).replace(',', '') # Remove commas for conversion price_f = float(price_str) price.append(price_f) - rating.append(rating_value) # Append rating only if price is valid + rating.append(rating_float) # Append rating only if price is valid else: print("Unable to extract a valid price from the string:", price_str) price.append(None) # Append None if price extraction fails @@ -300,7 +485,22 @@ def get_random_value_from_list(lst): if len(price): dataframe = pd.DataFrame( - {'Description': description, 'Price': price, 'Link': url, 'Website': site,'Ratings': rating}) + {'Image_URL': image, 'Description': description, 'Price': price, 'Link': url, 'Website': site,'Ratings': rating}) + + def add_http_if_not_present(url): + if url.startswith('http://') or url.startswith('https://'): + return url + else: + return 'https://' + url + dataframe['Link'] = dataframe['Link'].apply(add_http_if_not_present) + + dataframe['Image'] = dataframe.apply( + lambda row: f'', + axis=1 + ) + + dataframe = dataframe.drop(["Image_URL", "Link"], axis=1) + dataframe['Description'] = dataframe['Description'].apply( split_description) dataframe['Product'] = dataframe['Description'].str.split( @@ -311,18 +511,29 @@ def get_random_value_from_list(lst): dataframe.insert(0, 'Product', product_column) dataframe['Price'] = dataframe['Price'].apply( - lambda x: float(f'{x:.2f}')) + lambda x: float(f'{float(x):.2f}') if pd.notnull(x) and str(x).replace('.', '', 1).isdigit() else None + ) # dataframe = dataframe.sort_values(by='Price', ascending=True) dataframe = dataframe.reset_index(drop=True) - dataframe['Price'] = [f'{x:.2f}' for x in dataframe['Price']] + dataframe['Price'] = dataframe['Price'].apply(lambda x: f'{x:.2f}' if x is not None else 'N/A') + - def add_http_if_not_present(url): - if url.startswith('http://') or url.startswith('https://'): - return url - else: - return 'https://' + url - dataframe['Link'] = dataframe['Link'].apply(add_http_if_not_present) st.session_state['dataframe'] = dataframe + + styled_table = ( + dataframe.style + .set_properties(**{'text-align': 'center'}) + .set_table_styles([ + {"selector": "th", "props": [("text-align", "center")]}, + {"selector": "td", "props": [("text-align", "center")]} + ]) + ) + + # st.markdown( + # styled_table.to_html(escape=False), # Allow HTML content + # unsafe_allow_html=True + # ) + st.success("Data successfully scraped and cached.") st.session_state.dataframe = dataframe @@ -370,7 +581,7 @@ def get_button_indices(button_ix): st.markdown("

Result

", unsafe_allow_html=True) - col1, col2 = st.columns([1, 2])# adjust the columns for price range and filters + col1, col2 = st.columns([1, 1])# adjust the columns for price range and filters with col1: st.session_state.dataframe['Price'] = pd.to_numeric( @@ -454,13 +665,26 @@ def get_button_indices(button_ix): ) # Display styled DataFrame - st.dataframe( - styled_df, - column_config={"Link": st.column_config.LinkColumn("URL to website")}, - use_container_width=True # Ensure the DataFrame uses the maximum width + # st.dataframe( + # styled_df, + # column_config={"Link": st.column_config.LinkColumn("URL to website")}, + # use_container_width=True # Ensure the DataFrame uses the maximum width + # ) + styled_table = ( + filtered_df.style + .set_properties(**{'text-align': 'center'}) + .set_table_styles([ + {"selector": "th", "props": [("text-align", "center")]}, + {"selector": "td", "props": [("text-align", "center")]} + ]) + ) + + st.markdown( + styled_table.to_html(escape=False), # Allow HTML content + unsafe_allow_html=True ) - st.markdown("

Add for favourites

", - unsafe_allow_html=True) + # st.markdown("

Add for favourites

", + # unsafe_allow_html=True) # st.write('Add for favorites', unsafe_allow_html=True) # ////////////////////////////////////////////////////////////////////////////////////////////// # Prints the websites names from filter and dataframe to check @@ -474,63 +698,68 @@ def get_button_indices(button_ix): #/////////////////////////////////////////////////////////////////////////////////////////////// if st.session_state.dataframe is not None: + # Add a label for the selectbox st.markdown('Select index to add to favourites', unsafe_allow_html=True) - # Display the selectbox - selected_index = st.selectbox("", [None] + list(range(len(st.session_state.get("dataframe", []))))) + # Create a selectbox to choose an index from the dataframe + selected_index = st.selectbox( + "", + [None] + list(range(len(st.session_state.dataframe))), # Include 'None' for no selection + format_func=lambda x: f"Row {x}" if x is not None else "Select a row" + ) # Close the container div st.markdown("", unsafe_allow_html=True) + # Check if a valid row is selected if selected_index is not None: - fav = pd.DataFrame([st.session_state.dataframe.iloc[selected_index]]) + # Extract the selected row with required columns + fav_row = st.session_state.dataframe.loc[ + [selected_index], + ['Product', 'Description', 'Price', 'Website', 'Ratings', 'Image'] + ] + + fav_row['Ratings'] = fav_row['Ratings'].apply(lambda x: x[0] if isinstance(x, list) and len(x) > 0 else None) + # Append the row to the favorites table in session state if 'fav' in st.session_state: st.session_state.fav = pd.concat( - [st.session_state.fav, fav], axis=0).drop_duplicates() - st.dataframe(st.session_state.fav.style, column_config={"Link": st.column_config.LinkColumn( - "URL to website"), "Button": st.column_config.LinkColumn("Add to fav")},) - + [st.session_state.fav, fav_row], axis=0 + ).drop_duplicates() else: - st.session_state.fav = fav.copy() - st.dataframe(fav.style, column_config={"Link": st.column_config.LinkColumn( - "URL to website"), "Button": st.column_config.LinkColumn("Add to fav")},) + st.session_state.fav = fav_row + + styled_table = ( + st.session_state.fav.style + .set_properties(**{'text-align': 'center'}) + .set_table_styles([ + {"selector": "th", "props": [("text-align", "center")]}, + {"selector": "td", "props": [("text-align", "center")]} + ]) + ) + st.markdown( + styled_table.to_html(escape=False), # Allow HTML content + unsafe_allow_html=True + ) + #st.dataframe(st.session_state.fav[['Product', 'Description', 'Price', 'Website', 'Ratings', 'Image']], use_container_width=True) + + # Save the favorites table to Firestore user = auth.get_user_by_email(st.session_state.user_email) # Replace with actual user email uid = user.uid # Reference to the user's document in "favourites" collection user_fav_ref = db.collection("favourites").document(uid) - # Get the user's current favorites data, or create a new structure if it doesn't exist - user_fav_doc = user_fav_ref.get() - - if user_fav_doc.exists: - # If the document exists, retrieve the current data - user_fav_data = user_fav_doc.to_dict() - else: - # Initialize empty arrays if document doesn't exist - user_fav_data = { - "Description": [], - "Link": [], - "Price": [], - "Product": [], - # "Rating": [], - "Website": [] - } - - user_fav_data["Description"].append(fav["Description"].values[0]) # Access the actual value - user_fav_data["Link"].append(fav["Link"].values[0]) # Access the actual value - user_fav_data["Price"].append(fav["Price"].values[0]) # Access the actual value - user_fav_data["Product"].append(fav["Product"].values[0]) # Access the actual value - # user_fav_data["Rating"].append(fav["Rating"].values[0]) # Access the actual value - user_fav_data["Website"].append(fav["Website"].values[0]) # Access the actual value - - # Update the user's document in Firestore with the new data + # Convert favorites table to a dictionary format compatible with Firestore + user_fav_data = st.session_state.fav[['Product', 'Description', 'Price', 'Website', 'Ratings', 'Image']].to_dict(orient='list') + + # Save to Firestore user_fav_ref.set(user_fav_data) - - st.success(f"{product} has been added to your favorites!") + st.success(f"{st.session_state.dataframe.loc[selected_index, 'Product']} has been added to your favorites!") + + # Add footer to UI footer = """