DEV Community

Cover image for How to Build a Celebrity Dating App like Raya
Stephen568hub
Stephen568hub

Posted on • Edited on

How to Build a Celebrity Dating App like Raya

Building a dating app that celebrities actually use sounds like a dream project. Raya has turned this dream into reality by creating the most exclusive dating platform in the world. This premium app charges $25 monthly and rejects over 90% of applicants, yet millions still desperately want to join.

Famous actors, musicians, and wealthy entrepreneurs use Raya to find dates within their exclusive circles. The app proves that scarcity creates demand in the dating world.

In this article, we'll show you how to build a basic dating app like Raya. You'll discover what makes Raya special, how its core features work, and the essential components needed to create your own version. We'll walk through the fundamental building blocks, including user profiles, matching systems, and messaging features that bring people together.

What is Raya?

Raya is an exclusive dating app that launched in 2015. Unlike regular dating apps that anyone can join for free, Raya requires users to apply for membership and pay $25 each month if accepted.

The app only works on Apple devices like iPhones and iPads. Android users cannot download or use Raya, which adds to its exclusive appeal.

Getting accepted to Raya is extremely challenging. The app rejects more than 90% of people who apply. You need referrals from current members to even have a chance at approval. Some applicants wait months or years before getting accepted.

Raya attracts celebrities, actors, musicians, and wealthy professionals from creative industries. However, regular people can also get accepted if they work in interesting fields or have notable careers.

The app serves two main purposes. Most people use it for dating and finding relationships. Others use it for professional networking and making business connections within creative communities.

Raya has strict privacy rules that users must follow. Taking screenshots of profiles or conversations is forbidden. Breaking these rules can result in immediate removal from the platform.

The app gets its name from a Hebrew word meaning "friend." This reflects Raya's goal of building meaningful connections rather than promoting casual encounters. The platform focuses on quality relationships over quantity.

How Does Raya Work?

Raya operates through a strict application and membership system that sets it apart from regular dating apps. Here’s basically how Raya works:

  • Application process: Getting into Raya requires referrals from existing members. You submit an application with basic personal information, career details, and your Instagram handle. A committee of 500 trusted members reviews each application based on your social media presence, profession, and overall appeal to the community.
  • Profile creation: Once accepted, you create a profile using a slideshow format with up to six photos. You choose a song that plays in the background when others view your profile. Your Instagram account gets linked automatically for verification. You can write a brief bio, though this is optional.
  • Matching system: Raya shows you around 25 profiles at any time, not per day. You receive a few new profiles daily to review. The app connects users globally rather than focusing on local matches only. Both users must show interest to create a match and start messaging. Matches expire after 10 days if neither person initiates contact.
  • Key features: The app includes a map feature for finding nearby users, a directory for searching members by profession or location, and both dating and networking modes. You can also pay to message users before matching.
  • Privacy rules: Screenshots are completely forbidden and detected automatically. Sharing user information outside the platform or mentioning Raya users on social media results in immediate account removal.

How to Build a Celebrity Dating App like Raya

Creating an exclusive dating platform like Raya requires premium features that attract high-profile users who value privacy and quality connections. Raya's success stems from its selective membership process, sophisticated matching algorithms, and seamless communication tools that let celebrities and influencers connect safely.

ZEGOCLOUD SDKs provide the real-time messaging and video calling infrastructure you need to build these premium features without developing complex backend systems from scratch.

Prerequisites

Before building your celebrity dating app, make sure you have:

  • A ZEGOCLOUD developer account (sign up at console.zegocloud.com).
  • AppID and ServerSecret from your ZEGOCLOUD Admin Console.
  • Node.js version 16 or higher installed.
  • Basic knowledge of React and JavaScript.
  • A modern web browser for development testing.

Ready to start building? Let's create your exclusive dating platform!

1. Initialize Your React Project

Start by creating a new React application using Vite for optimal development performance and faster build times:

npm create vite@latest raya-dating-app -- --template react
cd raya-dating-app
npm install
Enter fullscreen mode Exit fullscreen mode

This command creates a modern React application with all the development tools configured for building interactive dating app features.

2. Install ZEGOCLOUD Communication SDKs

Install both messaging and video calling SDKs to handle all communication between matched users:

npm install zego-zim-web zego-express-engine-webrtc
Enter fullscreen mode Exit fullscreen mode

Your project structure should look like this:

raya-dating-app/
├── public/
├── src/
│   ├── components/
│   │   ├── ProfileCard/
│   │   ├── SwipeInterface/
│   │   ├── ChatRoom/
│   │   └── VideoDate/
│   ├── utils/
│   │   └── config.js
│   ├── App.jsx
│   ├── App.css
│   └── main.jsx
├── package.json
└── vite.config.js
Enter fullscreen mode Exit fullscreen mode

3. Configure Your App Settings

Create a centralized configuration file that manages your ZEGOCLOUD credentials and app data. Create src/utils/config.js:

export const DATING_CONFIG = {
    appID: your_zegocloud_app_id,
    serverSecret: "your_zegocloud_server_secret"
};

export function generateUserToken(userID) {
    // Development tokens for testing
    // In production, generate these securely on your backend
    const testTokens = {
        "alexandra_model": "dev_token_alexandra",
        "james_actor": "dev_token_james",
        "sophia_artist": "dev_token_sophia"
    };

    return testTokens[userID] || "";
}

