Aller au contenu
s3nd.sh

@s3nd/react

Des hooks qui ne voient jamais un identifiant.

Envoyez un fichier ou un snapshot, relisez un code, et un champ qui répare le code au fil de la frappe. Tout son arbre de dépendances, c’est le package protocol et nanoid, avec React en peer. Le SDK AWS reste sur votre serveur.

pointez-le sur les routes de transfert
import { S3ndProvider } from '@s3nd/react'

export default function Providers({ children }) {
  return <S3ndProvider baseUrl="/api/transfers">{children}</S3ndProvider>
}

01Envoyer

Un champ de fichier, un code.

sendFile() prend un File directement depuis un input, en gardant son nom et son type. Les échecs atterrissent dans error au lieu de rejeter, parce qu’un gestionnaire d’événement ne devrait pas avoir besoin d’un try/catch.

import { useSendTransfer } from '@s3nd/react'

function DropFile() {
  const { sendFile, transfer, isPending, error } = useSendTransfer()

  return (
    <>
      <input
        type="file"
        disabled={isPending}
        onChange={(event) => event.target.files?.[0] && sendFile(event.target.files[0])}
      />
      {transfer && <p>Read this out to them: {transfer.code}</p>}
      {error && <p>{error.message}</p>}
    </>
  )
}

Le hook poste vers les routes de transfert de votre serveur, qui détient les identifiants du bucket. Le navigateur ne voit jamais une clé, et votre fonction authorize décide qui peut envoyer.

transfer porte le code, le type, la taille et l’expiration. Affichez le code groupé par quatre ; le côté récepteur l’accepte avec ou sans les espaces.

Une drop box pour votre équipe

02Recevoir

Chercher, montrer, puis télécharger.

load() récupère ce qu’un code contient sans déplacer les octets, donc l’utilisateur voit un nom de fichier et une taille avant tout téléchargement. loadBytes() rapatrie le fichier.

import { useReceiveTransfer, useSyncCodeInput } from '@s3nd/react'

function PickUp() {
  const input = useSyncCodeInput()
  const { load, loadBytes, transfer, notFound, isPending } = useReceiveTransfer()

  async function download() {
    const bytes = await loadBytes(input.code!)
    if (bytes) saveToDisk(new Blob([bytes]), transfer?.filename ?? 'file') // your helper
  }

  return (
    <>
      <input {...input.inputProps} placeholder="K7QP 2M4X" />
      <button onClick={() => load(input.code!)} disabled={!input.isComplete || isPending}>
        Look it up
      </button>
      {notFound && <p>Unknown or expired code.</p>}
      {transfer?.kind === 'file' && (
        <button onClick={download}>
          Download {transfer.filename} · {transfer.size} bytes
        </button>
      )}
    </>
  )
}

03Le champ de code

Ce que l’utilisateur a tapé reste intact.

useSyncCodeInput fait la réparation dans le navigateur, avant toute requête. Réécrire le champ sous le curseur est la chose qui rend ces champs pénibles, donc il ne le fait jamais.

Ce que codes.normalize() cherche

Complet. Séparateurs retirés, casse repliée, et O, I et L lus comme 0, 1 et 1, parce que la base32 Crockford n’a ni O, ni I, ni L avec lesquels les confondre.

Alphabet 0123456789ABCDEFGHJKMNPQRSTVWXYZ · 8 caractères · 40 bits

value est mot pour mot. code est la forme canonique à soumettre, null tant que ce qui est tapé ne peut pas en être une. isComplete est le moment d’activer le bouton.

inputProps porte les indications de clavier et de remplissage automatique qu’un code à usage unique veut : autoComplete="one-time-code", majuscules, pas d’autocorrection, et un clavier numérique quand l’alphabet est fait de chiffres.

Passez la même forme que celle configurée sur votre serveur, { length: 4, alphabet }, et les deux moitiés suivent.

04État d’application

Les mêmes hooks transportent un snapshot.

L’état structuré passe par send() comme un snapshot, et revient en ligne dans data. Charger et appliquer sont délibérément séparés : seul votre code connaît ses object stores, et l’utilisateur doit voir ce qui va remplacer ses données.

const { send, transfer } = useSendTransfer()

// Structured state goes as a snapshot, with your schema version.
await send(await exportDatabase(), { version: 3 })

// On the other device: load, show, then apply.
const { load, transfer, data } = useReceiveTransfer<DatabaseDump>()
await load(code)
// transfer.device, transfer.createdAt → show them
// importDatabase(data!) → only after the user confirms

L’exemple IndexedDB du dépôt contient une paire export/import complète contre un vrai object store, et la page de cas d’usage déroule tout le flux.

05Garanties

Un utilisateur qui martèle un bouton obtient une seule réponse.

Chaque appel annule le précédent, une réponse tardive d’un appel remplacé est ignorée plutôt que publiée, et rien n’est écrit après le démontage.

Hooks client, prêts pour l’App Router

Chaque export est un hook client et le build porte 'use client', donc ça s’insère directement dans l’App Router de Next.js. React 18 ou plus.

Jetons et clients personnalisés

Passez headers au provider pour un jeton, ou client pour apporter le vôtre, ce qui est aussi la façon de le piloter en test sans aucun réseau.

Un statut que vous pouvez afficher

status vaut idle, pending, success ou error, et notFound couvre à la fois un code inconnu et un code expiré, comme le protocole.

useSendTransfer()
send, sendFile, transfer, status, isPending, error, reset
useReceiveTransfer()
load, loadBytes, burn, transfer, data, notFound, status, isPending, error, reset
useSyncCodeInput()
value, setValue, code, isComplete, error, reset, inputProps
useTransferClient()
le client sous-jacent, pour tout ce que les hooks ne couvrent pas

Déposez un fichier. Donnez le code.

Pointez-le sur le bucket que vous payez déjà. Rien à déployer, aucune inscription, personne au milieu.