Files
dumpsterChat/web/src/components/JoinServer.tsx
T
hobokenchicken bb650ac2a0 Phase 3: Polish & PWA
Backend:
- DB: reactions table, invites table, reply_to column on messages
- gateway/events.go: added REACTION_ADD, REACTION_REMOVE events
- internal/reaction/handlers.go: reaction CRUD with WebSocket broadcast
- internal/invite/handlers.go: invite creation, info, join with code
- gateway/hub.go: presence tracking with idle detection
- gateway/client.go: idle timeout support

Frontend - Social:
- TypingIndicator: real-time 'user is typing...' display
- ReactionBar: emoji reactions on messages with counts
- EmojiPicker: searchable emoji grid for reactions
- ReplyBar: quoted reply display above messages
- MentionPopup: @mention autocomplete with user list

Frontend - PWA:
- manifest.json: PWA manifest with theme color and icons
- sw.js: service worker with cache-first strategy and push support
- stores/push.ts: push notification subscription management
- InstallPrompt: 'Add to Home Screen' banner

Frontend - Mobile:
- MobileNav: bottom nav bar for mobile (servers/channels/chat/members)
- MobileDrawer: slide-out drawer with server bar + channel list
- index.html: PWA meta tags, safe area viewport

Frontend - Polish:
- ThemeToggle: dark/light mode switch with localStorage persistence
- InviteModal: generate invite links with expiry and max uses
- JoinServer: /invite/:code join flow
- stores/typing.ts: typing indicator state management
- stores/presence.ts: real-time presence tracking
- tailwind.config.js: darkMode: 'class', light mode color tokens
- styles/index.css: light mode CSS variable overrides
2026-06-28 16:44:39 -04:00

102 lines
3.2 KiB
TypeScript

import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { api } from '../lib/api.ts';
interface InviteInfo {
server_name: string;
inviter: string;
expires_at: string | null;
}
export function JoinServer() {
const { code } = useParams<{ code: string }>();
const navigate = useNavigate();
const [info, setInfo] = useState<InviteInfo | null>(null);
const [loading, setLoading] = useState(true);
const [joining, setJoining] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!code) return;
const fetchInfo = async () => {
try {
const data = await api.get<InviteInfo>(`/invites/${code}`);
setInfo(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Invalid invite');
} finally {
setLoading(false);
}
};
fetchInfo();
}, [code]);
const joinServer = async () => {
if (!code) return;
setJoining(true);
setError(null);
try {
const result = await api.post<{ server_id: string }>(`/invites/${code}/join`);
navigate(`/channels/${result.server_id}`);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to join');
} finally {
setJoining(false);
}
};
if (loading) {
return (
<div className="h-full flex items-center justify-center bg-gb-bg text-gb-fg font-mono">
<div className="text-gb-fg-f">Loading invite...</div>
</div>
);
}
if (error) {
return (
<div className="h-full flex items-center justify-center bg-gb-bg text-gb-fg font-mono">
<div className="bg-gb-bg-s border border-gb-red p-6 max-w-md text-center">
<div className="text-gb-red text-lg mb-2">INVALID INVITE</div>
<div className="text-gb-fg-f text-sm mb-4">{error}</div>
<button
onClick={() => navigate('/')}
className="px-4 py-2 bg-gb-bg-t text-gb-fg text-sm hover:bg-gb-bg-s transition-colors"
>
[GO HOME]
</button>
</div>
</div>
);
}
return (
<div className="h-full flex items-center justify-center bg-gb-bg text-gb-fg font-mono">
<div className="bg-gb-bg-s border border-gb-bg-t p-6 max-w-md">
<div className="text-center mb-4">
<div className="text-gb-orange text-lg mb-1">YOU'VE BEEN INVITED</div>
<div className="text-2xl text-gb-fg mb-2">{info?.server_name}</div>
{info?.inviter && (
<div className="text-sm text-gb-fg-f">by {info.inviter}</div>
)}
</div>
{info?.expires_at && (
<div className="text-xs text-gb-fg-f text-center mb-4">
Expires: {new Date(info.expires_at).toLocaleString()}
</div>
)}
{error && (
<div className="text-sm text-gb-red text-center mb-3">{error}</div>
)}
<button
onClick={joinServer}
disabled={joining}
className="w-full px-4 py-2 bg-gb-orange text-gb-bg text-sm font-mono hover:bg-gb-yellow transition-colors disabled:opacity-50"
>
{joining ? 'JOINING...' : '[ACCEPT INVITE]'}
</button>
</div>
</div>
);
}