export const DEMO_PROFILES = [
    {
        userID: "alexandra_model",
        name: "Alexandra",
        age: 28,
        profession: "Fashion Model",
        location: "Los Angeles, CA",
        photos: ["/api/placeholder/400/600", "/api/placeholder/400/600"],
        interests: ["Photography", "Travel", "Yoga"],
        verified: true
    },
    {
        userID: "james_actor",
        name: "James",
        age: 32,
        profession: "Film Actor",
        location: "New York, NY", 
        photos: ["/api/placeholder/400/600", "/api/placeholder/400/600"],
        interests: ["Theater", "Fitness", "Cooking"],
        verified: true
    },
    {
        userID: "sophia_artist",
        name: "Sophia",
        age: 26,
        profession: "Digital Artist",
        location: "Miami, FL",
        photos: ["/api/placeholder/400/600", "/api/placeholder/400/600"],
        interests: ["Art", "Music", "Dancing"],
        verified: true
    }
];
Enter fullscreen mode Exit fullscreen mode

This configuration provides everything needed for user authentication, profile management, and ZEGOCLOUD SDK integration.

4. Build Your Profile Swipe Component

Create the core swiping interface that lets users discover potential matches. Build src/components/SwipeInterface/SwipeInterface.jsx:

import React, { useState, useEffect } from 'react';
import { DEMO_PROFILES } from '../../utils/config';

const ProfileCard = ({ profile, onSwipe }) => {
  const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);

  const nextPhoto = () => {
    setCurrentPhotoIndex((prev) => 
      prev < profile.photos.length - 1 ? prev + 1 : 0
    );
  };

  const handleCardAction = (action) => {
    onSwipe(profile.userID, action);
  };

  return (
    <div className="profile-card">
      <div className="photo-container" onClick={nextPhoto}>
        <img 
          src={profile.photos[currentPhotoIndex]} 
          alt={`${profile.name}'s photo`}
          className="profile-photo"
        />
        <div className="photo-indicators">
          {profile.photos.map((_, index) => (
            <div 
              key={index}
              className={`indicator ${index === currentPhotoIndex ? 'active' : ''}`}
            />
          ))}
        </div>
        {profile.verified && (
          <div className="verification-badge"> Verified</div>
        )}
      </div>

      <div className="profile-info">
        <div className="name-age">
          <h2>{profile.name}, {profile.age}</h2>
        </div>
        <p className="profession">{profile.profession}</p>
        <p className="location">📍 {profile.location}</p>

        <div className="interests">
          {profile.interests.map((interest, index) => (
            <span key={index} className="interest-tag">
              {interest}
            </span>
          ))}
        </div>
      </div>

      <div className="action-buttons">
        <button 
          className="pass-button"
          onClick={() => handleCardAction('pass')}
        >
          
        </button>
        <button 
          className="like-button"
          onClick={() => handleCardAction('like')}
        >
          
        </button>
      </div>
    </div>
  );
};

const SwipeInterface = ({ currentUser, onMatch }) => {
  const [profiles, setProfiles] = useState([]);
  const [currentIndex, setCurrentIndex] = useState(0);

  useEffect(() => {
    // Filter out current user and load potential matches
    const availableProfiles = DEMO_PROFILES.filter(
      profile => profile.userID !== currentUser.userID
    );
    setProfiles(availableProfiles);
  }, [currentUser]);

  const handleSwipe = (profileID, action) => {
    if (action === 'like') {
      // Simulate match (in real app, this would be server logic)
      const matchedProfile = profiles.find(p => p.userID === profileID);
      onMatch(matchedProfile);
    }

    // Move to next profile
    setCurrentIndex(prev => prev + 1);
  };

  if (currentIndex >= profiles.length) {
    return (
      <div className="no-more-profiles">
        <h2>🎯 You're all caught up!</h2>
        <p>Check back later for new potential matches</p>
      </div>
    );
  }

  return (
    <div className="swipe-interface">
      <div className="app-header">
        <h1>💎 RayaClone</h1>
        <div className="user-info">
          Welcome, {currentUser.name}
        </div>
      </div>

      <div className="cards-container">
        {profiles.slice(currentIndex, currentIndex + 2).map((profile, index) => (
          <div 
            key={profile.userID}
            className={`card-wrapper ${index === 0 ? 'current' : 'next'}`}
            style={{ zIndex: 2 - index }}
          >
            <ProfileCard profile={profile} onSwipe={handleSwipe} />
          </div>
        ))}
      </div>
    </div>
  );
};

export default SwipeInterface;
Enter fullscreen mode Exit fullscreen mode

This component creates the signature Raya-style card interface with photo carousels, professional information, and swipe actions for discovering matches.

5. Create Your Chat Component

Build the messaging interface for matched users to communicate privately. Create src/components/ChatRoom/ChatRoom.jsx:

import React, { useState, useEffect, useRef } from 'react';
import { ZIM } from 'zego-zim-web';
import { DATING_CONFIG, generateUserToken } from '../../utils/config';

