Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -603,725 +603,3 @@ def main():
|
|
603 |
|
604 |
if __name__ == "__main__":
|
605 |
main()
|
606 |
-
import os
|
607 |
-
import functools
|
608 |
-
|
609 |
-
# Set page configuration
|
610 |
-
st.set_page_config(
|
611 |
-
page_title="PetMatch - Find Your Perfect Pet",
|
612 |
-
page_icon="🐾",
|
613 |
-
layout="centered"
|
614 |
-
)
|
615 |
-
|
616 |
-
# Custom CSS
|
617 |
-
st.markdown("""
|
618 |
-
<style>
|
619 |
-
.main-header {
|
620 |
-
font-size: 2.5rem;
|
621 |
-
color: #ff6b6c;
|
622 |
-
text-align: center;
|
623 |
-
margin-bottom: 1rem;
|
624 |
-
}
|
625 |
-
.sub-header {
|
626 |
-
font-size: 1.5rem;
|
627 |
-
color: #4a4a4a;
|
628 |
-
text-align: center;
|
629 |
-
margin-bottom: 2rem;
|
630 |
-
}
|
631 |
-
.pet-card {
|
632 |
-
border-radius: 10px;
|
633 |
-
border: 1px solid #e0e0e0;
|
634 |
-
padding: 1rem;
|
635 |
-
margin-bottom: 1rem;
|
636 |
-
}
|
637 |
-
.pet-name {
|
638 |
-
font-size: 1.3rem;
|
639 |
-
font-weight: bold;
|
640 |
-
color: #ff6b6b;
|
641 |
-
}
|
642 |
-
.pet-details {
|
643 |
-
margin-top: 0.5rem;
|
644 |
-
}
|
645 |
-
.pet-description {
|
646 |
-
margin-top: 1rem;
|
647 |
-
font-style: italic;
|
648 |
-
}
|
649 |
-
.tag {
|
650 |
-
background-color: #808080;
|
651 |
-
border-radius: 20px;
|
652 |
-
padding: 0.2rem 0.6rem;
|
653 |
-
margin-right: 0.3rem;
|
654 |
-
font-size: 0.8rem;
|
655 |
-
}
|
656 |
-
</style>
|
657 |
-
""", unsafe_allow_html=True)
|
658 |
-
|
659 |
-
# Initialize session state variables
|
660 |
-
if 'access_token' not in st.session_state:
|
661 |
-
st.session_state.access_token = None
|
662 |
-
if 'token_expires' not in st.session_state:
|
663 |
-
st.session_state.token_expires = 0
|
664 |
-
if 'search_results' not in st.session_state:
|
665 |
-
st.session_state.search_results = None
|
666 |
-
if 'selected_pet' not in st.session_state:
|
667 |
-
st.session_state.selected_pet = None
|
668 |
-
if 'page' not in st.session_state:
|
669 |
-
st.session_state.page = 1
|
670 |
-
if 'favorites' not in st.session_state:
|
671 |
-
st.session_state.favorites = []
|
672 |
-
|
673 |
-
# Function to get access token
|
674 |
-
def get_access_token():
|
675 |
-
# Check if token is still valid
|
676 |
-
if st.session_state.access_token and time.time() < st.session_state.token_expires:
|
677 |
-
return st.session_state.access_token
|
678 |
-
|
679 |
-
# Get API credentials from environment variables or secrets
|
680 |
-
api_key = os.environ.get('PETFINDER_API_KEY') or st.secrets.get('PETFINDER_API_KEY')
|
681 |
-
api_secret = os.environ.get('PETFINDER_API_SECRET') or st.secrets.get('PETFINDER_API_SECRET')
|
682 |
-
|
683 |
-
if not api_key or not api_secret:
|
684 |
-
st.error("⚠️ Petfinder API credentials are missing. Please set them in your environment variables or Streamlit secrets.")
|
685 |
-
return None
|
686 |
-
|
687 |
-
# Get new token
|
688 |
-
url = "https://api.petfinder.com/v2/oauth2/token"
|
689 |
-
data = {
|
690 |
-
"grant_type": "client_credentials",
|
691 |
-
"client_id": api_key,
|
692 |
-
"client_secret": api_secret
|
693 |
-
}
|
694 |
-
|
695 |
-
try:
|
696 |
-
response = requests.post(url, data=data)
|
697 |
-
response.raise_for_status()
|
698 |
-
token_data = response.json()
|
699 |
-
st.session_state.access_token = token_data['access_token']
|
700 |
-
st.session_state.token_expires = time.time() + token_data['expires_in'] - 60 # Buffer of 60 seconds
|
701 |
-
return st.session_state.access_token
|
702 |
-
except requests.exceptions.RequestException as e:
|
703 |
-
st.error(f"⚠️ Error getting access token: {str(e)}")
|
704 |
-
return None
|
705 |
-
|
706 |
-
# Function to get RescueGroups API key
|
707 |
-
def get_rescuegroups_api_key():
|
708 |
-
api_key = os.environ.get('RESCUEGROUPS_API_KEY') or st.secrets.get('RESCUEGROUPS_API_KEY')
|
709 |
-
if not api_key:
|
710 |
-
st.error("⚠️ RescueGroups API key is missing. Please set it in environment variables or Streamlit secrets.")
|
711 |
-
return None
|
712 |
-
return api_key
|
713 |
-
|
714 |
-
# Function to search pets from RescueGroups.org
|
715 |
-
@functools.lru_cache(maxsize=32)
|
716 |
-
def search_rescuegroups_pets_cached(type_param, location, distance):
|
717 |
-
api_key = get_rescuegroups_api_key()
|
718 |
-
if not api_key:
|
719 |
-
return None
|
720 |
-
|
721 |
-
url = "https://api.rescuegroups.org/v5/public/animals/search"
|
722 |
-
|
723 |
-
headers = {
|
724 |
-
"Authorization": api_key,
|
725 |
-
"Content-Type": "application/vnd.api+json"
|
726 |
-
}
|
727 |
-
|
728 |
-
body = {
|
729 |
-
"data": {
|
730 |
-
"type": "animals",
|
731 |
-
"attributes": {
|
732 |
-
"species": type_param,
|
733 |
-
"locationPostalcode": location,
|
734 |
-
"distance": distance,
|
735 |
-
"status": "Available",
|
736 |
-
"sort": "distance",
|
737 |
-
"limit": 100
|
738 |
-
}
|
739 |
-
}
|
740 |
-
}
|
741 |
-
|
742 |
-
try:
|
743 |
-
response = requests.post(url, headers=headers, json=body)
|
744 |
-
response.raise_for_status()
|
745 |
-
return response.json()
|
746 |
-
except requests.exceptions.RequestException as e:
|
747 |
-
st.error(f"⚠️ Error searching pets from RescueGroups: {str(e)}")
|
748 |
-
return None
|
749 |
-
|
750 |
-
def search_rescuegroups_pets(params):
|
751 |
-
return search_rescuegroups_pets_cached(params.get("type", "Dog"), params.get("location", ""), params.get("distance", 50))
|
752 |
-
|
753 |
-
# Function to normalize RescueGroups results
|
754 |
-
def normalize_rescuegroups_data(rg_data):
|
755 |
-
pets = []
|
756 |
-
if not rg_data or 'data' not in rg_data:
|
757 |
-
return pets
|
758 |
-
|
759 |
-
for item in rg_data['data']:
|
760 |
-
pet = {
|
761 |
-
'id': f"RG-{item['id']}",
|
762 |
-
'name': item['attributes'].get('name', 'Unknown'),
|
763 |
-
'status': 'adoptable',
|
764 |
-
'age': item['attributes'].get('ageGroup', ''),
|
765 |
-
'gender': item['attributes'].get('sex', ''),
|
766 |
-
'size': item['attributes'].get('sizeGroup', ''),
|
767 |
-
'breeds': {
|
768 |
-
'primary': item['attributes'].get('breedPrimary', ''),
|
769 |
-
'secondary': item['attributes'].get('breedSecondary', ''),
|
770 |
-
'mixed': False
|
771 |
-
},
|
772 |
-
'colors': {
|
773 |
-
'primary': item['attributes'].get('colorPrimary', ''),
|
774 |
-
'secondary': item['attributes'].get('colorSecondary', ''),
|
775 |
-
'tertiary': ''
|
776 |
-
},
|
777 |
-
'photos': [],
|
778 |
-
'description': item['attributes'].get('description', ''),
|
779 |
-
'contact': {
|
780 |
-
'email': '',
|
781 |
-
'phone': '',
|
782 |
-
'address': {
|
783 |
-
'city': '',
|
784 |
-
'state': '',
|
785 |
-
'postcode': ''
|
786 |
-
}
|
787 |
-
},
|
788 |
-
'source': 'RescueGroups'
|
789 |
-
}
|
790 |
-
if 'photos' in item['attributes'] and item['attributes']['photos']:
|
791 |
-
pet['photos'] = [{'medium': item['attributes']['photos'][0]}]
|
792 |
-
pets.append(pet)
|
793 |
-
return pets
|
794 |
-
|
795 |
-
# Example modification in your search form to call BOTH APIs
|
796 |
-
with st.form("pet_search_form"):
|
797 |
-
# ... your existing form fields ...
|
798 |
-
submitted = st.form_submit_button("Search")
|
799 |
-
|
800 |
-
if submitted:
|
801 |
-
params = {
|
802 |
-
"type": animal_type.split(" ")[0],
|
803 |
-
"location": location,
|
804 |
-
"distance": distance
|
805 |
-
}
|
806 |
-
|
807 |
-
petfinder_results = search_pets(params)
|
808 |
-
rescuegroups_results = search_rescuegroups_pets(params)
|
809 |
-
|
810 |
-
combined_pets = []
|
811 |
-
|
812 |
-
if petfinder_results and 'animals' in petfinder_results:
|
813 |
-
for pet in petfinder_results['animals']:
|
814 |
-
pet['source'] = 'Petfinder'
|
815 |
-
combined_pets.append(pet)
|
816 |
-
if rescuegroups_results:
|
817 |
-
normalized = normalize_rescuegroups_data(rescuegroups_results)
|
818 |
-
combined_pets.extend(normalized)
|
819 |
-
|
820 |
-
if combined_pets:
|
821 |
-
st.session_state.search_results = {'animals': combined_pets}
|
822 |
-
st.session_state.page = 1
|
823 |
-
st.success(f"Found {len(combined_pets)} pets from both sources!")
|
824 |
-
else:
|
825 |
-
st.error("No pets found. Try adjusting your filters.")
|
826 |
-
|
827 |
-
|
828 |
-
|
829 |
-
# Function to search pets
|
830 |
-
def search_pets(params):
|
831 |
-
token = get_access_token()
|
832 |
-
if not token:
|
833 |
-
return None
|
834 |
-
|
835 |
-
url = "https://api.petfinder.com/v2/animals"
|
836 |
-
headers = {"Authorization": f"Bearer {token}"}
|
837 |
-
|
838 |
-
try:
|
839 |
-
response = requests.get(url, headers=headers, params=params)
|
840 |
-
response.raise_for_status()
|
841 |
-
return response.json()
|
842 |
-
except requests.exceptions.RequestException as e:
|
843 |
-
st.error(f"⚠️ Error searching pets: {str(e)}")
|
844 |
-
return None
|
845 |
-
|
846 |
-
# Function to get breeds
|
847 |
-
def get_breeds(animal_type):
|
848 |
-
token = get_access_token()
|
849 |
-
if not token:
|
850 |
-
return []
|
851 |
-
|
852 |
-
url = f"https://api.petfinder.com/v2/types/{animal_type}/breeds"
|
853 |
-
headers = {"Authorization": f"Bearer {token}"}
|
854 |
-
|
855 |
-
try:
|
856 |
-
response = requests.get(url, headers=headers)
|
857 |
-
response.raise_for_status()
|
858 |
-
return [breed['name'] for breed in response.json()['breeds']]
|
859 |
-
except requests.exceptions.RequestException as e:
|
860 |
-
st.error(f"⚠️ Error getting breeds: {str(e)}")
|
861 |
-
return []
|
862 |
-
|
863 |
-
# Function to get organizations
|
864 |
-
def get_organizations(location):
|
865 |
-
token = get_access_token()
|
866 |
-
if not token:
|
867 |
-
return []
|
868 |
-
|
869 |
-
url = "https://api.petfinder.com/v2/organizations"
|
870 |
-
headers = {"Authorization": f"Bearer {token}"}
|
871 |
-
params = {"location": location, "distance": 100, "limit": 100}
|
872 |
-
|
873 |
-
try:
|
874 |
-
response = requests.get(url, headers=headers, params=params)
|
875 |
-
response.raise_for_status()
|
876 |
-
return [(org['id'], org['name']) for org in response.json()['organizations']]
|
877 |
-
except requests.exceptions.RequestException as e:
|
878 |
-
st.error(f"⚠️ Error getting organizations: {str(e)}")
|
879 |
-
return []
|
880 |
-
|
881 |
-
# Function to get pet details
|
882 |
-
def get_pet_details(pet_id):
|
883 |
-
token = get_access_token()
|
884 |
-
if not token:
|
885 |
-
return None
|
886 |
-
|
887 |
-
url = f"https://api.petfinder.com/v2/animals/{pet_id}"
|
888 |
-
headers = {"Authorization": f"Bearer {token}"}
|
889 |
-
|
890 |
-
try:
|
891 |
-
response = requests.get(url, headers=headers)
|
892 |
-
response.raise_for_status()
|
893 |
-
return response.json()['animal']
|
894 |
-
except requests.exceptions.RequestException as e:
|
895 |
-
st.error(f"⚠️ Error getting pet details: {str(e)}")
|
896 |
-
return None
|
897 |
-
|
898 |
-
# Function to format pet card
|
899 |
-
def display_pet_card(pet, is_favorite=False, context="search"):
|
900 |
-
col1, col2 = st.columns([1, 2])
|
901 |
-
|
902 |
-
with col1:
|
903 |
-
if pet['photos'] and len(pet['photos']) > 0:
|
904 |
-
st.image(pet['photos'][0]['medium'], use_container_width=True)
|
905 |
-
else:
|
906 |
-
st.image("https://via.placeholder.com/300x300?text=No+Image", use_container_width=True)
|
907 |
-
|
908 |
-
with col2:
|
909 |
-
st.markdown(f"<div class='pet-name'>{pet['name']}</div>", unsafe_allow_html=True)
|
910 |
-
|
911 |
-
# Tags
|
912 |
-
tags_html = ""
|
913 |
-
if pet['status'] == 'adoptable':
|
914 |
-
tags_html += "<span class='tag' style='background-color: #808080;'>Adoptable</span> "
|
915 |
-
else:
|
916 |
-
tags_html += f"<span class='tag' style='background-color: #808080;'>{pet['status'].title()}</span> "
|
917 |
-
|
918 |
-
if pet['age']:
|
919 |
-
tags_html += f"<span class='tag'>{pet['age']}</span> "
|
920 |
-
if pet['gender']:
|
921 |
-
tags_html += f"<span class='tag'>{pet['gender']}</span> "
|
922 |
-
if pet['size']:
|
923 |
-
tags_html += f"<span class='tag'>{pet['size']}</span> "
|
924 |
-
|
925 |
-
st.markdown(f"<div>{tags_html}</div>", unsafe_allow_html=True)
|
926 |
-
|
927 |
-
st.markdown("<div class='pet-details'>", unsafe_allow_html=True)
|
928 |
-
if pet['breeds']['primary']:
|
929 |
-
breed_text = pet['breeds']['primary']
|
930 |
-
if pet['breeds']['secondary']:
|
931 |
-
breed_text += f" & {pet['breeds']['secondary']}"
|
932 |
-
if pet['breeds']['mixed']:
|
933 |
-
breed_text += " (Mixed)"
|
934 |
-
st.markdown(f"<strong>Breed:</strong> {breed_text}", unsafe_allow_html=True)
|
935 |
-
|
936 |
-
if pet['colors']['primary'] or pet['colors']['secondary'] or pet['colors']['tertiary']:
|
937 |
-
colors = [c for c in [pet['colors']['primary'], pet['colors']['secondary'], pet['colors']['tertiary']] if c]
|
938 |
-
st.markdown(f"<strong>Colors:</strong> {', '.join(colors)}", unsafe_allow_html=True)
|
939 |
-
|
940 |
-
if 'location' in pet and pet['contact']['address']['city'] and pet['contact']['address']['state']:
|
941 |
-
st.markdown(f"<strong>Location:</strong> {pet['contact']['address']['city']}, {pet['contact']['address']['state']}", unsafe_allow_html=True)
|
942 |
-
|
943 |
-
st.markdown("</div>", unsafe_allow_html=True)
|
944 |
-
|
945 |
-
if pet['description']:
|
946 |
-
st.markdown(f"<div class='pet-description'>{pet['description'][:500]}{'...' if len(pet['description']) > 500 else ''}</div>", unsafe_allow_html=True)
|
947 |
-
|
948 |
-
col1, col2 = st.columns(2)
|
949 |
-
with col1:
|
950 |
-
if st.button("View Details", key=f"details_{context}_{pet['id']}"):
|
951 |
-
st.session_state.selected_pet = pet['id']
|
952 |
-
st.rerun()
|
953 |
-
with col2:
|
954 |
-
if not is_favorite:
|
955 |
-
if st.button("Add to Favorites", key=f"fav_{context}_{pet['id']}"):
|
956 |
-
if pet['id'] not in [p['id'] for p in st.session_state.favorites]:
|
957 |
-
st.session_state.favorites.append(pet)
|
958 |
-
st.success(f"Added {pet['name']} to favorites!")
|
959 |
-
st.rerun()
|
960 |
-
else:
|
961 |
-
if st.button("Remove from Favorites", key=f"unfav_{context}_{pet['id']}"):
|
962 |
-
st.session_state.favorites = [p for p in st.session_state.favorites if p['id'] != pet['id']]
|
963 |
-
st.success(f"Removed {pet['name']} from favorites!")
|
964 |
-
st.rerun()
|
965 |
-
|
966 |
-
# Function to generate pet compatibility message
|
967 |
-
def get_compatibility_message(pet):
|
968 |
-
messages = []
|
969 |
-
|
970 |
-
# Check for kids
|
971 |
-
if 'children' in pet['environment'] and pet['environment']['children'] is not None:
|
972 |
-
if pet['environment']['children']:
|
973 |
-
messages.append("✅ Good with children")
|
974 |
-
else:
|
975 |
-
messages.append("❌ Not recommended for homes with children")
|
976 |
-
|
977 |
-
# Check for dogs
|
978 |
-
if 'dogs' in pet['environment'] and pet['environment']['dogs'] is not None:
|
979 |
-
if pet['environment']['dogs']:
|
980 |
-
messages.append("✅ Good with dogs")
|
981 |
-
else:
|
982 |
-
messages.append("❌ Not recommended for homes with dogs")
|
983 |
-
|
984 |
-
# Check for cats
|
985 |
-
if 'cats' in pet['environment'] and pet['environment']['cats'] is not None:
|
986 |
-
if pet['environment']['cats']:
|
987 |
-
messages.append("✅ Good with cats")
|
988 |
-
else:
|
989 |
-
messages.append("❌ Not recommended for homes with cats")
|
990 |
-
|
991 |
-
# Handling care needs
|
992 |
-
if pet['attributes']:
|
993 |
-
if 'special_needs' in pet['attributes'] and pet['attributes']['special_needs']:
|
994 |
-
messages.append("⚠️ Has special needs")
|
995 |
-
|
996 |
-
if 'house_trained' in pet['attributes'] and pet['attributes']['house_trained']:
|
997 |
-
messages.append("✅ House-trained")
|
998 |
-
elif 'house_trained' in pet['attributes']:
|
999 |
-
messages.append("❌ Not house-trained")
|
1000 |
-
|
1001 |
-
if 'shots_current' in pet['attributes'] and pet['attributes']['shots_current']:
|
1002 |
-
messages.append("✅ Vaccinations up to date")
|
1003 |
-
|
1004 |
-
if 'spayed_neutered' in pet['attributes'] and pet['attributes']['spayed_neutered']:
|
1005 |
-
messages.append("✅ Spayed/neutered")
|
1006 |
-
|
1007 |
-
return messages
|
1008 |
-
|
1009 |
-
# Function to display pet details page
|
1010 |
-
# Changes to make keys unique across different tabs
|
1011 |
-
|
1012 |
-
# Function to display pet details page with unique tab identifier
|
1013 |
-
def display_pet_details(pet_id, context="search", tab_id="tab1"):
|
1014 |
-
pet = get_pet_details(pet_id)
|
1015 |
-
if not pet:
|
1016 |
-
st.error("Unable to retrieve pet details. Please try again.")
|
1017 |
-
return
|
1018 |
-
|
1019 |
-
# Back button with unique key that includes tab identifier
|
1020 |
-
if st.button("← Back to Search Results", key=f"back_{tab_id}_{context}_{pet_id}"):
|
1021 |
-
st.session_state.selected_pet = None
|
1022 |
-
st.rerun() # Force immediate rerun
|
1023 |
-
|
1024 |
-
# Pet name and status
|
1025 |
-
st.markdown(f"<h1 class='main-header'>{pet['name']}</h1>", unsafe_allow_html=True)
|
1026 |
-
|
1027 |
-
status_color = "#c8e6c9" if pet['status'] == 'adoptable' else "#ffcdd2"
|
1028 |
-
st.markdown(f"<div style='text-align: center;'><span class='tag' style='background-color: {status_color}; font-size: 1rem;'>{pet['status'].title()}</span></div>", unsafe_allow_html=True)
|
1029 |
-
|
1030 |
-
# Pet photos
|
1031 |
-
if pet['photos'] and len(pet['photos']) > 0:
|
1032 |
-
photo_cols = st.columns(min(3, len(pet['photos'])))
|
1033 |
-
for i, col in enumerate(photo_cols):
|
1034 |
-
if i < len(pet['photos']):
|
1035 |
-
col.image(pet['photos'][i]['large'], use_container_width=True)
|
1036 |
-
else:
|
1037 |
-
st.image("https://via.placeholder.com/500x300?text=No+Image", use_container_width=True)
|
1038 |
-
|
1039 |
-
# Pet details
|
1040 |
-
col1, col2 = st.columns(2)
|
1041 |
-
|
1042 |
-
with col1:
|
1043 |
-
st.markdown("### Details")
|
1044 |
-
# Fix the breed line
|
1045 |
-
breed_text = pet['breeds']['primary']
|
1046 |
-
if pet['breeds']['secondary']:
|
1047 |
-
breed_text += f" & {pet['breeds']['secondary']}"
|
1048 |
-
if pet['breeds']['mixed']:
|
1049 |
-
breed_text += " (Mixed)"
|
1050 |
-
|
1051 |
-
details = [
|
1052 |
-
f"**Type:** {pet['type']}",
|
1053 |
-
f"**Breed:** {breed_text}",
|
1054 |
-
f"**Age:** {pet['age']}",
|
1055 |
-
f"**Gender:** {pet['gender']}",
|
1056 |
-
f"**Size:** {pet['size']}"
|
1057 |
-
]
|
1058 |
-
|
1059 |
-
# Fix the colors line as well, to be safe
|
1060 |
-
colors = [c for c in [pet['colors']['primary'], pet['colors']['secondary'], pet['colors']['tertiary']] if c]
|
1061 |
-
if colors:
|
1062 |
-
details.append(f"**Colors:** {', '.join(colors)}")
|
1063 |
-
|
1064 |
-
for detail in details:
|
1065 |
-
st.markdown(detail)
|
1066 |
-
|
1067 |
-
with col2:
|
1068 |
-
st.markdown("### Compatibility")
|
1069 |
-
compatibility = get_compatibility_message(pet)
|
1070 |
-
for msg in compatibility:
|
1071 |
-
st.markdown(msg)
|
1072 |
-
|
1073 |
-
# Description
|
1074 |
-
if pet['description']:
|
1075 |
-
if pet['description']:
|
1076 |
-
st.markdown("### About")
|
1077 |
-
#st.markdown(pet['description'])
|
1078 |
-
st.markdown(f"<div class='pet-description'>{pet['description'][:500]}{'...' if len(pet['description']) > 500 else ''}</div>", unsafe_allow_html=True)
|
1079 |
-
|
1080 |
-
# Contact information
|
1081 |
-
st.markdown("### Adoption Information")
|
1082 |
-
|
1083 |
-
# Organization info
|
1084 |
-
if pet['organization_id']:
|
1085 |
-
st.markdown(f"**Organization:** {pet['organization_id']}")
|
1086 |
-
|
1087 |
-
# Contact details
|
1088 |
-
contact_info = []
|
1089 |
-
if pet['contact']['email']:
|
1090 |
-
contact_info.append(f"**Email:** {pet['contact']['email']}")
|
1091 |
-
if pet['contact']['phone']:
|
1092 |
-
contact_info.append(f"**Phone:** {pet['contact']['phone']}")
|
1093 |
-
if pet['contact']['address']['city'] and pet['contact']['address']['state']:
|
1094 |
-
contact_info.append(f"**Location:** {pet['contact']['address']['city']}, {pet['contact']['address']['state']} {pet['contact']['address']['postcode'] or ''}")
|
1095 |
-
|
1096 |
-
for info in contact_info:
|
1097 |
-
st.markdown(info)
|
1098 |
-
|
1099 |
-
# URL to pet on Petfinder
|
1100 |
-
if pet['url']:
|
1101 |
-
st.markdown(f"[View on Petfinder]({pet['url']})")
|
1102 |
-
|
1103 |
-
# Add to favorites with unique key
|
1104 |
-
is_favorite = pet['id'] in [p['id'] for p in st.session_state.favorites]
|
1105 |
-
if not is_favorite:
|
1106 |
-
if st.button("Add to Favorites", key=f"add_fav_{tab_id}_{context}_{pet_id}"):
|
1107 |
-
st.session_state.favorites.append(pet)
|
1108 |
-
st.success(f"Added {pet['name']} to favorites!")
|
1109 |
-
st.rerun()
|
1110 |
-
else:
|
1111 |
-
if st.button("Remove from Favorites", key=f"rem_fav_{tab_id}_{context}_{pet_id}"):
|
1112 |
-
st.session_state.favorites = [p for p in st.session_state.favorites if p['id'] != pet['id']]
|
1113 |
-
st.success(f"Removed {pet['name']} from favorites!")
|
1114 |
-
st.rerun()
|
1115 |
-
|
1116 |
-
# Function to format pet card with unique tab identifier
|
1117 |
-
def display_pet_card(pet, is_favorite=False, context="search", tab_id="tab1"):
|
1118 |
-
col1, col2 = st.columns([1, 2])
|
1119 |
-
|
1120 |
-
with col1:
|
1121 |
-
if pet['photos'] and len(pet['photos']) > 0:
|
1122 |
-
st.image(pet['photos'][0]['medium'], use_container_width=True)
|
1123 |
-
else:
|
1124 |
-
st.image("https://via.placeholder.com/300x300?text=No+Image", use_container_width=True)
|
1125 |
-
|
1126 |
-
with col2:
|
1127 |
-
st.markdown(f"<div class='pet-name'>{pet['name']}</div>", unsafe_allow_html=True)
|
1128 |
-
|
1129 |
-
# Tags
|
1130 |
-
tags_html = ""
|
1131 |
-
if pet['status'] == 'adoptable':
|
1132 |
-
tags_html += "<span class='tag' style='background-color: #808080;'>Adoptable</span> "
|
1133 |
-
else:
|
1134 |
-
tags_html += f"<span class='tag' style='background-color: #808080;'>{pet['status'].title()}</span> "
|
1135 |
-
|
1136 |
-
if pet['age']:
|
1137 |
-
tags_html += f"<span class='tag'>{pet['age']}</span> "
|
1138 |
-
if pet['gender']:
|
1139 |
-
tags_html += f"<span class='tag'>{pet['gender']}</span> "
|
1140 |
-
if pet['size']:
|
1141 |
-
tags_html += f"<span class='tag'>{pet['size']}</span> "
|
1142 |
-
|
1143 |
-
st.markdown(f"<div>{tags_html}</div>", unsafe_allow_html=True)
|
1144 |
-
|
1145 |
-
st.markdown("<div class='pet-details'>", unsafe_allow_html=True)
|
1146 |
-
if pet['breeds']['primary']:
|
1147 |
-
breed_text = pet['breeds']['primary']
|
1148 |
-
if pet['breeds']['secondary']:
|
1149 |
-
breed_text += f" & {pet['breeds']['secondary']}"
|
1150 |
-
if pet['breeds']['mixed']:
|
1151 |
-
breed_text += " (Mixed)"
|
1152 |
-
st.markdown(f"<strong>Breed:</strong> {breed_text}", unsafe_allow_html=True)
|
1153 |
-
|
1154 |
-
if pet['colors']['primary'] or pet['colors']['secondary'] or pet['colors']['tertiary']:
|
1155 |
-
colors = [c for c in [pet['colors']['primary'], pet['colors']['secondary'], pet['colors']['tertiary']] if c]
|
1156 |
-
st.markdown(f"<strong>Colors:</strong> {', '.join(colors)}", unsafe_allow_html=True)
|
1157 |
-
|
1158 |
-
if 'location' in pet and pet['contact']['address']['city'] and pet['contact']['address']['state']:
|
1159 |
-
st.markdown(f"<strong>Location:</strong> {pet['contact']['address']['city']}, {pet['contact']['address']['state']}", unsafe_allow_html=True)
|
1160 |
-
|
1161 |
-
st.markdown("</div>", unsafe_allow_html=True)
|
1162 |
-
|
1163 |
-
if pet['description']:
|
1164 |
-
st.markdown(f"<div class='pet-description'>{pet['description'][:300]}{'...' if len(pet['description']) > 300 else ''}</div>", unsafe_allow_html=True)
|
1165 |
-
|
1166 |
-
col1, col2 = st.columns(2)
|
1167 |
-
with col1:
|
1168 |
-
if st.button("View Details", key=f"details_{tab_id}_{context}_{pet['id']}"):
|
1169 |
-
st.session_state.selected_pet = pet['id']
|
1170 |
-
st.rerun()
|
1171 |
-
with col2:
|
1172 |
-
if not is_favorite:
|
1173 |
-
if st.button("Add to Favorites", key=f"fav_{tab_id}_{context}_{pet['id']}"):
|
1174 |
-
if pet['id'] not in [p['id'] for p in st.session_state.favorites]:
|
1175 |
-
st.session_state.favorites.append(pet)
|
1176 |
-
st.success(f"Added {pet['name']} to favorites!")
|
1177 |
-
st.rerun()
|
1178 |
-
else:
|
1179 |
-
if st.button("Remove from Favorites", key=f"unfav_{tab_id}_{context}_{pet['id']}"):
|
1180 |
-
st.session_state.favorites = [p for p in st.session_state.favorites if p['id'] != pet['id']]
|
1181 |
-
st.success(f"Removed {pet['name']} from favorites!")
|
1182 |
-
st.rerun()
|
1183 |
-
|
1184 |
-
# Main app with updated function calls
|
1185 |
-
def main():
|
1186 |
-
# Title
|
1187 |
-
st.markdown("<h1 class='main-header'>🐾 PetMatch</h1>", unsafe_allow_html=True)
|
1188 |
-
st.markdown("<p class='sub-header'>Find your perfect pet companion</p>", unsafe_allow_html=True)
|
1189 |
-
|
1190 |
-
# Create tabs
|
1191 |
-
tab1, tab2, tab3 = st.tabs(["Search", "Favorites", "About"])
|
1192 |
-
|
1193 |
-
with tab1:
|
1194 |
-
# If a pet is selected, show details
|
1195 |
-
if st.session_state.selected_pet:
|
1196 |
-
display_pet_details(st.session_state.selected_pet, context="search", tab_id="tab1")
|
1197 |
-
else:
|
1198 |
-
# Search form
|
1199 |
-
with st.expander("Search Options", expanded=True):
|
1200 |
-
with st.form("pet_search_form"):
|
1201 |
-
col1, col2 = st.columns(2)
|
1202 |
-
|
1203 |
-
with col1:
|
1204 |
-
animal_type = st.selectbox(
|
1205 |
-
"Animal Type",
|
1206 |
-
["Dog", "Cat", "Rabbit", "Small & Furry", "Horse", "Bird", "Scales, Fins & Other", "Barnyard"]
|
1207 |
-
)
|
1208 |
-
|
1209 |
-
location = st.text_input("Location (ZIP code or City, State)", "")
|
1210 |
-
|
1211 |
-
distance = st.slider("Distance (miles)", min_value=10, max_value=500, value=50, step=10)
|
1212 |
-
|
1213 |
-
with col2:
|
1214 |
-
age_options = ["", "Baby", "Young", "Adult", "Senior"]
|
1215 |
-
age = st.selectbox("Age", age_options)
|
1216 |
-
|
1217 |
-
size_options = ["", "Small", "Medium", "Large", "XLarge"]
|
1218 |
-
size = st.selectbox("Size", size_options)
|
1219 |
-
|
1220 |
-
gender_options = ["", "Male", "Female"]
|
1221 |
-
gender = st.selectbox("Gender", gender_options)
|
1222 |
-
good_with_children = st.checkbox("Good with children")
|
1223 |
-
good_with_dogs = st.checkbox("Good with dogs")
|
1224 |
-
good_with_cats = st.checkbox("Good with cats")
|
1225 |
-
house_trained = st.checkbox("House-trained")
|
1226 |
-
special_needs = st.checkbox("Special needs")
|
1227 |
-
|
1228 |
-
submitted = st.form_submit_button("Search")
|
1229 |
-
|
1230 |
-
if submitted:
|
1231 |
-
# Build search parameters
|
1232 |
-
params = {
|
1233 |
-
"type": animal_type.split(" ")[0], # Take first word for types like "Small & Furry"
|
1234 |
-
"location": location,
|
1235 |
-
"distance": distance,
|
1236 |
-
"status": "adoptable",
|
1237 |
-
"sort": "distance",
|
1238 |
-
"limit": 100
|
1239 |
-
}
|
1240 |
-
|
1241 |
-
if age and age != "":
|
1242 |
-
params["age"] = age
|
1243 |
-
if size and size != "":
|
1244 |
-
params["size"] = size
|
1245 |
-
if gender and gender != "":
|
1246 |
-
params["gender"] = gender
|
1247 |
-
|
1248 |
-
# Add advanced filters
|
1249 |
-
if good_with_children:
|
1250 |
-
params["good_with_children"] = 1
|
1251 |
-
if good_with_dogs:
|
1252 |
-
params["good_with_dogs"] = 1
|
1253 |
-
if good_with_cats:
|
1254 |
-
params["good_with_cats"] = 1
|
1255 |
-
if house_trained:
|
1256 |
-
params["house_trained"] = 1
|
1257 |
-
if special_needs:
|
1258 |
-
params["special_needs"] = 1
|
1259 |
-
|
1260 |
-
# Perform search
|
1261 |
-
results = search_pets(params)
|
1262 |
-
if results and 'animals' in results:
|
1263 |
-
st.session_state.search_results = results
|
1264 |
-
st.session_state.page = 1
|
1265 |
-
st.success(f"Found {len(results['animals'])} pets!")
|
1266 |
-
else:
|
1267 |
-
st.error("No pets found with those criteria. Try expanding your search.")
|
1268 |
-
|
1269 |
-
# Display search results
|
1270 |
-
if st.session_state.search_results and 'animals' in st.session_state.search_results:
|
1271 |
-
st.markdown("### Search Results")
|
1272 |
-
|
1273 |
-
# Pagination
|
1274 |
-
results = st.session_state.search_results['animals']
|
1275 |
-
total_pages = (len(results) + 9) // 10 # 10 items per page
|
1276 |
-
|
1277 |
-
# Display page selector
|
1278 |
-
if total_pages > 1:
|
1279 |
-
col1, col2, col3 = st.columns([1, 3, 1])
|
1280 |
-
with col2:
|
1281 |
-
page = st.slider("Page", 1, total_pages, st.session_state.page)
|
1282 |
-
if page != st.session_state.page:
|
1283 |
-
st.session_state.page = page
|
1284 |
-
|
1285 |
-
# Display pets for current page
|
1286 |
-
start_idx = (st.session_state.page - 1) * 10
|
1287 |
-
end_idx = min(start_idx + 10, len(results))
|
1288 |
-
|
1289 |
-
for pet in results[start_idx:end_idx]:
|
1290 |
-
st.markdown("---")
|
1291 |
-
display_pet_card(pet, tab_id="tab1")
|
1292 |
-
|
1293 |
-
with tab2:
|
1294 |
-
st.markdown("### Your Favorite Pets")
|
1295 |
-
|
1296 |
-
if not st.session_state.favorites:
|
1297 |
-
st.info("You haven't added any pets to your favorites yet. Start searching to find your perfect match!")
|
1298 |
-
else:
|
1299 |
-
# Check if a pet is selected from favorites
|
1300 |
-
if st.session_state.selected_pet:
|
1301 |
-
display_pet_details(st.session_state.selected_pet, context="favorites", tab_id="tab2")
|
1302 |
-
else:
|
1303 |
-
for pet in st.session_state.favorites:
|
1304 |
-
st.markdown("---")
|
1305 |
-
display_pet_card(pet, is_favorite=True, context="favorites", tab_id="tab2")
|
1306 |
-
|
1307 |
-
with tab3:
|
1308 |
-
st.markdown("### About PetMatch")
|
1309 |
-
st.markdown("""
|
1310 |
-
PetMatch helps you find your perfect pet companion from thousands of adoptable animals across the country.
|
1311 |
-
|
1312 |
-
**How to use PetMatch:**
|
1313 |
-
1. Search for pets based on your preferences and location
|
1314 |
-
2. Browse through the results and click "View Details" to learn more about each pet
|
1315 |
-
3. Add pets to your favorites to keep track of the ones you're interested in
|
1316 |
-
4. Contact the shelter or rescue organization directly using the provided information
|
1317 |
-
|
1318 |
-
**Data Source:**
|
1319 |
-
PetMatch uses the Petfinder API to provide up-to-date information on adoptable pets. Petfinder is North America's largest adoption website with hundreds of thousands of adoptable pets listed by more than 11,500 animal shelters and rescue organizations.
|
1320 |
-
|
1321 |
-
**Privacy:**
|
1322 |
-
PetMatch does not store any personal information or search history. Your favorites are stored locally in your browser and are not shared with any third parties.
|
1323 |
-
""")
|
1324 |
-
|
1325 |
-
|
1326 |
-
if __name__ == "__main__":
|
1327 |
-
main()
|
|
|
603 |
|
604 |
if __name__ == "__main__":
|
605 |
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|