Creating a Virtual Pet that Lives in Your Blog

Creating a Virtual Pet that Lives in Your Blog

Static content is great, but what if you could add something truly interactive and personal to your blog? In this post, I'll show you how to create a virtual pet component that lives in your blog, similar to the classic Tamagotchi toys. Your readers can interact with it, and it will remember its state between visits!

Meet Your New Blog Pet

Here's your very own virtual pet! Give it a name, feed it, play with it, and make sure it gets enough rest. It will remember its state even if you leave and come back later. Each pet has its own unique personality and favorite activity!

Pixel

😊

Loading pet...

Hunger--
Happiness--
Energy--
Health--

Why Add a Virtual Pet to Your Blog?

A virtual pet adds several unique elements to your blog:

  • Creates a personal connection with your readers
  • Encourages return visits to check on the pet
  • Demonstrates your technical skills in a fun way
  • Adds a nostalgic element for those who loved Tamagotchis
  • Makes your blog stand out from the crowd

Let's build a component that maintains state across sessions and provides a fun, interactive experience for your readers.

Two Ways to Use the Pet

Our virtual pet component can be used in two ways:

  1. Embedded in content - As shown above, the pet can be embedded directly in your blog posts
  2. Floating in the corner - As you can see in the bottom right corner of this page, the pet can also float persistently on your site

The floating version is particularly engaging because:

  • It stays visible as users browse your site
  • It randomly shows speech bubbles with messages based on its mood and personality
  • It can spontaneously perform random actions like dancing or thinking
  • It's compact but expands when clicked, showing a larger interface
  • It automatically closes after interactions for a cleaner experience
  • It creates a consistent companion experience

Building the Virtual Pet Component

Our virtual pet component needs to:

  1. Track stats like hunger, happiness, and energy
  2. Save state to localStorage
  3. Update stats over time
  4. Provide interactive buttons for feeding, playing, and sleeping
  5. Display visual feedback based on the pet's state
  6. Support both embedded and floating modes
  7. Have unique personality traits that affect behavior
  8. Perform random actions to keep things interesting
  9. Prevent text selection when interacting with the pet

Here's the core structure of our component:

'use client';
 
import React, { useState, useEffect, useRef } from 'react';
 
interface PetStats {
  hunger: number;
  happiness: number;
  energy: number;
  health: number;
  lastInteraction: number;
  personality: 'playful' | 'shy' | 'grumpy' | 'cheerful';
  favoriteActivity: 'eating' | 'playing' | 'sleeping';
}
 
interface VirtualPetProps {
  initialName?: string;
  petId?: string;
  floating?: boolean;
}
 
export function VirtualPet({ initialName = 'Bloggy', petId = 'default', floating = false }: VirtualPetProps) {
  const [isClient, setIsClient] = useState(false);
  const [name, setName] = useState(initialName);
  const [stats, setStats] = useState<PetStats>({
    hunger: 80,
    happiness: 80,
    energy: 80,
    health: 100,
    lastInteraction: Date.now(),
    personality: 'playful',
    favoriteActivity: 'playing'
  });
  const [showControls, setShowControls] = useState(false);
  const [showSpeech, setShowSpeech] = useState(false);
  const [speechText, setSpeechText] = useState('');
  
  // Rest of the component...
}

Personality and Random Behaviors

To make each pet feel unique, we've added personality traits and random behaviors:

// Initialize with random personality and favorite activity
useEffect(() => {
  // ... existing code ...
  
  if (!saved) {
    const personalities = ['playful', 'shy', 'grumpy', 'cheerful'];
    const activities = ['eating', 'playing', 'sleeping'];
    
    setStats(prev => ({
      ...prev,
      personality: personalities[Math.floor(Math.random() * personalities.length)],
      favoriteActivity: activities[Math.floor(Math.random() * activities.length)]
    }));
  }
}, [petId]);
 
// Random events
useEffect(() => {
  if (!isClient || !floating) return;
  
  const randomEventInterval = setInterval(() => {
    if (petState === 'idle' && Date.now() - lastRandomEvent > 60000) {
      const shouldTriggerEvent = Math.random() < 0.15; // 15% chance
      
      if (shouldTriggerEvent) {
        const possibleEvents = ['dance', 'think', 'surprise', 'favorite'];
        const event = possibleEvents[Math.floor(Math.random() * possibleEvents.length)];
        
        // Handle different random events...
      }
    }
  }, 20000);
  
  return () => clearInterval(randomEventInterval);
}, [isClient, floating, petState, stats.favoriteActivity, lastRandomEvent]);

Improved Floating Pet Implementation

The floating pet is implemented as a fixed-position element in the bottom right corner, with a larger size and improved UI:

// Floating pet in the corner
if (floating) {
  return (
    <div className="fixed bottom-4 right-4 z-50 select-none">
      {showSpeech && (
        <div className="absolute bottom-20 right-0 bg-white dark:bg-gray-800 p-3 rounded-lg shadow-md mb-2 speech-bubble min-w-[150px]">
          <p className="text-sm">{speechText}</p>
        </div>
      )}
      
      <div 
        ref={petRef}
        className="relative w-20 h-20 bg-gradient-to-r from-blue-100 to-purple-100 dark:from-blue-900 dark:to-purple-900 rounded-full flex items-center justify-center cursor-pointer shadow-lg hover:shadow-xl transition-all"
        onClick={(e) => {
          e.preventDefault();
          setShowControls(!showControls);
        }}
      >
        <div className="text-4xl">{getEmoji()}</div>
      </div>
      
      {showControls && (
        <div 
          ref={controlsRef}
          className="absolute bottom-24 right-0 bg-white dark:bg-gray-800 p-4 rounded-lg shadow-md z-10 min-w-[220px]"
        >
          {/* Controls panel with name editing UI */}
        </div>
      )}
    </div>
  );
}

Auto-Closing Controls After Interactions

To keep the interface clean, we automatically close the controls panel after interactions:

const feed = () => {
  // ... existing feed logic ...
  
  if (floating) {
    setTimeout(() => setShowControls(false), 1000);
  }
};
 
const play = () => {
  // ... existing play logic ...
  
  if (floating) {
    setTimeout(() => setShowControls(false), 1000);
  }
};
 
const sleep = () => {
  // ... existing sleep logic ...
  
  if (floating) {
    setTimeout(() => setShowControls(false), 1000);
  }
};
 
const saveName = () => {
  // ... existing name save logic ...
  
  if (floating) {
    setTimeout(() => setShowControls(false), 1500);
  }
};

Preventing Text Selection

To ensure a smooth user experience, we've added measures to prevent text selection when interacting with the pet:

// Add select-none class to prevent text selection
<div className="fixed bottom-4 right-4 z-50 select-none">
  {/* Pet content */}
</div>
 
// Prevent default behavior on mouse events
<button
  onClick={(e) => {
    e.preventDefault();
    feed();
  }}
  onMouseDown={(e) => e.preventDefault()}
>
  🍔 Feed
</button>
 
// Close controls when clicking outside
useEffect(() => {
  if (!isClient || !floating) return;
  
  const handleClickOutside = (event: MouseEvent) => {
    if (
      showControls && 
      petRef.current && 
      controlsRef.current && 
      !petRef.current.contains(event.target as Node) && 
      !controlsRef.current.contains(event.target as Node)
    ) {
      setShowControls(false);
      if (isEditingName) {
        setIsEditingName(false);
        setTempName(name);
      }
    }
  };
  
  document.addEventListener('mousedown', handleClickOutside);
  return () => {
    document.removeEventListener('mousedown', handleClickOutside);
  };
}, [isClient, floating, showControls, isEditingName, name]);

Adding to Your MDX Blog

To use this component in your MDX blog, you need to register it in your MDX components:

// In your mdx-components.tsx file
import { VirtualPet } from "@/components/VirtualPet"
 
const components = {
  // Other components...
  VirtualPet
}

Then you can use it in any MDX file:

<!-- Embedded pet -->
<VirtualPet initialName="Bloggy" petId="unique-id" />
 
<!-- Floating pet -->
<VirtualPet initialName="Bloggy" petId="global-pet" floating={true} />

Adding the Floating Pet to Your Layout

For the best experience, you might want to add the floating pet to your site's layout so it appears on every page:

// In your layout.tsx file
import { VirtualPet } from "@/components/VirtualPet"
 
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        {children}
        <VirtualPet floating={true} petId="global-pet" initialName="Bloggy" />
      </body>
    </html>
  )
}

Potential Enhancements

There are many ways you could extend this virtual pet:

  1. Custom Avatars: Replace emojis with custom pet images or animations
  2. Achievements: Reward readers for taking good care of their pet
  3. Mini-games: Add simple games to play with the pet
  4. Growth stages: Let the pet evolve based on care quality
  5. Seasonal events: Special interactions during holidays
  6. More personalities: Add additional personality types with unique behaviors
  7. Pet items: Allow users to collect and give items to their pet
  8. Mobile optimization: Ensure the pet works well on touch devices

Conclusion

Adding a virtual pet to your blog creates a unique, interactive experience that encourages readers to return. The floating pet version is especially engaging as it creates a persistent companion that follows readers throughout your site.

The combination of React state management, localStorage persistence, time-based effects, and random behaviors creates a surprisingly engaging experience. Plus, it's a great conversation starter!

What other interactive elements would you like to see in blogs? Let me know in the comments!