const ChatRoom = ({ currentUser, matchedUser, onVideoCall, onBack }) => {
  const [messages, setMessages] = useState([]);
  const [newMessage, setNewMessage] = useState('');
  const [zimInstance, setZimInstance] = useState(null);
  const [connectionStatus, setConnectionStatus] = useState('connecting');
  const messagesEndRef = useRef(null);

  useEffect(() => {
    initializeChat();
    return () => {
      disconnectChat();
    };
  }, [currentUser, matchedUser]);

  useEffect(() => {
    scrollToBottom();
  }, [messages]);

  const initializeChat = async () => {
    try {
      setConnectionStatus('connecting');

      const zim = ZIM.create({ appID: DATING_CONFIG.appID });

      // Set up message listeners
      zim.on('peerMessageReceived', handleIncomingMessage);
      zim.on('connectionStateChanged', handleConnectionChange);
      zim.on('error', handleError);

      // Login to ZIM
      const userInfo = {
        userID: currentUser.userID,
        userName: currentUser.name
      };

      await zim.login(userInfo, generateUserToken(currentUser.userID));

      setZimInstance(zim);
      setConnectionStatus('connected');

      // Load initial welcome message
      setMessages([{
        content: `You matched with ${matchedUser.name}! Start the conversation.`,
        timestamp: Date.now(),
        isSystem: true
      }]);

    } catch (error) {
      console.error('Chat initialization failed:', error);
      setConnectionStatus('error');
    }
  };

  const handleIncomingMessage = (zim, { messageList, fromConversationID }) => {
    if (fromConversationID === matchedUser.userID) {
      messageList.forEach(message => {
        setMessages(prev => [...prev, {
          content: message.message,
          timestamp: message.timestamp,
          isOwn: false,
          senderName: matchedUser.name
        }]);
      });
    }
  };

  const handleConnectionChange = (zim, { state, event }) => {
    if (state === 1) {
      setConnectionStatus('connected');
    } else if (state === 0) {
      setConnectionStatus('disconnected');
    }
  };

  const handleError = (zim, errorInfo) => {
    console.error('ZIM Error:', errorInfo);
    setConnectionStatus('error');
  };

  const sendMessage = async () => {
    if (!newMessage.trim() || !zimInstance) return;

    try {
      const messageObj = {
        type: 1, // Text message
        message: newMessage.trim()
      };

      await zimInstance.sendMessage(
        messageObj,
        matchedUser.userID,
        0, // Peer conversation
        { priority: 1 }
      );

      // Add to local messages
      setMessages(prev => [...prev, {
        content: newMessage.trim(),
        timestamp: Date.now(),
        isOwn: true
      }]);

      setNewMessage('');
    } catch (error) {
      console.error('Failed to send message:', error);
    }
  };

  const disconnectChat = async () => {
    if (zimInstance) {
      try {
        await zimInstance.logout();
        zimInstance.destroy();
      } catch (error) {
        console.error('Disconnect error:', error);
      }
    }
  };

  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
  };

  const handleKeyPress = (e) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      sendMessage();
    }
  };

  return (
    <div className="chat-room">
      <div className="chat-header">
        <button onClick={onBack} className="back-button">
           Back
        </button>
        <div className="match-info">
          <img 
            src={matchedUser.photos[0]} 
            alt={matchedUser.name}
            className="header-avatar"
          />
          <div className="match-details">
            <h3>{matchedUser.name}</h3>
            <span className="status">
              {connectionStatus === 'connected' ? 'Online' : 'Connecting...'}
            </span>
          </div>
        </div>
        <button onClick={onVideoCall} className="video-call-button">
          📹 Video Date
        </button>
      </div>

      <div className="messages-container">
        {messages.map((message, index) => (
          <div 
            key={index} 
            className={`message ${message.isOwn ? 'own' : ''} ${message.isSystem ? 'system' : ''}`}
          >
            {!message.isOwn && !message.isSystem && (
              <div className="sender-name">{message.senderName}</div>
            )}
            <div className="message-bubble">
              {message.content}
            </div>
            <div className="message-time">
              {new Date(message.timestamp).toLocaleTimeString([], {
                hour: '2-digit', 
                minute: '2-digit'
              })}
            </div>
          </div>
        ))}
        <div ref={messagesEndRef} />
      </div>

      <div className="message-input-container">
        <input
          type="text"
          value={newMessage}
          onChange={(e) => setNewMessage(e.target.value)}
          onKeyPress={handleKeyPress}
          placeholder={`Message ${matchedUser.name}...`}
          className="message-input"
          maxLength={500}
        />
        <button 
          onClick={sendMessage}
          disabled={!newMessage.trim()}
          className="send-button"
        >
          Send
        </button>
      </div>
    </div>
  );
};

export default ChatRoom;
Enter fullscreen mode Exit fullscreen mode

We used the ZEGOCLOUD's ZIM SDK to enable real-time messaging between matched users with connection status monitoring and message history.

6. Build Your Video Dating Component

Create the video calling interface for face-to-face conversations. Build src/components/VideoDate/VideoDate.jsx:

import React, { useState, useEffect, useRef } from 'react';
import { ZegoExpressEngine } from 'zego-express-engine-webrtc';
import { DATING_CONFIG } from '../../utils/config';

