Debuggler LogoDebuggler
All challenges

The Directory That Rewinds

API / Sync

Team directory search hits a live API. When you type quickly, results sometimes rewind to an older query. The list should always match the name in the box.

After you fix it

  • Slow typing should show contacts whose names match the current search.
  • A slower earlier request must not replace results after you type a longer query.
  • Try “le”, then quickly “lean” — you should end on Leanne Graham, not a wider stale list.
import { useState, useEffect } from "react";

const DIRECTORY_URL = "https://jsonplaceholder.typicode.com/users";

async function fetchDirectory(term) {
  const q = term.trim().toLowerCase();
  const res = await fetch(DIRECTORY_URL);
  if (!res.ok) throw new Error("Directory unavailable");
  const users = await res.json();
  const ms = q.length <= 2 ? 650 : q.length === 3 ? 300 : 80;
  await new Promise((r) => setTimeout(r, ms));
  return users
    .filter((u) => u.name.toLowerCase().includes(q))
    .slice(0, 6)
    .map((u) => ({ id: String(u.id), name: u.name }));
}

export default function App() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");

  useEffect(() => {
    const b = document.body;
    const pb = b.style.background;
    const pm = b.style.margin;
    b.style.background = "#fafafa";
    b.style.margin = "0";
    return () => {
      b.style.background = pb;
      b.style.margin = pm;
    };
  }, []);

  // Loads team contacts from the public directory service.
  // Rows should match the current search text.
  useEffect(() => {
    const q = query.trim();
    if (!q) {
      setResults([]);
      setLoading(false);
      setError("");
      return;
    }
    setLoading(true);
    setError("");
    const debounce = setTimeout(() => {
      fetchDirectory(q)
        .then((rows) => {
          setResults(rows);
          setLoading(false);
        })
        .catch(() => {
          setError("Could not reach directory.");
          setLoading(false);
        });
    }, 300);
    return () => clearTimeout(debounce);
  }, [query]);

  return (
    <div style={styles.page}>
      <div style={styles.card}>
        <h1 style={styles.h1}>Team directory</h1>
        <p style={styles.sub}>Live lookup against the org contact API.</p>
        <input
          data-testid="search-input"
          type="search"
          placeholder="Name"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          style={styles.input}
        />
        <p data-testid="search-status" style={styles.status}>
          {error || (loading ? "Searching…" : results.length ? results.length + " contact(s)" : query ? "No contacts" : "")}
        </p>
        <ul style={styles.list}>
          {results.map((c) => (
            <li key={c.id} data-testid="result-row" style={styles.row}>{c.name}</li>
          ))}
        </ul>
      </div>
    </div>
  );
}

const styles = {
  page: { display: "flex", alignItems: "center", justifyContent: "center", padding: "48px 24px", fontFamily: "'Segoe UI', system-ui, sans-serif" },
  card: { background: "#fff", borderRadius: 16, padding: "28px 32px", boxShadow: "0 1px 3px rgba(0,0,0,0.08), 0 8px 24px rgba(0,0,0,0.04)", width: "100%", maxWidth: 400 },
  h1: { margin: "0 0 6px", fontSize: 20, fontWeight: 700, color: "#1a1a2e" },
  sub: { margin: "0 0 16px", fontSize: 13, color: "#666" },
  input: { width: "100%", boxSizing: "border-box", padding: "10px 12px", fontSize: 15, borderRadius: 8, border: "1px solid #ddd", marginBottom: 10 },
  status: { margin: "0 0 10px", fontSize: 12, color: "#666", minHeight: 16 },
  list: { margin: 0, padding: 0, listStyle: "none" },
  row: { padding: "8px 0", fontSize: 14, color: "#1a1a2e", borderBottom: "1px solid #eee" },
};