import { useState, useCallback } from 'react';
import { useAI, useAuth, useBroadcast, useCollection, useNotifications, usePresence, sendEmail } from '@deplixo/sdk';
import { ProfileSetup } from './components/ProfileSetup.jsx';
import { ExchangeBoard } from './components/ExchangeBoard.jsx';
import { Matches } from './components/Matches.jsx';
import { MyProfile } from './components/MyProfile.jsx';
function App() {
const { user, loading, login, logout } = useAuth();
const { items: profiles, loading: profilesLoading, add, update } = useCollection('language_profiles', { personal: true });
const { items: loungeItems, loading: loungeLoading, add: addLounge, update: updateLounge } = useCollection('chat_lounges', { personal: true });
const { items: loungeMessageItems, loading: loungeMessagesLoading, add: addLoungeMessage } = useCollection('chat_lounge_messages', { personal: true });
const { items: introItems, loading: introLoading, add: addIntro, update: updateIntro } = useCollection('match_intros', { personal: true });
const { notifications, unreadCount, send: sendNotification, markRead, loading: notificationsLoading } = useNotifications();
const { generate, loading: aiLoading, error: aiError } = useAI();
const [activeTab, setActiveTab] = useState('board');
const [editMode, setEditMode] = useState(false);
const [loungeName, setLoungeName] = useState('');
const [joinedLoungeId, setJoinedLoungeId] = useState('');
const [messageText, setMessageText] = useState('');
const [matchSuggestions, setMatchSuggestions] = useState([]);
const [matchLoading, setMatchLoading] = useState(false);
const [matchError, setMatchError] = useState('');
const [notifiedMatchIds, setNotifiedMatchIds] = useState([]);
const [introModalMatch, setIntroModalMatch] = useState(null);
const [introDraft, setIntroDraft] = useState('');
const [introGenerating, setIntroGenerating] = useState(false);
const [introSending, setIntroSending] = useState(false);
const [introStatusMessage, setIntroStatusMessage] = useState('');
const presence = usePresence(
user
? {
name: user.name,
avatar: user.avatar,
status: 'online',
}
: { name: 'Guest', avatar: '', status: 'offline' }
);
const onlineUsers = presence?.users || [];
const activeLounge = loungeItems?.find(lounge => lounge.id === joinedLoungeId) || null;
const loungeMessages = (loungeMessageItems || []).filter(message => message.loungeId === joinedLoungeId);
const sentIntros = introItems || [];
const handleLoungeBroadcast = useCallback((data, authorId) => {
if (!data?.loungeId || data.loungeId !== joinedLoungeId) return;
if (data.type === 'message' || data.type === 'join' || data.type === 'create') {
addLoungeMessage({
loungeId: data.loungeId,
authorId: data.authorId || authorId || 'system',
authorName: data.authorName || 'Anonymous',
authorAvatar: data.authorAvatar || '',
type: data.type,
text: data.text || '',
createdAt: data.createdAt || new Date().toISOString(),
});
}
}, [joinedLoungeId, addLoungeMessage]);
const { send: sendLoungeBroadcast } = useBroadcast('lounge-message', handleLoungeBroadcast);
const userProfile = profiles?.find(profile => profile.userId === user?.id) || null;
const buildIntroPrompt = useCallback((match, profile) => {
const myName = user?.name || 'User';
const partnerName = match?.userName || 'your match';
const myNative = profile?.nativeLanguage || 'unspecified';
const myLearning = profile?.learningLanguage || 'unspecified';
const partnerNative = match?.nativeLanguage || 'unspecified';
const partnerLearning = match?.learningLanguage || 'unspecified';
return [
`Write a warm, concise email introduction for two matched language partners.`,
`Sender name: ${myName}`,
`Recipient name: ${partnerName}`,
`Sender native language: ${myNative}`,
`Sender learning language: ${myLearning}`,
`Recipient native language: ${partnerNative}`,
`Recipient learning language: ${partnerLearning}`,
`Tone: friendly, encouraging, and practical.`,
`Include a short greeting, why they were matched, and a simple invitation to reply and start practicing.`,
`Keep it under 180 words.`,
].join('\n');
}, [user?.name]);
const handleOpenIntro = useCallback(async match => {
setIntroModalMatch(match || null);
setIntroStatusMessage('');
setIntroDraft('');
if (!match) return;
const existingIntro = sentIntros.find(item => item.matchUserId === match.userId && item.senderId === user?.id);
if (existingIntro?.introHtml) {
setIntroDraft(existingIntro.introHtml);
return;
}
setIntroGenerating(true);
try {
const prompt = buildIntroPrompt(match, userProfile);
const result = await generate({
system: 'You create short, friendly email introductions for language exchange partners.',
user: prompt,
json: true,
});
const html = typeof result === 'string'
? result
: (result?.html || result?.emailHtml || result?.body || '');
setIntroDraft(html || `
Hi ${match?.userName || 'there'}
I’d love to introduce you to a new language exchange partner.
`);
} catch (error) {
setIntroDraft(`Hi ${match?.userName || 'there'}
You’ve been matched with a language exchange partner. Reply to start practicing together.
`);
} finally {
setIntroGenerating(false);
}
}, [buildIntroPrompt, generate, sentIntros, user?.id, userProfile]);
const refreshMatchSuggestions = useCallback(async () => {
if (!user || !profiles) return;
setMatchLoading(true);
setMatchError('');
try {
const preferencesSummary = userProfile
? [
`Native language: ${userProfile.nativeLanguage || 'unspecified'}`,
`Learning language: ${userProfile.learningLanguage || 'unspecified'}`,
`Timezone: ${userProfile.timezone || 'unspecified'}`,
`Preferred practice style: ${userProfile.practiceStyle || 'unspecified'}`,
`Availability: ${userProfile.availability || 'unspecified'}`,
].join('\n')
: 'No profile found yet. Use whatever is available in the user profile data.';
const candidateProfiles = (profiles || [])
.filter(profile => profile.userId !== user.id)
.map(profile => ({
userId: profile.userId || '',
userName: profile.userName || 'Anonymous',
nativeLanguage: profile.nativeLanguage || '',
learningLanguage: profile.learningLanguage || '',
timezone: profile.timezone || '',
practiceStyle: profile.practiceStyle || '',
availability: profile.availability || '',
goals: profile.goals || '',
bio: profile.bio || '',
avatar: profile.userAvatar || profile.avatar || '',
}));
const response = await generateMatchSuggestions({
userName: user.name,
userPreferences: preferencesSummary,
candidates: candidateProfiles,
});
const parsed = Array.isArray(response) ? response : (response?.matches || response?.suggestions || []);
setMatchSuggestions(parsed.slice(0, 5));
} catch (error) {
setMatchError('Unable to generate AI matchmaking suggestions right now.');
} finally {
setMatchLoading(false);
}
}, [profiles, user, userProfile]);
const { send: sendMatchRefreshBroadcast } = useBroadcast('match-refresh', async () => {});
useEffect(() => {
if (user && activeTab === 'matches' && profiles && !profilesLoading) {
refreshMatchSuggestions();
}
}, [activeTab, profiles, profilesLoading, refreshMatchSuggestions, user]);
useEffect(() => {
if (!user || !matchSuggestions.length || !profiles?.length) return;
const matchedIds = new Set([
...(matchSuggestions || []).map(match => match?.userId).filter(Boolean),
...(notifiedMatchIds || []),
]);
const targetProfiles = (profiles || []).filter(profile => matchedIds.has(profile.userId));
if (!targetProfiles.length) return;
const onlineIds = new Set((onlineUsers || []).map(person => person.id).filter(Boolean));
const availableMatches = targetProfiles.filter(profile => onlineIds.has(profile.userId));
const notifyAvailableMatches = async () => {
for (const profile of availableMatches) {
if (!profile?.userId || (notifiedMatchIds || []).includes(profile.userId)) continue;
await sendNotification({
to: user.id,
title: 'Good match is online',
body: `${profile.userName || 'A recommended partner'} is now available to chat.`,
});
setNotifiedMatchIds(prev => (prev.includes(profile.userId) ? prev : [...prev, profile.userId]));
}
};
notifyAvailableMatches();
}, [matchSuggestions, notifiedMatchIds, onlineUsers, profiles, sendNotification, user]);
const handleMatchBroadcast = useCallback((data) => {
if (!data?.type || data.type !== 'match-refresh' || data.userId !== user?.id) return;
if (Array.isArray(data.suggestions)) {
setMatchSuggestions(data.suggestions);
}
}, [user?.id]);
useBroadcast('match-refresh', handleMatchBroadcast);
const handleSendIntroEmail = useCallback(async () => {
if (!user || !introModalMatch?.userId) return;
setIntroSending(true);
setIntroStatusMessage('');
try {
const recipientProfile = profiles?.find(profile => profile.userId === introModalMatch.userId) || null;
const recipientEmail = recipientProfile?.userEmail || recipientProfile?.email || '';
const senderName = user.name || 'A language exchange member';
const recipientName = introModalMatch.userName || recipientProfile?.userName || 'there';
const subject = `Intro from ${senderName} via LangBridge`;
const html = introDraft || `Hi ${recipientName}
${senderName} would love to connect and start practicing together.
`;
if (!recipientEmail) {
throw new Error('Recipient email unavailable');
}
if (typeof sendEmail === 'function') {
await sendEmail({
to: recipientEmail,
subject,
html,
});
}
const payload = {
senderId: user.id,
senderName: senderName,
senderEmail: user.email || '',
matchUserId: introModalMatch.userId,
matchUserName: recipientName,
recipientEmail,
subject,
introHtml: html,
sentAt: new Date().toISOString(),
status: 'sent',
};
const created = await addIntro(payload);
if (created?.id) {
await updateIntro(created.id, { ...payload, id: created.id });
}
await sendNotification({
to: user.id,
title: 'Intro email sent',
body: `Your introduction email to ${recipientName} has been sent.`,
});
setIntroStatusMessage('Intro email sent successfully.');
setIntroModalMatch(null);
setIntroDraft('');
} catch (error) {
setIntroStatusMessage('Unable to send intro email right now.');
} finally {
setIntroSending(false);
}
}, [addIntro, introDraft, introModalMatch, profiles, sendNotification, user, updateIntro]);
if (loading) {
return (
🌍
Connecting you to the world...
);
}
if (!user) {
return (
🌍
Sign in with Google
Create your language exchange profile, browse partners, and find matching learners.
);
}
const tabs = [
{ id: 'board', label: 'Exchange Board', icon: '🌐' },
{ id: 'matches', label: 'My Matches', icon: '🤝' },
{ id: 'lounge', label: 'Chat Lounge', icon: '💬' },
{ id: 'profile', label: 'My Profile', icon: '👤' },
];
const handleSaveProfile = async profileData => {
const payload = {
...profileData,
userId: user.id,
userName: user.name,
userEmail: user.email,
userAvatar: user.avatar,
updatedAt: new Date().toISOString(),
};
if (userProfile?.id) {
await update(userProfile.id, payload);
} else {
await add({
...payload,
createdAt: new Date().toISOString(),
});
}
setEditMode(false);
setActiveTab('profile');
};
const handleEditProfile = () => {
setActiveTab('profile');
setEditMode(true);
};
const handleCreateLounge = async e => {
e.preventDefault();
const name = loungeName.trim();
if (!name) return;
const newLounge = {
name,
createdBy: user.id,
createdByName: user.name,
createdByAvatar: user.avatar,
createdAt: new Date().toISOString(),
memberIds: [user.id],
};
const created = await addLounge(newLounge);
const createdId = created?.id || created?.data?.id || '';
if (createdId) {
setJoinedLoungeId(createdId);
sendLoungeBroadcast({
type: 'create',
loungeId: createdId,
authorId: user.id,
authorName: user.name,
authorAvatar: user.avatar,
text: `${user.name} created lounge "${name}"`,
createdAt: new Date().toISOString(),
});
}
setLoungeName('');
setActiveTab('lounge');
};
const handleJoinLounge = async lounge => {
setJoinedLoungeId(lounge.id);
const nextMembers = Array.from(new Set([...(lounge.memberIds || []), user.id]));
if (lounge.id) {
await updateLounge(lounge.id, { ...lounge, memberIds: nextMembers, updatedAt: new Date().toISOString() });
}
sendLoungeBroadcast({
type: 'join',
loungeId: lounge.id,
authorId: user.id,
authorName: user.name,
authorAvatar: user.avatar,
text: `${user.name} joined ${lounge.name}`,
createdAt: new Date().toISOString(),
});
setActiveTab('lounge');
};
const handleSendMessage = async e => {
e.preventDefault();
const text = messageText.trim();
if (!text || !joinedLoungeId) return;
const payload = {
loungeId: joinedLoungeId,
authorId: user.id,
authorName: user.name,
authorAvatar: user.avatar,
type: 'message',
text,
createdAt: new Date().toISOString(),
};
await addLoungeMessage(payload);
sendLoungeBroadcast(payload);
setMessageText('');
};
if (profilesLoading || loungeLoading || loungeMessagesLoading || notificationsLoading || introLoading) {
return (
🌍
Loading your profile...
);
}
return (
{editMode && (
setEditMode(false)}
/>
)}
{!editMode && activeTab === 'board' && setEditMode(true)} />}
{!editMode && activeTab === 'matches' && (
AI Matchmaking Suggestions
Recommendations are based on language pair compatibility, timezone overlap, and your profile preferences.
{matchError ? {matchError}
: null}
{aiError ? {aiError}
: null}
{introStatusMessage ? {introStatusMessage}
: null}
{!userProfile ? (
Complete your profile to get personalized match recommendations.
) : (
{matchSuggestions.length > 0 ? (
matchSuggestions.map((match, index) => (
handleOpenIntro(match)}
/>
))
) : (
No suggestions yet. Generate a fresh set of AI recommendations.
)}
)}
)}
{!editMode && activeTab === 'lounge' && (
Group Chat Lounge
Create a lounge, join others, and chat live with broadcast updates.
Available Lounges
{(loungeItems || []).map(lounge => (
))}
{activeLounge ? activeLounge.name : 'Select a lounge to start chatting'}
{loungeMessages.map(message => (
{message.authorAvatar ?

:
👤}
{message.authorName}
{message.text}
))}
)}
{!editMode && activeTab === 'profile' && }
setIntroModalMatch(null)}
onChangeDraft={setIntroDraft}
onGenerate={() => introModalMatch ? handleOpenIntro(introModalMatch) : null}
onSend={handleSendIntroEmail}
/>
);
}
export { App };
function PresenceRoster({ users = [], title = 'Online now' }) {
return (
{title}
{users.length}
{users.map(user => (
{user.avatar ?

: null}
{user.name}
))}
);
}
function MatchSuggestionCard({ match, onIntro }) {
const score = typeof match?.score === 'number' ? match.score : null;
const reasons = Array.isArray(match?.reasons) ? match.reasons : [];
const overlap = match?.timezoneOverlap || match?.overlap || 'Good overlap';
return (
{match?.avatar ?

:
👤}
{match?.userName || 'Suggested Partner'}
{score !== null ? {score}% match : null}
{match?.nativeLanguage ? `${match.nativeLanguage} native` : 'Language data unavailable'}
{match?.learningLanguage ? ` • learning ${match.learningLanguage}` : ''}
Timezone overlap: {overlap}
{match?.summary ? {match.summary}
: null}
{reasons.length > 0 ? (
{reasons.map((reason, index) => (
- {reason}
))}
) : null}
{match?.userId ? : null}
);
}
function NotificationCenter({ notifications = [], unreadCount = 0, markRead }) {
return (
Notifications
Updates about recommended partners, matches, and activity.
{unreadCount} unread
{notifications.length > 0 ? notifications.map(notification => (
)) : (
)}
);
}
function MatchIntroModal({ isOpen, match, draftHtml = '', generating = false, sending = false, onClose, onChangeDraft, onGenerate, onSend }) {
if (!isOpen || !match) return null;
return (
Send intro email
Review the AI-drafted introduction before sending it to your match.
Recipient:
{match.userName || 'Suggested Partner'}
This uses the Deplixo email SDK on the client to initiate the send action.
);
}
ReactDOM.createRoot(document.getElementById('root')).render();