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>
195 lines
6.6 KiB
TypeScript
195 lines
6.6 KiB
TypeScript
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>
|
||
);
|
||
}
|