anydrop/web/src/pages/JoinRoom.tsx
ordinarthur c18d995c3f feat(web): Paper & Envelope design system
Replace generic slate/indigo dark theme with a custom editorial
direction: warm paper neutrals, oxblood signal, Fraunces serif
display, Inter body, JetBrains Mono for codes. SVG paper-texture
noise overlay and thin rules across the app.

Refactored: Home, Settings, JoinRoom, Pair, Share, plus every
modal and panel (DropZone, DevicePairingPanel, PublicRoomPanel,
ProfileSetup, TextShareModal, ReceiveDialog, TransferProgress,
PeerList, PeerAvatar).

Also drops three pre-existing Uint8Array/BlobPart strictness
errors so the production build is green again.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 10:49:15 +02:00

195 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useParams } from "react-router-dom";
import { useCallback, useState } from "react";
import { useSignaling } from "../hooks/useSignaling";
import { useStore } from "../stores/useStore";
import { useProfileStore } from "../stores/useProfileStore";
import PeerList from "../components/PeerList";
import DropZone from "../components/DropZone";
import TransferProgress from "../components/TransferProgress";
import TextShareModal from "../components/TextShareModal";
import ReceiveDialog from "../components/ReceiveDialog";
import ProfileSetup from "../components/ProfileSetup";
export default function JoinRoom() {
const { code } = useParams<{ code: string }>();
const isSetUp = useProfileStore((s) => s.isSetUp);
if (!isSetUp) {
return <ProfileSetup onDone={() => {}} />;
}
return <JoinRoomConnected code={code} />;
}
function JoinRoomConnected({ code }: { code?: string }) {
const { sendFiles, sendText, acceptTransfer, rejectTransfer } = useSignaling(code);
const { deviceName, avatar } = useProfileStore();
const [showProfileEdit, setShowProfileEdit] = useState(false);
const selectedPeerId = useStore((s) => s.selectedPeerId);
const showTextModal = useStore((s) => s.showTextModal);
const incomingRequest = useStore((s) => s.incomingRequest);
const error = useStore((s) => s.error);
const setSelectedPeerId = useStore((s) => s.setSelectedPeerId);
const setShowTextModal = useStore((s) => s.setShowTextModal);
const setError = useStore((s) => s.setError);
const handlePeerSelect = useCallback(
(peerId: string) => {
setSelectedPeerId(selectedPeerId === peerId ? null : peerId);
},
[selectedPeerId, setSelectedPeerId],
);
const handleFilesSelected = useCallback(
(files: File[]) => {
if (!selectedPeerId) return;
sendFiles(selectedPeerId, files);
},
[selectedPeerId, sendFiles],
);
const handleSendText = useCallback(
(text: string) => {
if (!selectedPeerId) return;
sendText(selectedPeerId, text);
},
[selectedPeerId, sendText],
);
const handleAcceptTransfer = useCallback(() => {
if (incomingRequest) {
if (incomingRequest.text && incomingRequest.files.length === 0) {
navigator.clipboard.writeText(incomingRequest.text);
useStore.getState().setIncomingRequest(null);
} else {
acceptTransfer(incomingRequest.peerId);
}
}
}, [incomingRequest, acceptTransfer]);
const handleRejectTransfer = useCallback(() => {
if (incomingRequest) {
rejectTransfer(incomingRequest.peerId);
}
}, [incomingRequest, rejectTransfer]);
return (
<div className="min-h-screen">
<div className="max-w-xl mx-auto px-5 sm:px-8 pt-10 pb-24">
<header className="flex items-start justify-between pb-8 mb-10 rule">
<div>
<div className="text-xs uppercase tracking-[0.2em] text-ink-muted">
Public room
</div>
<h1 className="font-display text-3xl leading-none tracking-tight text-ink mt-2">
AnyDrop
</h1>
<p className="font-mono text-xs uppercase tracking-[0.25em] text-signal mt-3">
{code?.toUpperCase()}
</p>
</div>
<button
onClick={() => setShowProfileEdit(true)}
className="group flex items-center gap-2.5 px-3 py-2 border border-paper-edge
hover:border-ink transition-colors duration-fast ease-crisp
bg-paper rounded-sm"
>
{avatar ? (
<img
src={avatar}
alt=""
className="w-5 h-5 rounded-full object-cover border border-paper-edge"
/>
) : (
<span className="w-5 h-5 rounded-full bg-paper-deep border border-paper-edge" />
)}
<span className="text-sm text-ink truncate max-w-[120px]">{deviceName}</span>
</button>
</header>
{error && (
<div className="mb-8 px-4 py-3 border border-fail/40 bg-signal-quiet flex items-center justify-between rounded-sm">
<span className="text-sm text-ink">{error}</span>
<button
onClick={() => setError(null)}
className="ml-3 text-ink-muted hover:text-ink text-lg leading-none"
aria-label="Dismiss"
>
×
</button>
</div>
)}
<section className="mb-12">
<div className="text-xs uppercase tracking-[0.22em] text-ink-muted">
Room peers
</div>
<h2 className="font-display text-2xl text-ink mt-2 mb-5 tracking-tight">
Who's here
</h2>
<PeerList onPeerSelect={handlePeerSelect} />
</section>
{selectedPeerId && (
<section className="mb-12">
<div className="text-xs uppercase tracking-[0.22em] text-ink-muted">
Compose
</div>
<h2 className="font-display text-2xl text-ink mt-2 mb-5 tracking-tight">
Drop files or send text
</h2>
<div className="space-y-3">
<DropZone onFilesSelected={handleFilesSelected} />
<button
onClick={() => setShowTextModal(true)}
className="w-full text-left px-4 py-3 border border-paper-edge bg-paper
hover:bg-paper-deep transition-colors duration-fast ease-crisp
rounded-sm text-sm text-ink"
>
Send text instead
</button>
</div>
</section>
)}
<section className="mb-12">
<div className="text-xs uppercase tracking-[0.22em] text-ink-muted">
Activity
</div>
<TransferProgress />
</section>
<footer className="pt-8 mt-14 rule flex items-center justify-between text-xs text-ink-muted">
<span>End-to-end encrypted · Nothing transits the server</span>
<a
href="/"
className="text-ink hover:text-signal transition-colors duration-fast"
>
Home
</a>
</footer>
</div>
{showProfileEdit && (
<ProfileSetup isEditing onDone={() => setShowProfileEdit(false)} />
)}
{showTextModal && selectedPeerId && (
<TextShareModal
onSend={handleSendText}
onClose={() => setShowTextModal(false)}
/>
)}
{incomingRequest && (
<ReceiveDialog
request={incomingRequest}
onAccept={handleAcceptTransfer}
onReject={handleRejectTransfer}
/>
)}
</div>
);
}