const VideoDate = ({ currentUser, matchedUser, onEndCall }) => {
  const [engineInstance, setEngineInstance] = useState(null);
  const [callStatus, setCallStatus] = useState('connecting');
  const [localStream, setLocalStream] = useState(null);
  const [remoteStream, setRemoteStream] = useState(null);
  const [micEnabled, setMicEnabled] = useState(true);
  const [cameraEnabled, setCameraEnabled] = useState(true);
  const localVideoRef = useRef(null);
  const remoteVideoRef = useRef(null);

  useEffect(() => {
    startVideoCall();
    return () => {
      endVideoCall();
    };
  }, []);

  const startVideoCall = async () => {
    try {
      setCallStatus('connecting');

      // Create engine instance
      const engine = new ZegoExpressEngine(
        DATING_CONFIG.appID,
        "wss://webliveroom-api.zego.im/ws"
      );

      // Set up event listeners
      engine.on('roomStateUpdate', handleRoomStateUpdate);
      engine.on('roomStreamUpdate', handleStreamUpdate);
      engine.on('publisherStateUpdate', handlePublisherStateUpdate);

      setEngineInstance(engine);

      // Generate unique room ID for this video date
      const roomID = `date_${currentUser.userID}_${matchedUser.userID}_${Date.now()}`;

      // Login to room
      await engine.loginRoom(roomID, {
        userID: currentUser.userID,
        userName: currentUser.name
      });

      // Create and publish local stream
      const stream = await engine.createStream({
        camera: {
          audio: true,
          video: true,
          videoQuality: 4 // High quality for dating
        }
      });

      if (localVideoRef.current) {
        stream.play(localVideoRef.current);
      }

      await engine.startPublishingStream(currentUser.userID, stream);
      setLocalStream(stream);
      setCallStatus('connected');

    } catch (error) {
      console.error('Video call failed:', error);
      setCallStatus('error');
    }
  };

  const handleRoomStateUpdate = (roomID, state, errorCode) => {
    if (state === 'CONNECTED') {
      setCallStatus('connected');
    } else if (state === 'DISCONNECTED') {
      setCallStatus('disconnected');
    }
  };

  const handleStreamUpdate = async (roomID, updateType, streamList) => {
    if (updateType === 'ADD' && streamList.length > 0) {
      try {
        // Play remote user's stream
        const remoteStreamObj = await engineInstance.startPlayingStream(
          streamList[0].streamID
        );

        if (remoteVideoRef.current) {
          remoteStreamObj.play(remoteVideoRef.current);
        }

        setRemoteStream(remoteStreamObj);
        setCallStatus('active');
      } catch (error) {
        console.error('Failed to play remote stream:', error);
      }
    } else if (updateType === 'DELETE') {
      setRemoteStream(null);
      // Other user left, end call after short delay
      setTimeout(() => onEndCall(), 2000);
    }
  };

  const handlePublisherStateUpdate = (result) => {
    if (result.state === 'PUBLISHING') {
      console.log('Successfully publishing stream');
    }
  };

  const toggleMicrophone = async () => {
    if (engineInstance && localStream) {
      try {
        await engineInstance.mutePublishStreamAudio(localStream, !micEnabled);
        setMicEnabled(!micEnabled);
      } catch (error) {
        console.error('Failed to toggle microphone:', error);
      }
    }
  };

  const toggleCamera = async () => {
    if (engineInstance && localStream) {
      try {
        await engineInstance.mutePublishStreamVideo(localStream, !cameraEnabled);
        setCameraEnabled(!cameraEnabled);
      } catch (error) {
        console.error('Failed to toggle camera:', error);
      }
    }
  };

  const endVideoCall = async () => {
    try {
      if (engineInstance) {
        if (localStream) {
          await engineInstance.stopPublishingStream(currentUser.userID);
          engineInstance.destroyStream(localStream);
        }
        if (remoteStream) {
          await engineInstance.stopPlayingStream(remoteStream.streamID);
        }
        await engineInstance.logoutRoom();
        engineInstance.destroyEngine();
      }
      onEndCall();
    } catch (error) {
      console.error('Error ending call:', error);
      onEndCall();
    }
  };

  const getStatusMessage = () => {
    switch (callStatus) {
      case 'connecting':
        return `Connecting to ${matchedUser.name}...`;
      case 'connected':
        return `Waiting for ${matchedUser.name} to join...`;
      case 'active':
        return `Video date with ${matchedUser.name}`;
      case 'error':
        return 'Connection failed';
      case 'disconnected':
        return 'Call ended';
      default:
        return '';
    }
  };

  return (
    <div className="video-date">
      <div className="video-container">
        <div className="remote-video-wrapper">
          <video 
            ref={remoteVideoRef}
            className="remote-video"
            autoPlay
            playsInline
            muted={false}
          />
          {!remoteStream && (
            <div className="video-placeholder">
              <div className="placeholder-avatar">
                <img src={matchedUser.photos[0]} alt={matchedUser.name} />
              </div>
              <div className="status-text">{getStatusMessage()}</div>
            </div>
          )}
        </div>

        <div className="local-video-wrapper">
          <video 
            ref={localVideoRef}
            className="local-video"
            autoPlay
            playsInline
            muted={true}
          />
          {!cameraEnabled && (
            <div className="video-off-indicator">Camera Off</div>
          )}
        </div>
      </div>

      <div className="call-controls">
        <button 
          onClick={toggleMicrophone}
          className={`control-button ${micEnabled ? 'active' : 'inactive'}`}
        >
          {micEnabled ? '🎤' : '🔇'}
        </button>

        <button 
          onClick={toggleCamera}
          className={`control-button ${cameraEnabled ? 'active' : 'inactive'}`}
        >
          {cameraEnabled ? '📹' : '📵'}
        </button>

        <button 
          onClick={endVideoCall}
          className="control-button end-call"
        >
          📞 End Date
        </button>
      </div>

      <div className="call-info">
        <h3>Video Date</h3>
        <p>{getStatusMessage()}</p>
      </div>
    </div>
  );
};

