diff --git a/index.html b/index.html index 38a5fa7..975cff0 100644 --- a/index.html +++ b/index.html @@ -1,26 +1,28 @@ - + - - Lovable App - - + Pawfect Match - Find Love for Your Pet + + - - - + + - + + + + +
- + \ No newline at end of file diff --git a/src/assets/pets/cat1.jpg b/src/assets/pets/cat1.jpg new file mode 100644 index 0000000..e97b7a4 Binary files /dev/null and b/src/assets/pets/cat1.jpg differ diff --git a/src/assets/pets/cat2.jpg b/src/assets/pets/cat2.jpg new file mode 100644 index 0000000..c077bd8 Binary files /dev/null and b/src/assets/pets/cat2.jpg differ diff --git a/src/assets/pets/dog1.jpg b/src/assets/pets/dog1.jpg new file mode 100644 index 0000000..8ac7a15 Binary files /dev/null and b/src/assets/pets/dog1.jpg differ diff --git a/src/assets/pets/dog2.jpg b/src/assets/pets/dog2.jpg new file mode 100644 index 0000000..e554600 Binary files /dev/null and b/src/assets/pets/dog2.jpg differ diff --git a/src/assets/pets/dog3.jpg b/src/assets/pets/dog3.jpg new file mode 100644 index 0000000..2b72f83 Binary files /dev/null and b/src/assets/pets/dog3.jpg differ diff --git a/src/components/EmptyState.tsx b/src/components/EmptyState.tsx new file mode 100644 index 0000000..76cdfbf --- /dev/null +++ b/src/components/EmptyState.tsx @@ -0,0 +1,37 @@ +import { Heart, RefreshCw } from 'lucide-react'; + +interface EmptyStateProps { + onReset: () => void; + likedPets: number; +} + +export function EmptyState({ onReset, likedPets }: EmptyStateProps) { + return ( +
+
+ +
+ +

+ No More Pets Nearby! +

+ +

+ You've seen all the adorable pets in your area. + {likedPets > 0 && ( + + You matched with {likedPets} pet{likedPets > 1 ? 's' : ''}! 🎉 + + )} +

+ + +
+ ); +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 0000000..48f7044 --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,22 @@ +import { Heart, User, Settings } from 'lucide-react'; + +export function Header() { + return ( +
+ + +
+ +

+ Pawfect +

+
+ + +
+ ); +} diff --git a/src/components/MatchModal.tsx b/src/components/MatchModal.tsx new file mode 100644 index 0000000..d037b73 --- /dev/null +++ b/src/components/MatchModal.tsx @@ -0,0 +1,65 @@ +import { Heart, MessageCircle, X } from 'lucide-react'; +import type { Pet } from '@/data/pets'; + +interface MatchModalProps { + pet: Pet; + onClose: () => void; +} + +export function MatchModal({ pet, onClose }: MatchModalProps) { + return ( +
+
+
+ {/* Close button */} + + + {/* Hearts animation */} +
+
+ + + +
+
+ +

It's a Match!

+

+ You and {pet.name} liked each other! +

+ + {/* Pet image */} +
+ {pet.name} +
+ + {/* Actions */} +
+ + +
+
+
+
+ ); +} diff --git a/src/components/SwipeCard.tsx b/src/components/SwipeCard.tsx new file mode 100644 index 0000000..800c267 --- /dev/null +++ b/src/components/SwipeCard.tsx @@ -0,0 +1,159 @@ +import { useState } from 'react'; +import { Heart, X, MapPin, Info } from 'lucide-react'; +import type { Pet } from '@/data/pets'; + +interface SwipeCardProps { + pet: Pet; + onSwipe: (direction: 'left' | 'right') => void; + isTop: boolean; +} + +export function SwipeCard({ pet, onSwipe, isTop }: SwipeCardProps) { + const [swipeDirection, setSwipeDirection] = useState<'left' | 'right' | null>(null); + const [showInfo, setShowInfo] = useState(false); + const [dragStart, setDragStart] = useState<{ x: number; y: number } | null>(null); + const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); + + const handleSwipe = (direction: 'left' | 'right') => { + setSwipeDirection(direction); + setTimeout(() => onSwipe(direction), 400); + }; + + const handleDragStart = (clientX: number, clientY: number) => { + if (!isTop) return; + setDragStart({ x: clientX, y: clientY }); + }; + + const handleDragMove = (clientX: number) => { + if (!dragStart || !isTop) return; + const offset = clientX - dragStart.x; + setDragOffset({ x: offset, y: 0 }); + }; + + const handleDragEnd = () => { + if (!dragStart || !isTop) return; + + if (dragOffset.x > 100) { + handleSwipe('right'); + } else if (dragOffset.x < -100) { + handleSwipe('left'); + } + + setDragStart(null); + setDragOffset({ x: 0, y: 0 }); + }; + + const rotation = dragOffset.x * 0.05; + const opacity = Math.max(0.5, 1 - Math.abs(dragOffset.x) / 500); + + return ( +
handleDragStart(e.clientX, e.clientY)} + onMouseMove={(e) => handleDragMove(e.clientX)} + onMouseUp={handleDragEnd} + onMouseLeave={handleDragEnd} + onTouchStart={(e) => handleDragStart(e.touches[0].clientX, e.touches[0].clientY)} + onTouchMove={(e) => handleDragMove(e.touches[0].clientX)} + onTouchEnd={handleDragEnd} + > +
+ {/* Image */} + {pet.name} + + {/* Gradient overlay */} +
+ + {/* Like/Nope indicators */} + {dragOffset.x > 50 && ( +
+ LIKE +
+ )} + {dragOffset.x < -50 && ( +
+ NOPE +
+ )} + + {/* Pet info */} +
+
+
+

+ {pet.name}, {pet.age} +

+

{pet.breed}

+
+ + {pet.distance} +
+
+ +
+ + {/* Expanded info */} + {showInfo && ( +
+

{pet.bio}

+
+ {pet.interests.map((interest) => ( + + {interest} + + ))} +
+
+ )} +
+
+ + {/* Action buttons */} + {isTop && ( +
+ + +
+ )} +
+ ); +} diff --git a/src/data/pets.ts b/src/data/pets.ts new file mode 100644 index 0000000..60bf26f --- /dev/null +++ b/src/data/pets.ts @@ -0,0 +1,75 @@ +import dog1 from '@/assets/pets/dog1.jpg'; +import cat1 from '@/assets/pets/cat1.jpg'; +import dog2 from '@/assets/pets/dog2.jpg'; +import cat2 from '@/assets/pets/cat2.jpg'; +import dog3 from '@/assets/pets/dog3.jpg'; + +export interface Pet { + id: string; + name: string; + age: number; + breed: string; + type: 'dog' | 'cat'; + bio: string; + image: string; + distance: string; + interests: string[]; +} + +export const pets: Pet[] = [ + { + id: '1', + name: 'Buddy', + age: 2, + breed: 'Golden Retriever', + type: 'dog', + bio: 'Love long walks, belly rubs, and making new furry friends! 🐾 Looking for my pawfect match to explore the park together.', + image: dog1, + distance: '2 miles away', + interests: ['Fetch', 'Swimming', 'Cuddles', 'Treats'], + }, + { + id: '2', + name: 'Whiskers', + age: 3, + breed: 'Orange Tabby', + type: 'cat', + bio: 'Professional napper and window watcher. Seeking a sophisticated feline companion who appreciates the finer things in life. 😺', + image: cat1, + distance: '1 mile away', + interests: ['Napping', 'Bird Watching', 'Cardboard Boxes', 'Laser Dots'], + }, + { + id: '3', + name: 'Pierre', + age: 4, + breed: 'French Bulldog', + type: 'dog', + bio: 'Oui oui! I am a distinguished gentlepup with a passion for snoring and being adorable. Bonus points if you love snuggling! 🥖', + image: dog2, + distance: '3 miles away', + interests: ['Snoring', 'Snuggling', 'Short Walks', 'Fashion'], + }, + { + id: '4', + name: 'Luna', + age: 2, + breed: 'Persian', + type: 'cat', + bio: 'Elegant, fluffy, and fabulous. I deserve a companion as majestic as myself. Grooming sessions are my love language. ✨', + image: cat2, + distance: '4 miles away', + interests: ['Grooming', 'Sunbathing', 'Being Admired', 'Tuna'], + }, + { + id: '5', + name: 'Max', + age: 1, + breed: 'Beagle', + type: 'dog', + bio: 'Adventure seeker with the best nose in town! Always down for a good sniff and a treat. Will howl for you! 🎵', + image: dog3, + distance: '1.5 miles away', + interests: ['Sniffing', 'Howling', 'Exploring', 'Bacon'], + }, +]; diff --git a/src/index.css b/src/index.css index 4844bbd..07f88d4 100644 --- a/src/index.css +++ b/src/index.css @@ -8,92 +8,146 @@ All colors MUST be HSL. @layer base { :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; + --background: 30 50% 98%; + --foreground: 20 20% 20%; --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; + --card-foreground: 20 20% 20%; --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; + --popover-foreground: 20 20% 20%; - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; + --primary: 10 80% 65%; + --primary-foreground: 0 0% 100%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; + --secondary: 160 40% 50%; + --secondary-foreground: 0 0% 100%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; + --muted: 30 30% 92%; + --muted-foreground: 20 10% 50%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; + --accent: 45 90% 65%; + --accent-foreground: 20 20% 20%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; + --destructive: 0 70% 55%; + --destructive-foreground: 0 0% 100%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; + --border: 30 20% 88%; + --input: 30 20% 88%; + --ring: 10 80% 65%; - --radius: 0.5rem; + --radius: 1rem; - --sidebar-background: 0 0% 98%; + --like: 160 60% 45%; + --like-foreground: 0 0% 100%; + --nope: 0 70% 60%; + --nope-foreground: 0 0% 100%; + --superlike: 200 80% 55%; + --superlike-foreground: 0 0% 100%; - --sidebar-foreground: 240 5.3% 26.1%; - - --sidebar-primary: 240 5.9% 10%; - - --sidebar-primary-foreground: 0 0% 98%; - - --sidebar-accent: 240 4.8% 95.9%; - - --sidebar-accent-foreground: 240 5.9% 10%; - - --sidebar-border: 220 13% 91%; - - --sidebar-ring: 217.2 91.2% 59.8%; + --sidebar-background: 30 50% 98%; + --sidebar-foreground: 20 20% 20%; + --sidebar-primary: 10 80% 65%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 30 30% 92%; + --sidebar-accent-foreground: 20 20% 20%; + --sidebar-border: 30 20% 88%; + --sidebar-ring: 10 80% 65%; } .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; + --background: 20 15% 12%; + --foreground: 30 20% 92%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; + --card: 20 15% 16%; + --card-foreground: 30 20% 92%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; + --popover: 20 15% 16%; + --popover-foreground: 30 20% 92%; - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; + --primary: 10 80% 65%; + --primary-foreground: 0 0% 100%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; + --secondary: 160 40% 45%; + --secondary-foreground: 0 0% 100%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; + --muted: 20 15% 22%; + --muted-foreground: 30 15% 60%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; + --accent: 45 90% 65%; + --accent-foreground: 20 20% 15%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; + --destructive: 0 70% 50%; + --destructive-foreground: 0 0% 100%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; + --border: 20 15% 25%; + --input: 20 15% 25%; + --ring: 10 80% 65%; + + --like: 160 60% 45%; + --like-foreground: 0 0% 100%; + --nope: 0 70% 60%; + --nope-foreground: 0 0% 100%; + --superlike: 200 80% 55%; + --superlike-foreground: 0 0% 100%; + + --sidebar-background: 20 15% 12%; + --sidebar-foreground: 30 20% 92%; + --sidebar-primary: 10 80% 65%; --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; + --sidebar-accent: 20 15% 22%; + --sidebar-accent-foreground: 30 20% 92%; + --sidebar-border: 20 15% 25%; + --sidebar-ring: 10 80% 65%; } } +@keyframes swipe-right { + 0% { transform: translateX(0) rotate(0deg); opacity: 1; } + 100% { transform: translateX(150%) rotate(20deg); opacity: 0; } +} + +@keyframes swipe-left { + 0% { transform: translateX(0) rotate(0deg); opacity: 1; } + 100% { transform: translateX(-150%) rotate(-20deg); opacity: 0; } +} + +@keyframes float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-10px); } +} + +@keyframes pulse-heart { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.2); } +} + +@keyframes match-pop { + 0% { transform: scale(0) rotate(-10deg); opacity: 0; } + 50% { transform: scale(1.1) rotate(5deg); opacity: 1; } + 100% { transform: scale(1) rotate(0deg); opacity: 1; } +} + +.animate-swipe-right { + animation: swipe-right 0.4s ease-out forwards; +} + +.animate-swipe-left { + animation: swipe-left 0.4s ease-out forwards; +} + +.animate-float { + animation: float 3s ease-in-out infinite; +} + +.animate-pulse-heart { + animation: pulse-heart 0.6s ease-in-out; +} + +.animate-match-pop { + animation: match-pop 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards; +} + @layer base { * { @apply border-border; diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 7130b54..f73d437 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -1,12 +1,64 @@ -// Update this page (the content is just a fallback if you fail to update the page) +import { useState, useCallback } from 'react'; +import { Header } from '@/components/Header'; +import { SwipeCard } from '@/components/SwipeCard'; +import { MatchModal } from '@/components/MatchModal'; +import { EmptyState } from '@/components/EmptyState'; +import { pets, type Pet } from '@/data/pets'; const Index = () => { + const [currentPets, setCurrentPets] = useState([...pets]); + const [likedPets, setLikedPets] = useState([]); + const [matchedPet, setMatchedPet] = useState(null); + + const handleSwipe = useCallback((direction: 'left' | 'right') => { + const currentPet = currentPets[currentPets.length - 1]; + + if (direction === 'right' && currentPet) { + setLikedPets((prev) => [...prev, currentPet]); + // Random chance of match (50%) + if (Math.random() > 0.5) { + setTimeout(() => setMatchedPet(currentPet), 500); + } + } + + setCurrentPets((prev) => prev.slice(0, -1)); + }, [currentPets]); + + const handleReset = useCallback(() => { + setCurrentPets([...pets]); + setLikedPets([]); + }, []); + + const handleCloseMatch = useCallback(() => { + setMatchedPet(null); + }, []); + return ( -
-
-

Welcome to Your Blank App

-

Start building your amazing project here!

-
+
+
+ +
+ {currentPets.length > 0 ? ( +
+ {/* Card stack */} + {currentPets.slice(-3).map((pet, index, arr) => ( + + ))} +
+ ) : ( + + )} +
+ + {/* Match modal */} + {matchedPet && ( + + )}
); }; diff --git a/tailwind.config.ts b/tailwind.config.ts index a1edb69..7247424 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -47,6 +47,18 @@ export default { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))", }, + like: { + DEFAULT: "hsl(var(--like))", + foreground: "hsl(var(--like-foreground))", + }, + nope: { + DEFAULT: "hsl(var(--nope))", + foreground: "hsl(var(--nope-foreground))", + }, + superlike: { + DEFAULT: "hsl(var(--superlike))", + foreground: "hsl(var(--superlike-foreground))", + }, sidebar: { DEFAULT: "hsl(var(--sidebar-background))", foreground: "hsl(var(--sidebar-foreground))", @@ -58,6 +70,10 @@ export default { ring: "hsl(var(--sidebar-ring))", }, }, + fontFamily: { + display: ['Fredoka', 'sans-serif'], + body: ['Nunito', 'sans-serif'], + }, borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)",