Changes
This commit is contained in:
parent
b331aa1a7d
commit
13aefc8293
22
index.html
22
index.html
@ -1,26 +1,28 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<!-- TODO: Set the document title to the name of your application -->
|
||||
<title>Lovable App</title>
|
||||
<meta name="description" content="Lovable Generated Project" />
|
||||
<meta name="author" content="Lovable" />
|
||||
<title>Pawfect Match - Find Love for Your Pet</title>
|
||||
<meta name="description" content="Swipe right to find the perfect companion for your furry friend!" />
|
||||
<meta name="author" content="Pawfect Match" />
|
||||
|
||||
<!-- TODO: Update og:title to match your application name -->
|
||||
<meta property="og:title" content="Lovable App" />
|
||||
<meta property="og:description" content="Lovable Generated Project" />
|
||||
<meta property="og:title" content="Pawfect Match - Tinder for Animals" />
|
||||
<meta property="og:description" content="Swipe right to find the perfect companion for your furry friend!" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@Lovable" />
|
||||
<meta name="twitter:site" content="@PawfectMatch" />
|
||||
<meta name="twitter:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Nunito:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
BIN
src/assets/pets/cat1.jpg
Normal file
BIN
src/assets/pets/cat1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
BIN
src/assets/pets/cat2.jpg
Normal file
BIN
src/assets/pets/cat2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
BIN
src/assets/pets/dog1.jpg
Normal file
BIN
src/assets/pets/dog1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
src/assets/pets/dog2.jpg
Normal file
BIN
src/assets/pets/dog2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
BIN
src/assets/pets/dog3.jpg
Normal file
BIN
src/assets/pets/dog3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
37
src/components/EmptyState.tsx
Normal file
37
src/components/EmptyState.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { Heart, RefreshCw } from 'lucide-react';
|
||||
|
||||
interface EmptyStateProps {
|
||||
onReset: () => void;
|
||||
likedPets: number;
|
||||
}
|
||||
|
||||
export function EmptyState({ onReset, likedPets }: EmptyStateProps) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center px-8 text-center">
|
||||
<div className="animate-float mb-6">
|
||||
<Heart className="h-20 w-20 fill-muted text-muted" />
|
||||
</div>
|
||||
|
||||
<h2 className="font-display text-2xl font-bold text-foreground">
|
||||
No More Pets Nearby!
|
||||
</h2>
|
||||
|
||||
<p className="mt-3 font-body text-muted-foreground">
|
||||
You've seen all the adorable pets in your area.
|
||||
{likedPets > 0 && (
|
||||
<span className="mt-1 block text-primary">
|
||||
You matched with {likedPets} pet{likedPets > 1 ? 's' : ''}! 🎉
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={onReset}
|
||||
className="mt-8 flex items-center gap-2 rounded-full bg-primary px-8 py-4 font-display font-semibold text-primary-foreground shadow-lg transition-all hover:scale-105 hover:shadow-xl"
|
||||
>
|
||||
<RefreshCw className="h-5 w-5" />
|
||||
Start Over
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
22
src/components/Header.tsx
Normal file
22
src/components/Header.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { Heart, User, Settings } from 'lucide-react';
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<header className="flex items-center justify-between px-6 py-4">
|
||||
<button className="rounded-full p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground">
|
||||
<User className="h-6 w-6" />
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Heart className="h-8 w-8 fill-primary text-primary" />
|
||||
<h1 className="font-display text-2xl font-bold text-foreground">
|
||||
Paw<span className="text-primary">fect</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<button className="rounded-full p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground">
|
||||
<Settings className="h-6 w-6" />
|
||||
</button>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
65
src/components/MatchModal.tsx
Normal file
65
src/components/MatchModal.tsx
Normal file
@ -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 (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4 backdrop-blur-sm">
|
||||
<div className="animate-match-pop relative max-w-sm rounded-3xl bg-gradient-to-br from-primary via-primary to-secondary p-1 shadow-2xl">
|
||||
<div className="rounded-3xl bg-card p-8 text-center">
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute right-4 top-4 rounded-full p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
{/* Hearts animation */}
|
||||
<div className="mb-4 flex justify-center">
|
||||
<div className="animate-float relative">
|
||||
<Heart className="h-16 w-16 fill-primary text-primary" />
|
||||
<Heart className="absolute -right-4 -top-2 h-8 w-8 fill-secondary text-secondary opacity-70" />
|
||||
<Heart className="absolute -left-3 top-4 h-6 w-6 fill-accent text-accent opacity-60" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="font-display text-3xl font-bold text-primary">It's a Match!</h2>
|
||||
<p className="mt-2 font-body text-muted-foreground">
|
||||
You and <span className="font-semibold text-foreground">{pet.name}</span> liked each other!
|
||||
</p>
|
||||
|
||||
{/* Pet image */}
|
||||
<div className="mx-auto mt-6 h-32 w-32 overflow-hidden rounded-full border-4 border-primary shadow-lg">
|
||||
<img
|
||||
src={pet.image}
|
||||
alt={pet.name}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="mt-8 flex flex-col gap-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex items-center justify-center gap-2 rounded-full bg-primary px-6 py-3 font-display font-semibold text-primary-foreground shadow-lg transition-all hover:scale-105 hover:shadow-xl"
|
||||
>
|
||||
<MessageCircle className="h-5 w-5" />
|
||||
Send a Woof!
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-full px-6 py-3 font-body font-medium text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Keep Swiping
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
159
src/components/SwipeCard.tsx
Normal file
159
src/components/SwipeCard.tsx
Normal file
@ -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 (
|
||||
<div
|
||||
className={`absolute inset-0 transition-transform ${
|
||||
swipeDirection === 'right'
|
||||
? 'animate-swipe-right'
|
||||
: swipeDirection === 'left'
|
||||
? 'animate-swipe-left'
|
||||
: ''
|
||||
}`}
|
||||
style={{
|
||||
transform: dragStart
|
||||
? `translateX(${dragOffset.x}px) rotate(${rotation}deg)`
|
||||
: undefined,
|
||||
opacity: dragStart ? opacity : 1,
|
||||
zIndex: isTop ? 10 : 0,
|
||||
}}
|
||||
onMouseDown={(e) => 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}
|
||||
>
|
||||
<div className="relative h-full w-full overflow-hidden rounded-3xl bg-card shadow-2xl cursor-grab active:cursor-grabbing">
|
||||
{/* Image */}
|
||||
<img
|
||||
src={pet.image}
|
||||
alt={pet.name}
|
||||
className="h-full w-full object-cover"
|
||||
draggable={false}
|
||||
/>
|
||||
|
||||
{/* Gradient overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent" />
|
||||
|
||||
{/* Like/Nope indicators */}
|
||||
{dragOffset.x > 50 && (
|
||||
<div className="absolute left-6 top-6 rotate-[-15deg] rounded-lg border-4 border-like px-4 py-2">
|
||||
<span className="font-display text-3xl font-bold text-like">LIKE</span>
|
||||
</div>
|
||||
)}
|
||||
{dragOffset.x < -50 && (
|
||||
<div className="absolute right-6 top-6 rotate-[15deg] rounded-lg border-4 border-nope px-4 py-2">
|
||||
<span className="font-display text-3xl font-bold text-nope">NOPE</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pet info */}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-6 text-primary-foreground">
|
||||
<div className="flex items-end justify-between">
|
||||
<div className="flex-1">
|
||||
<h2 className="font-display text-4xl font-bold drop-shadow-lg">
|
||||
{pet.name}, {pet.age}
|
||||
</h2>
|
||||
<p className="mt-1 font-body text-lg opacity-90 drop-shadow">{pet.breed}</p>
|
||||
<div className="mt-2 flex items-center gap-1 opacity-80">
|
||||
<MapPin className="h-4 w-4" />
|
||||
<span className="font-body text-sm">{pet.distance}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowInfo(!showInfo);
|
||||
}}
|
||||
className="rounded-full bg-card/30 p-3 backdrop-blur-sm transition-all hover:bg-card/50"
|
||||
>
|
||||
<Info className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Expanded info */}
|
||||
{showInfo && (
|
||||
<div className="mt-4 rounded-2xl bg-card/20 p-4 backdrop-blur-sm">
|
||||
<p className="font-body text-sm leading-relaxed">{pet.bio}</p>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{pet.interests.map((interest) => (
|
||||
<span
|
||||
key={interest}
|
||||
className="rounded-full bg-primary/80 px-3 py-1 font-body text-xs font-medium"
|
||||
>
|
||||
{interest}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
{isTop && (
|
||||
<div className="absolute -bottom-20 left-1/2 flex -translate-x-1/2 gap-6">
|
||||
<button
|
||||
onClick={() => handleSwipe('left')}
|
||||
className="group flex h-16 w-16 items-center justify-center rounded-full border-2 border-nope bg-card shadow-lg transition-all hover:scale-110 hover:bg-nope"
|
||||
>
|
||||
<X className="h-8 w-8 text-nope transition-colors group-hover:text-nope-foreground" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSwipe('right')}
|
||||
className="group flex h-16 w-16 items-center justify-center rounded-full border-2 border-like bg-card shadow-lg transition-all hover:scale-110 hover:bg-like"
|
||||
>
|
||||
<Heart className="h-8 w-8 text-like transition-colors group-hover:text-like-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
75
src/data/pets.ts
Normal file
75
src/data/pets.ts
Normal file
@ -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'],
|
||||
},
|
||||
];
|
||||
170
src/index.css
170
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;
|
||||
|
||||
@ -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<Pet[]>([...pets]);
|
||||
const [likedPets, setLikedPets] = useState<Pet[]>([]);
|
||||
const [matchedPet, setMatchedPet] = useState<Pet | null>(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 (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background">
|
||||
<div className="text-center">
|
||||
<h1 className="mb-4 text-4xl font-bold">Welcome to Your Blank App</h1>
|
||||
<p className="text-xl text-muted-foreground">Start building your amazing project here!</p>
|
||||
</div>
|
||||
<div className="flex min-h-screen flex-col bg-background font-body">
|
||||
<Header />
|
||||
|
||||
<main className="relative flex flex-1 flex-col items-center justify-center px-4 pb-28">
|
||||
{currentPets.length > 0 ? (
|
||||
<div className="relative h-[70vh] w-full max-w-sm">
|
||||
{/* Card stack */}
|
||||
{currentPets.slice(-3).map((pet, index, arr) => (
|
||||
<SwipeCard
|
||||
key={pet.id}
|
||||
pet={pet}
|
||||
isTop={index === arr.length - 1}
|
||||
onSwipe={handleSwipe}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState onReset={handleReset} likedPets={likedPets.length} />
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* Match modal */}
|
||||
{matchedPet && (
|
||||
<MatchModal pet={matchedPet} onClose={handleCloseMatch} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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)",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user