export default VideoDate;
Enter fullscreen mode Exit fullscreen mode

The video dating component uses the ZEGOCLOUD's Express SDK to create high-quality video calls between matched users with professional controls and status monitoring.

7. Integrate Your Main App Logic

Bring all components together in your main application file. Replace src/App.jsx:

import React, { useState } from 'react';
import SwipeInterface from './components/SwipeInterface/SwipeInterface';
import ChatRoom from './components/ChatRoom/ChatRoom';
import VideoDate from './components/VideoDate/VideoDate';
import { DEMO_PROFILES } from './utils/config';
import './App.css';

const LoginScreen = ({ onLogin }) => {
  const [selectedUser, setSelectedUser] = useState('');

  const handleLogin = () => {
    if (selectedUser) {
      const user = DEMO_PROFILES.find(p => p.userID === selectedUser);
      onLogin(user);
    }
  };

  return (
    <div className="login-screen">
      <div className="login-container">
        <h1>💎 RayaClone</h1>
        <p>Exclusive dating for verified professionals</p>

        <div className="demo-login">
          <h3>Demo Login</h3>
          <p>Choose a profile to experience the app:</p>

          <div className="profile-selector">
            {DEMO_PROFILES.map(profile => (
              <div 
                key={profile.userID}
                className={`profile-option ${selectedUser === profile.userID ? 'selected' : ''}`}
                onClick={() => setSelectedUser(profile.userID)}
              >
                <img src={profile.photos[0]} alt={profile.name} />
                <div className="profile-details">
                  <h4>{profile.name}</h4>
                  <p>{profile.profession}</p>
                </div>
              </div>
            ))}
          </div>

          <button 
            onClick={handleLogin}
            disabled={!selectedUser}
            className="login-button"
          >
            Enter RayaClone
          </button>
        </div>
      </div>
    </div>
  );
};

const MatchNotification = ({ match, onStartChat, onClose }) => {
  return (
    <div className="match-overlay">
      <div className="match-notification">
        <h2> It's a Match!</h2>
        <div className="matched-users">
          <img src={match.photos[0]} alt={match.name} />
        </div>
        <p>You and {match.name} liked each other</p>
        <div className="match-actions">
          <button onClick={onStartChat} className="start-chat-button">
            💬 Start Conversation
          </button>
          <button onClick={onClose} className="continue-button">
            Keep Swiping
          </button>
        </div>
      </div>
    </div>
  );
};

