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
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
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
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
}
];
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;
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;
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;
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;
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);
}
9. Test Your Dating App
Launch your development server to experience your celebrity dating platform:
npm run dev
Open http://localhost:5173
and test these key features:
- Profile selection: Choose a demo user to experience different perspectives.
- Swiping interface: Browse potential matches with photo carousels and professional details.
- Matching system: Like profiles to trigger match notifications.
- 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)