function App() {
  const [currentUser, setCurrentUser] = useState(null);
  const [currentScreen, setCurrentScreen] = useState('login');
  const [activeMatch, setActiveMatch] = useState(null);
  const [showMatchNotification, setShowMatchNotification] = useState(false);
  const [matches, setMatches] = useState([]);

  const handleLogin = (user) => {
    setCurrentUser(user);
    setCurrentScreen('swipe');
  };

  const handleMatch = (matchedProfile) => {
    setActiveMatch(matchedProfile);
    setMatches(prev => [...prev, matchedProfile]);
    setShowMatchNotification(true);
  };

  const startChatWithMatch = () => {
    setShowMatchNotification(false);
    setCurrentScreen('chat');
  };

  const startVideoDate = () => {
    setCurrentScreen('video');
  };

  const endVideoDate = () => {
    setCurrentScreen('chat');
  };

  const goBackToSwipe = () => {
    setCurrentScreen('swipe');
    setActiveMatch(null);
  };

  const renderCurrentScreen = () => {
    switch (currentScreen) {
      case 'login':
        return <LoginScreen onLogin={handleLogin} />;

      case 'swipe':
        return (
          <SwipeInterface 
            currentUser={currentUser}
            onMatch={handleMatch}
          />
        );

      case 'chat':
        return (
          <ChatRoom
            currentUser={currentUser}
            matchedUser={activeMatch}
            onVideoCall={startVideoDate}
            onBack={goBackToSwipe}
          />
        );

      case 'video':
        return (
          <VideoDate
            currentUser={currentUser}
            matchedUser={activeMatch}
            onEndCall={endVideoDate}
          />
        );

      default:
        return <LoginScreen onLogin={handleLogin} />;
    }
  };

  return (
    <div className="app">
      {renderCurrentScreen()}

      {showMatchNotification && activeMatch && (
        <MatchNotification
          match={activeMatch}
          onStartChat={startChatWithMatch}
          onClose={() => setShowMatchNotification(false)}
        />
      )}
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Your main app coordinates between swiping, matching, chatting, and video dating while maintaining clean state management across all user interactions.

8. Style Your Dating App

Create an elegant design that appeals to exclusive users. Replace src/App.css:

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', sans-serif;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: #333;
  overflow-x: hidden;
}

.app {
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 1rem;
  width: 100vw;
}

/* Login Screen */
.login-screen {
  width: 100%;
  max-width: 500px;
  display: flex;
  justify-content: center;
  align-items: center;
}

.login-container {
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(20px);
  padding: 3rem;
  border-radius: 24px;
  text-align: center;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
  border: 1px solid rgba(255, 255, 255, 0.2);
}

.login-container h1 {
  font-size: 3rem;
  margin-bottom: 1rem;
  background: linear-gradient(135deg, #667eea, #764ba2);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  font-weight: 700;
}

.login-container p {
  color: #666;
  margin-bottom: 2rem;
  font-size: 1.1rem;
}

.demo-login {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
}

.demo-login h3 {
  margin-bottom: 1rem;
  color: #333;
  font-weight: 600;
  text-align: center;
}

.demo-login p {
  text-align: center;
  margin-bottom: 2rem;
}

.profile-selector {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  margin: 2rem 0;
  width: 100%;
  max-width: 350px;
}

.profile-option {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 1rem;
  padding: 1rem;
  border: 2px solid #e5e7eb;
  border-radius: 16px;
  cursor: pointer;
  transition: all 0.3s ease;
  background: rgba(255, 255, 255, 0.8);
  width: 100%;
}

.profile-option:hover {
  border-color: #667eea;
  transform: translateY(-2px);
  box-shadow: 0 8px 25px rgba(102, 126, 234, 0.15);
}

.profile-option.selected {
  border-color: #667eea;
  background: rgba(102, 126, 234, 0.1);
}

.profile-option img {
  width: 60px;
  height: 60px;
  border-radius: 12px;
  object-fit: cover;
}

.profile-details {
  text-align: left;
  flex: 1;
}

.profile-details h4 {
  margin-bottom: 0.25rem;
  font-weight: 600;
}

.profile-details p {
  color: #666;
  font-size: 0.9rem;
  margin: 0;
}

.login-button {
  width: 100%;
  max-width: 350px;
  padding: 16px;
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
  border: none;
  border-radius: 16px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.3s ease;
  box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
  margin: 0 auto;
}

.login-button:hover:not(:disabled) {
  transform: translateY(-2px);
  box-shadow: 0 12px 35px rgba(102, 126, 234, 0.4);
}

.login-button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none;
}

/* Swipe Interface */
.swipe-interface {
  width: 100%;
  max-width: 400px;
  height: 90vh;
  max-height: 700px;
  display: flex;
  flex-direction: column;
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(20px);
  border-radius: 24px;
  overflow: hidden;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
}

.app-header {
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
  padding: 1.5rem 2rem;
  text-align: center;
}

.app-header h1 {
  font-size: 1.8rem;
  font-weight: 700;
  margin-bottom: 0.5rem;
}

.user-info {
  opacity: 0.9;
  font-size: 0.9rem;
}

.cards-container {
  flex: 1;
  position: relative;
  padding: 2rem;
  display: flex;
  justify-content: center;
  align-items: center;
}

.card-wrapper {
  position: absolute;
  width: 100%;
  max-width: 320px;
  height: 500px;
}

.card-wrapper.next {
  transform: scale(0.95) translateY(10px);
  opacity: 0.7;
}

.profile-card {
  width: 100%;
  height: 100%;
  background: white;
  border-radius: 20px;
  box-shadow: 0 15px 40px rgba(0, 0, 0, 0.1);
  overflow: hidden;
  position: relative;
  cursor: pointer;
  transition: all 0.3s ease;
}

.profile-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.15);
}

.photo-container {
  position: relative;
  height: 65%;
  overflow: hidden;
}

.profile-photo {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.photo-indicators {
  position: absolute;
  top: 1rem;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 0.5rem;
}

.indicator {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.5);
  transition: all 0.3s ease;
}

.indicator.active {
  background: white;
  transform: scale(1.2);
}

.verification-badge {
  position: absolute;
  top: 1rem;
  right: 1rem;
  background: rgba(34, 197, 94, 0.9);
  color: white;
  padding: 0.5rem 1rem;
  border-radius: 20px;
  font-size: 0.8rem;
  font-weight: 600;
  backdrop-filter: blur(10px);
}

.profile-info {
  padding: 1.5rem;
  height: 35%;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.name-age h2 {
  font-size: 1.5rem;
  font-weight: 700;
  margin-bottom: 0.5rem;
  color: #333;
}

.profession {
  color: #666;
  font-weight: 500;
  margin-bottom: 0.25rem;
}

.location {
  color: #888;
  font-size: 0.9rem;
  margin-bottom: 1rem;
}

.interests {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.interest-tag {
  background: rgba(102, 126, 234, 0.1);
  color: #667eea;
  padding: 0.25rem 0.75rem;
  border-radius: 20px;
  font-size: 0.8rem;
  font-weight: 500;
}

.action-buttons {
  position: absolute;
  bottom: 1rem;
  right: 1rem;
  display: flex;
  gap: 1rem;
}

.pass-button,
.like-button {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  border: none;
  font-size: 1.5rem;
  cursor: pointer;
  transition: all 0.3s ease;
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}

.pass-button {
  background: linear-gradient(135deg, #f87171, #ef4444);
  color: white;
}

.like-button {
  background: linear-gradient(135deg, #10b981, #059669);
  color: white;
}

.pass-button:hover,
.like-button:hover {
  transform: translateY(-2px) scale(1.1);
  box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
}

.no-more-profiles {
  text-align: center;
  padding: 3rem;
  color: #666;
}

.no-more-profiles h2 {
  font-size: 2rem;
  margin-bottom: 1rem;
  color: #333;
}

/* Match Notification */
.match-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background: rgba(0, 0, 0, 0.8);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
  backdrop-filter: blur(10px);
}

.match-notification {
  background: white;
  padding: 3rem;
  border-radius: 24px;
  text-align: center;
  max-width: 400px;
  margin: 2rem;
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.3);
}

.match-notification h2 {
  font-size: 2.5rem;
  margin-bottom: 2rem;
  background: linear-gradient(135deg, #667eea, #764ba2);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  font-weight: 700;
}

.matched-users img {
  width: 120px;
  height: 120px;
  border-radius: 50%;
  object-fit: cover;
  border: 4px solid #667eea;
  margin-bottom: 1.5rem;
}

.match-notification p {
  font-size: 1.2rem;
  color: #666;
  margin-bottom: 2rem;
}

.match-actions {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.start-chat-button,
.continue-button {
  padding: 16px 24px;
  border: none;
  border-radius: 16px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.3s ease;
}

.start-chat-button {
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
  box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
}

.continue-button {
  background: rgba(102, 126, 234, 0.1);
  color: #667eea;
  border: 1px solid #667eea;
}

.start-chat-button:hover,
.continue-button:hover {
  transform: translateY(-2px);
}

/* Chat Room */
.chat-room {
  width: 100%;
  max-width: 450px;
  height: 90vh;
  max-height: 700px;
  display: flex;
  flex-direction: column;
  background: white;
  border-radius: 24px;
  overflow: hidden;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
}

.chat-header {
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
  padding: 1.5rem 2rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.back-button {
  background: rgba(255, 255, 255, 0.2);
  color: white;
  border: none;
  padding: 8px 12px;
  border-radius: 8px;
  cursor: pointer;
  font-weight: 600;
  transition: all 0.3s ease;
}

.back-button:hover {
  background: rgba(255, 255, 255, 0.3);
}

.match-info {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex: 1;
  justify-content: center;
}

.header-avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  object-fit: cover;
  border: 2px solid rgba(255, 255, 255, 0.3);
}

.match-details h3 {
  font-weight: 700;
  margin-bottom: 0.25rem;
}

.status {
  font-size: 0.8rem;
  opacity: 0.8;
}

.video-call-button {
  background: rgba(255, 255, 255, 0.2);
  color: white;
  border: none;
  padding: 8px 12px;
  border-radius: 8px;
  cursor: pointer;
  font-weight: 600;
  transition: all 0.3s ease;
}

.video-call-button:hover {
  background: rgba(255, 255, 255, 0.3);
}

.messages-container {
  flex: 1;
  padding: 2rem;
  overflow-y: auto;
  background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
}

.message {
  margin-bottom: 1.5rem;
  display: flex;
  flex-direction: column;
}

.message.own {
  align-items: flex-end;
}

.message.system {
  align-items: center;
}

.sender-name {
  font-size: 0.8rem;
  color: #666;
  margin-bottom: 0.25rem;
  font-weight: 500;
}

.message-bubble {
  max-width: 75%;
  padding: 12px 16px;
  border-radius: 20px;
  word-wrap: break-word;
  line-height: 1.5;
  font-size: 0.95rem;
}

.message.own .message-bubble {
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
  border-bottom-right-radius: 6px;
}

.message:not(.own):not(.system) .message-bubble {
  background: white;
  color: #333;
  border: 1px solid #e5e7eb;
  border-bottom-left-radius: 6px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}

.message.system .message-bubble {
  background: rgba(102, 126, 234, 0.1);
  color: #667eea;
  font-style: italic;
  border-radius: 16px;
  text-align: center;
}

.message-time {
  font-size: 0.75rem;
  color: #999;
  margin-top: 0.25rem;
}

.message-input-container {
  padding: 1.5rem 2rem;
  background: white;
  border-top: 1px solid #e5e7eb;
  display: flex;
  gap: 1rem;
  align-items: center;
}

.message-input {
  flex: 1;
  padding: 12px 16px;
  border: 2px solid #e5e7eb;
  border-radius: 20px;
  outline: none;
  font-size: 16px;
  transition: all 0.3s ease;
}

.message-input:focus {
  border-color: #667eea;
  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}

.send-button {
  padding: 12px 20px;
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
  border: none;
  border-radius: 20px;
  cursor: pointer;
  font-weight: 600;
  transition: all 0.3s ease;
}

.send-button:hover:not(:disabled) {
  transform: translateY(-1px);
  box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}

.send-button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Video Date */
.video-date {
  width: 100%;
  max-width: 450px;
  height: 90vh;
  max-height: 700px;
  background: #000;
  border-radius: 24px;
  overflow: hidden;
  position: relative;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}

.video-container {
  width: 100%;
  height: 100%;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
}

.remote-video-wrapper {
  width: 100%;
  height: 100%;
  position: relative;
}

.remote-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.video-placeholder {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: linear-gradient(135deg, #1f2937, #374151);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: white;
}

.placeholder-avatar img {
  width: 120px;
  height: 120px;
  border-radius: 50%;
  object-fit: cover;
  border: 3px solid rgba(255, 255, 255, 0.3);
  margin-bottom: 2rem;
}

.status-text {
  font-size: 1.2rem;
  font-weight: 600;
  text-align: center;
  color: rgba(255, 255, 255, 0.9);
}

.local-video-wrapper {
  position: absolute;
  top: 20px;
  right: 20px;
  width: 120px;
  height: 160px;
  border-radius: 16px;
  overflow: hidden;
  border: 2px solid rgba(255, 255, 255, 0.3);
  background: #333;
}

.local-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.video-off-indicator {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.8);
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-size: 0.8rem;
  font-weight: 600;
}

.call-controls {
  position: absolute;
  bottom: 30px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 1rem;
  background: rgba(0, 0, 0, 0.6);
  padding: 1rem 1.5rem;
  border-radius: 20px;
  backdrop-filter: blur(10px);
}

.control-button {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  border: none;
  font-size: 1.2rem;
  cursor: pointer;
  transition: all 0.3s ease;
  background: rgba(255, 255, 255, 0.2);
  color: white;
  backdrop-filter: blur(10px);
}

.control-button:hover {
  background: rgba(255, 255, 255, 0.3);
  transform: translateY(-2px);
}

.control-button.active {
  background: rgba(34, 197, 94, 0.8);
}

.control-button.inactive {
  background: rgba(239, 68, 68, 0.8);
}

.control-button.end-call {
  background: linear-gradient(135deg, #dc2626, #ef4444);
  width: auto;
  padding: 0 1rem;
  font-size: 0.9rem;
  font-weight: 600;
}

.call-info {
  position: absolute;
  top: 20px;
  left: 20px;
  color: white;
  background: rgba(0, 0, 0, 0.6);
  padding: 1rem 1.5rem;
  border-radius: 16px;
  backdrop-filter: blur(10px);
}

.call-info h3 {
  font-size: 1.1rem;
  font-weight: 700;
  margin-bottom: 0.25rem;
}

.call-info p {
  font-size: 0.9rem;
  opacity: 0.9;
}

/* Responsive Design */
@media (max-width: 768px) {
  .app {
    padding: 0;
    align-items: stretch;
  }

  .swipe-interface,
  .chat-room,
  .video-date {
    height: 100vh;
    max-height: none;
    border-radius: 0;
    max-width: 100%;
  }

  .login-container {
    margin: 1rem;
    padding: 2rem;
  }

  .profile-selector {
    gap: 0.75rem;
  }

  .profile-option {
    padding: 0.75rem;
  }

  .profile-option img {
    width: 50px;
    height: 50px;
  }

  .cards-container {
    padding: 1rem;
  }

  .card-wrapper {
    max-width: 100%;
  }

  .profile-card {
    height: 450px;
  }

  .local-video-wrapper {
    width: 100px;
    height: 130px;
    top: 15px;
    right: 15px;
  }

  .call-controls {
    gap: 0.75rem;
    padding: 0.75rem 1rem;
  }

  .control-button {
    width: 45px;
    height: 45px;
    font-size: 1rem;
  }
}

.messages-container::-webkit-scrollbar {
  width: 6px;
}

.messages-container::-webkit-scrollbar-track {
  background: rgba(0, 0, 0, 0.05);
}

.messages-container::-webkit-scrollbar-thumb {
  background: rgba(102, 126, 234, 0.3);
  border-radius: 3px;
}

.messages-container::-webkit-scrollbar-thumb:hover {
  background: rgba(102, 126, 234, 0.5);
}
Enter fullscreen mode Exit fullscreen mode

9. Test Your Dating App

Launch your development server to experience your celebrity dating platform:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:5173 and test these key features:

Image description

  • Profile selection: Choose a demo user to experience different perspectives.

profile selection

  • Swiping interface: Browse potential matches with photo carousels and professional details.

date match

  • Matching system: Like profiles to trigger match notifications.

chat

  • Real-time chat: Send messages between matched users using ZEGOCLOUD's messaging.
  • Video dating: Start face-to-face conversations with high-quality video calls.
  • Responsive design: Test on mobile devices for complete user experience.

"Download the complete source code from GitHub to customize the matching system, add moderation features, or update the design. This sample shows the basics - ZEGOCLOUD's SDKs can power much more advanced communication features as your app grows.

Conclusion

Building a dating app like Raya demonstrates how the right tools can transform complex ideas into reality. ZEGOCLOUD's SDKs eliminated months of backend development, letting you focus on creating engaging user experiences instead of wrestling with video protocols and messaging infrastructure.

Your exclusive dating platform now connects users through sophisticated matching, private conversations, and face-to-face video dates. The app handles everything from profile discovery to real-time communication with professional-grade quality that celebrity users expect.

Whether you're launching the next premium dating platform or adding social features to existing apps, you've proven that powerful communication tools are within reach. Start building, iterate based on user feedback, and create connections that matter.

Top comments (0)