Skip to content

TUTORIAL

How to use the site.

ESEPI is a wholesaler. You stage lines, you request a release, and you keep the reference. Nothing here is a store.

  1. 01 Start on the homepage
  2. 02 Read the line card
  3. 03 Stage an allocation
  4. 04 Send the request
  5. 05 Look up a reference
  6. 06 Read the floor, the company, and the officers
  7. 07 Use it on a phone, in either theme
  8. 08 Create a family
  9. 09 Create a line card
  10. 10 Write an office
  11. 11 Reference

01

Start on the homepage

The first screen is the company hallway, playing silently behind the headline. It loops. If this device asks for less motion, you get a still of that corridor instead.Scroll to the films. Company Promo is the walk. Electronics Wholesale is the mark coming together. Press play on the bar under the picture. Sound starts then, not before. The bar scrubs. Full screen is the control at the end. Choosing the other film shows its last frame until you press play.The rest of the homepage is the company in order: what ESEPI is, the line families, the crates that leave, the four bays, the officers, and a last link to the allocation desk.The header is the same on every page. Line card, Warehouses, Leadership, and Company are the text links. On a wide screen, Desk jumps to the request form. The clipboard button opens the lines you have staged. The sun and moon button switches light and dark.

02

Read the line card

Open the line card from the header, or Lines on a phone. The card is families, then the lines under each family. It is what the desk will discuss. It is not a price list and it is not a store.Search looks through the family name, the line name, the description, and the pack. A search that matches nothing says so. Clear a filter, or search the pack, if a part number is not on the card.Three rows of filters sit under the search. Family limits you to one group, such as Power or Networking. Status is All, On hand, Inbound, Allocated, or Hold. Bay is All bays, North, Central, West, or Bonded Hold. Filters combine. The count above the list is how many lines are still showing.Each line shows its pack, its minimum, its lead, and the bay it lives in. The status word is a fact. On hand can be discussed now. Inbound is coming. Allocated is already spoken for. Hold is not offered: the button reads Not offered and will not stage that line.

03

Stage an allocation

Add puts that line on a request kept in this browser. Adding the same line again raises the quantity by one. Nothing is purchased. Nothing is transmitted until you send the desk form.Open the request with the clipboard button at the top right. A number on that button is how many units are staged. The panel is titled Allocation. Each line shows the family, the name, and the pack. Minus and plus change the quantity, from 1 to 999. Remove drops that line. Clear lines drops all of them.Continue to the desk closes the panel and opens the request form, with those lines attached. You can also open Desk from the header, or from the phone bar. If you leave and come back on the same browser, the staged lines are still there until you clear them.

04

Send the request

The desk page is headed Request the release. The address on the left is the company office: ESEPI LLC, 412 N Main St, Ste 100, Buffalo, Wyoming 82834. The note beside it asks for the program, the pack, the quantity against the minimum, and the week you need it in the crate.If lines are staged, they are listed above the form, with quantities. If none are staged, you can still write. The request is stored with the lines marked as none staged.Company, your name, a work email, and the note are required. What you build is optional. A missing field is marked under that box. Send to the desk writes the request. If the desk does not take it, the form stays and asks you to try again.When it is received, the form is replaced by the reference, shaped like ESEPI-4821. Keep that number. It is the handle for the conversation. It is shown once on this screen. It is not emailed, and there is no public page where a visitor can look it up.

05

Look up a reference

The private list is Desk list in the footer, not the Desk button in the header. The header Desk is the public request form. The list asks for the desk key before it shows anything.A wrong key says it does not open the list. Too many wrong tries in a short time asks you to wait. A correct key stays open on this browser for twelve hours, or until you press Lock.With the box empty, Find shows the newest requests, up to forty. Type a reference, part of a company name, a contact name, or an email. The match ignores capital letters. The note and the staged lines are shown on the card, but they are not what the search looks through.Each card is one request: the reference, the time it was received, the company, the name, the email, what they build, the lines, and the note. An older reference still comes back when you type it, even if it has fallen off the open list.

06

Read the floor, the company, and the officers

Warehouses is the floor. North is high-velocity reels. Central is bulk and crated OEM. West is displays, imaging, and harnesses. Bonded Hold is unreleased stock. Nothing in Hold is offered, and it is not mixed with the other bays.Choose a bay to read what it holds. On a phone, that choice scrolls the detail up. The inbound board on that page is the same idea: pick a row and the matching bay comes into view.Company is the standard: identity before movement, and the mark on a crate as the company’s word. Leadership is the people who own that word. Joseph James is chief executive. R. Okonkwo runs the floor. A. Shah holds the line card. Five senior officers cover quality, warehousing, programs, finance, and export. Each bio is the job, not a brochure.

07

Use it on a phone, in either theme

The sun and moon control in the header switches light and dark. This browser remembers the choice the next time the site opens.On a phone the header links collapse. The bar at the bottom is Home, Lines, Bays, Company, People, Desk, and Help. Help opens this page. Desk there is the request form. The clipboard for staged lines stays in the header. Desk list, for looking up a reference, stays in the footer.The films, the line card, the request form, and the private list are the same job on a narrow screen. Nothing in this guide is a second, hidden product.

08

Create a family.

A family is a heading on the line card, not a product and not a model. Buyers filter by it. The homepage gives it a tile. The lines under it are the only rows anyone can stage. Families are the objects in the families list in src/lib/content.ts. There is no button that creates one.

Read the card before you add a heading. Android handsets is already there. Semiconductors, passives, memory, boards, power, networking, displays, and interconnect are already there. A new series, a region, or a pack belongs under the heading that already names that kind of goods. A new family is only for a kind the card does not name.

1. Name it once

Five fields. All of them are required. The family is inserted in the families list, after the previous family’s closing brace, with a comma between them.

id
A short word, lowercase, no spaces. handsets is one. It has to be unique among families. The homepage tile opens the card with this id, so a typo here opens an empty filter.
name
The heading people read. Android handsets. Search looks at this, and so does the chip above the lines. Do not put a region or a model in the heading.
blurb
One sentence under the heading: what the family includes, and the limit. For handsets, that limit is Samsung, US and international as different lines, not every model.
image
The homepage tile. Use a picture that is already on the site: /media/crates.jpg, /media/dock.jpg, /media/reels.jpg, /media/panels.jpg, /media/power.jpg, /media/harness.jpg, /media/network-gear.jpg, or /media/campus.jpg. A path you invent is a broken tile.
lines
The rows under the heading. Do not leave this empty. A family with no lines still makes a homepage tile, and that tile opens a card that says nothing matches.

2. Write the heading with its first line

This is the shape. The handset family is already on the card, so do not paste it a second time. Copy the shape only when the card has no heading for these goods. Change every word. Put at least one real line in lines before you stop. The next chapter is how that line is written.

{
  id: "handsets",
  name: "Android handsets",
  blurb: "Samsung, sealed in the manufacturer carton. United States and international are different lines. Not every model.",
  image: "/media/crates.jpg",
  lines: [
    {
      id: "samsung-s-us",
      name: "Galaxy S series, United States",
      detail: "Flagship Samsung handsets built for the US. Factory sealed.",
      pack: "Master carton",
      moq: "10 units",
      lead: "On hand",
      bay: "central",
      status: "On hand",
    },
  ],
},

3. What not to split

Do not make a family per region. United States and international are lines. Do not make a family per status. On hand, inbound, allocated, and hold are a field on the line. Do not make a family per bay. North, central, west, and bonded are a field on the line.

Do not make a family because a name would look better on its own tile. A tile is a heading for a group of lines. One model, one color, or one memory size is not a group.

4. Check the heading

On the homepage, the new name should be a tile with the picture you named. Choosing it opens the line card with only that family. On the card, the same name is a chip and a heading, with the blurb under the heading and the lines beside it.

The scrolling words under the homepage hero do not come from families. They are the disciplines list in the same file. Add a word there only if the new heading should scroll with the others. The card does not read that list.

The homepage count is the number of lines, not the number of families. A new heading does not move the count until it has a line. Search finds the heading by its name and its blurb is not part of that search. If a buyer should find the family by a word, put that word in the name or in a line.

If two families would answer the same search, you have split something that should be one heading. Put the line in the family that already exists and delete the extra heading.

09

Create a line card.

The line card is not a form. It is the families list in src/lib/content.ts. A family is the heading a buyer sees. A line is one row under that heading, and the only thing Add can stage. The homepage tiles, the filter chips, the count, and the search all read this list. There is no second copy.

Build the smallest card that is true. One family. Then the lines a buyer is allowed to request. Samsung handsets are already a family. A new series goes in that family. A new family is only for a kind of goods the card does not name yet.

1. Decide the family

Open the line card and read the headings. If the goods belong under one of them, skip to the lines. Do not open a second family for a region, a color, or a single model. United States and international are two lines in the same family, not two cards.

A new family needs five fields. The full rules are in Create a family, just above. Do not open one for a region or a single model. United States and international are two lines in the same family.

2. Write one line

Put the line inside that family’s lines list. A comma separates it from the line before it. Eight fields. Leave one out and the card will not build.

id
Unique across the whole card, not just the family. samsung-s-us is one. Add remembers the line by this id. Reusing an id merges two different goods into one staged row.
name
The words on the row. Put the region in the name when the packs must not be mixed: Galaxy S series, United States.
detail
What is sealed, what the desk still has to name, and what will not be done. The exact model of a series is named on the allocation, not by listing every model here. No prices.
pack
How it leaves. Master carton, reel, tray, crate. Search looks at this word, so use the pack a buyer would type.
moq
The smallest release, written as people say it. 10 units. 1 reel. 1 crate. The card shows it. It does not enforce it.
lead
When it can move. On hand, or a range such as 2–4 weeks. On review if the line is not ready to be promised.
bay
Exactly one of north, central, west, bonded. Handset cartons sit in central. Large or fragile faces sit in west. Anything you cannot identify sits in bonded.
status
Exactly one of On hand, Inbound, Allocated, Hold. Capital letters matter. Hold is visible and cannot be staged. The button reads Not offered.

One finished line. Copy the shape. Change the words. Do not add a second line for every model inside the series.

{
  id: "samsung-s-us",
  name: "Galaxy S series, United States",
  detail: "Flagship Samsung handsets built for the US. Factory sealed. The exact model is named on the allocation.",
  pack: "Master carton",
  moq: "10 units",
  lead: "On hand",
  bay: "central",
  status: "On hand",
}

3. Add only the lines that differ

A second line is justified when a buyer could be sent the wrong goods. Region is one of those differences. A US carton is not the international line. Foldables are not the volume series. A color, a memory size, or a carrier name is not a new line. The desk writes that on the request.

Give each of those lines its own id. Set status to what is true today. On hand means it is in the bay. Inbound means it is expected, not that it is here. Allocated means a quantity is already spoken for. Hold, with bay bonded, means the row may be seen and may not be added.

The strip of words under the homepage hero is a separate list, disciplines, in the same file. The card does not read it. Add a word there only if the new family should scroll past with the others.

4. Check the card

Open the line card. The new heading should be a chip and, on the homepage, a tile. Choosing the chip leaves only that family. Search looks at the family name, the line name, the detail, and the pack. It does not look at the minimum, the lead, or the bay. If a search for the pack misses, the pack field is the place to fix.

Each row should show pack, minimum, lead, bay, and either Add or Not offered. Add stages one unit in this browser. Adding the same id again raises the quantity. It does not publish a second row, and it does not create a reference. The reference appears only after the desk form is sent.

The homepage count is the number of lines in every family. It moves when a line is added or removed. It does not move when someone stages a request.

If the region is still unknown, do not guess a status that lets a buyer add it. Bonded and Hold. A line you cannot stand behind does not belong on the card as On hand.

10

Write an office.

Every office has an address at esepillc.com. The role comes first: ceo@, coo@, corporate@, and the senior offices. Where a first name is published, that name in lowercase is a second address for the same person. R. Okonkwo and A. Shah are reached by the office address only.

A letter does not create a reference. The desk form does. If you need a number you can quote later, send the form and keep what it shows. Then you may write desk@esepillc.com and put that reference in the subject.

Use the address that matches the question. Writing all of them does not make it arrive faster.

The same list sits on the allocation desk, under Write, and on each biography.

11

Reference.

These are the words the site uses. A status is not a suggestion, and a bay is not a nickname. If a control is in the header, the footer, or the phone bar, it goes to a page that exists.

Status

On hand
The goods are in a bay and can be discussed. Add will stage the line.
Inbound
The goods are expected. They are not a promise that they are here. Add will still stage the request.
Allocated
A quantity is already spoken for. Add stages a request. It does not take the quantity from someone else.
Hold
Not offered. The button reads Not offered. The usual bay is Bonded Hold.

Bays

NorthBAY N
High velocity. Reels, cut tape, trays, moisture-barrier bags. The short aisle. Small packs move here, with the same lot identity the bulk floor uses.
CentralBAY C
Bulk and crated OEM. Master cartons, open-frame power, memory trays. Where the heavy stock lives. If it needs a pallet, it is staged in Central.
WestBAY W
Displays, imaging, harnesses. Panel crates, imaging kits, kitted cable. Large faces and finished kits. Crates are built here so the mark sits square on the side.
Bonded HoldHOLD
Unreleased. Unmatched paperwork, open date-code questions. Nothing in Hold is offered. It stays unmixed until it is cleared or it goes back.

Where to go

Line card
Header, footer, and Lines on a phone. Families and the lines under them.
Warehouses
Header, footer, and Bays on a phone. The four bays and inbound.
Leadership
Header, People on a phone, and the footer. The eight officers and their addresses.
Privacy
Header, and the footer. What the site stores and what it does not.
Terms
Header, and the footer. The agreement for using the site.
Company
Header, and Standards in the footer. The release standard.
Desk
Header on a wide screen, Desk on a phone, and Request in the footer. The allocation form.
Desk list
Footer only. The private lookup. It is not the Desk button.
Tutorial
Header on a wide screen, Help on a phone, and the footer. This page.
Clipboard
Header. Opens the staged lines. It is not a page.

The code that signs a desk reference, looks one up, and draws the list is still below, under the file it lives in.

REFERENCE

Where the desk code lives.

A request is stored under a reference such as ESEPI-4821. The list that reads those references is three files. Each name below is the place that code resides. The live desk key is not printed here.

  1. 01 src/lib/desk.server.ts
  2. 02 src/lib/desk.ts
  3. 03 src/routes/desk.tsx

REFERENCE NAME

src/lib/desk.server.ts

Signs the desk cookie and checks it. The live key is not printed on this page.

import { createHmac, timingSafeEqual } from "node:crypto";
import { getRequest, setCookie } from "@tanstack/react-start/server";

const COOKIE = "esepi_desk";
const KEY = "not printed here";
const DAY = 60 * 60 * 12;

const attempts = new Map<string, { n: number; at: number }>();

function secret() {
  return process.env.DESK_KEY?.trim() || KEY;
}

function safeEqual(a: string, b: string) {
  const left = Buffer.from(a);
  const right = Buffer.from(b);
  if (left.length !== right.length) return false;
  return timingSafeEqual(left, right);
}

function token() {
  const exp = Date.now() + DAY * 1000;
  const payload = String(exp);
  const mac = createHmac("sha256", secret()).update(payload).digest("hex");
  return `${payload}.${mac}`;
}

export function deskOpen() {
  const request = getRequest();
  const raw = request?.headers.get("cookie") ?? "";
  const found = raw
    .split(";")
    .map((part) => part.trim())
    .find((part) => part.startsWith(`${COOKIE}=`));
  if (!found) return false;
  const value = decodeURIComponent(found.slice(COOKIE.length + 1));
  const [payload, mac] = value.split(".");
  if (!payload || !mac) return false;
  const expected = createHmac("sha256", secret()).update(payload).digest("hex");
  if (!safeEqual(mac, expected)) return false;
  return Number(payload) > Date.now();
}

function caller() {
  const request = getRequest();
  return request?.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "local";
}

export function tooManyAttempts() {
  const now = Date.now();
  const slot = attempts.get(caller());
  if (!slot || now - slot.at > 10 * 60 * 1000) return false;
  return slot.n >= 8;
}

export function noteAttempt(ok: boolean) {
  const id = caller();
  const now = Date.now();
  const slot = attempts.get(id);
  if (!ok) {
    if (!slot || now - slot.at > 10 * 60 * 1000) attempts.set(id, { n: 1, at: now });
    else slot.n += 1;
    return;
  }
  attempts.delete(id);
}

function cookieFlags() {
  const request = getRequest();
  const proto = request?.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
  const secure = proto === "https" || request?.url.startsWith("https:") === true;
  return { path: "/", httpOnly: true, sameSite: "lax" as const, secure, maxAge: DAY };
}

export function openDesk(key: string) {
  if (!safeEqual(key, secret())) return false;
  setCookie(COOKIE, token(), cookieFlags());
  return true;
}

export function closeDesk() {
  setCookie(COOKIE, "", { ...cookieFlags(), maxAge: 0 });
}

REFERENCE NAME

src/lib/desk.ts

The calls the page is allowed to make: session, unlock, lock, and the lookup.

import { createServerFn } from "@tanstack/react-start";

export type DeskRow = {
  ref: string;
  company: string;
  name: string;
  email: string;
  build: string;
  note: string;
  lines: string;
  at: string;
};

function stamp(value: unknown) {
  if (value instanceof Date) return value.toISOString();
  return String(value ?? "");
}

export const deskSession = createServerFn({ method: "POST" }).handler(async () => {
  const { deskOpen } = await import("./desk.server");
  return { open: deskOpen() };
});

export const unlockDesk = createServerFn({ method: "POST" })
  .inputValidator((input: unknown) => {
    const key = String((input as { key?: unknown } | null)?.key ?? "").slice(0, 200);
    return { key };
  })
  .handler(async ({ data }) => {
    const { openDesk, tooManyAttempts, noteAttempt } = await import("./desk.server");
    if (tooManyAttempts()) return { ok: false as const, limited: true as const };
    const ok = openDesk(data.key);
    noteAttempt(ok);
    return { ok, limited: false as const };
  });

export const lockDesk = createServerFn({ method: "POST" }).handler(async () => {
  const { closeDesk } = await import("./desk.server");
  closeDesk();
  return { ok: true as const };
});

export const listDesk = createServerFn({ method: "POST" })
  .inputValidator((input: unknown) => {
    const query = String((input as { query?: unknown } | null)?.query ?? "")
      .trim()
      .slice(0, 80);
    return { query };
  })
  .handler(async ({ data }) => {
    const { deskOpen } = await import("./desk.server");
    if (!deskOpen()) return { open: false as const, rows: [] as DeskRow[] };
    const { getSql } = await import("@/lib/db");
    const sql = await getSql();
    const like = `%${data.query.replace(/[%_]/g, "")}%`;
    const rows = data.query
      ? await sql.query(
          `select ref, company, contact_name, email, build, note, lines, created_at
           from allocation_requests
           where ref ilike $1 or company ilike $1 or contact_name ilike $1 or email ilike $1
           order by created_at desc
           limit 40`,
          [like],
        )
      : await sql`
          select ref, company, contact_name, email, build, note, lines, created_at
          from allocation_requests
          order by created_at desc
          limit 40
        `;
    return {
      open: true as const,
      rows: rows.map((row) => ({
        ref: String(row.ref ?? ""),
        company: String(row.company ?? ""),
        name: String(row.contact_name ?? ""),
        email: String(row.email ?? ""),
        build: String(row.build ?? ""),
        note: String(row.note ?? ""),
        lines: String(row.lines ?? ""),
        at: stamp(row.created_at),
      })),
    };
  });

REFERENCE NAME

src/routes/desk.tsx

The desk list page. This is the only gate. There is no separate login page.

import { createFileRoute } from "@tanstack/react-router";
import { useEffect, useState, type FormEvent } from "react";
import { listDesk, lockDesk, unlockDesk, deskSession, type DeskRow } from "@/lib/desk";

export const Route = createFileRoute("/desk")({
  head: () => ({
    meta: [{ title: "Desk list — ESEPI LLC" }],
  }),
  component: DeskPage,
});

function when(value: string) {
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return value;
  return date.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });
}

function DeskPage() {
  const [checking, setChecking] = useState(true);
  const [open, setOpen] = useState(false);
  const [key, setKey] = useState("");
  const [error, setError] = useState("");
  const [query, setQuery] = useState("");
  const [rows, setRows] = useState<DeskRow[]>([]);
  const [loading, setLoading] = useState(false);

  async function load(next: string) {
    setLoading(true);
    try {
      const result = await listDesk({ data: { query: next } });
      if (!result.open) {
        setOpen(false);
        setRows([]);
        return;
      }
      setOpen(true);
      setRows(result.rows);
    } finally {
      setLoading(false);
    }
  }

  useEffect(() => {
    let live = true;
    deskSession()
      .then(async (session) => {
        if (!live) return;
        if (!session.open) return;
        await load("");
      })
      .finally(() => {
        if (live) setChecking(false);
      });
    return () => {
      live = false;
    };
  }, []);

  async function onUnlock(event: FormEvent) {
    event.preventDefault();
    setError("");
    const result = await unlockDesk({ data: { key } });
    if (!result.ok) {
      setError(
        result.limited
          ? "Too many tries. Wait and use the key again."
          : "That key does not open the list.",
      );
      return;
    }
    setKey("");
    setOpen(true);
    await load("");
  }

  async function onFind(event: FormEvent) {
    event.preventDefault();
    await load(query);
  }

  async function onLock() {
    await lockDesk();
    setOpen(false);
    setRows([]);
    setQuery("");
  }

  return (
    <div className="mx-auto max-w-3xl px-4 py-16 sm:px-6 lg:py-24">
      <p className="text-xs tracking-widest text-accent">DESK LIST</p>
      <h1 className="mt-3 text-4xl sm:text-5xl">Check a reference.</h1>
      {checking ? <p className="mt-10 text-sm text-muted">Checking the desk.</p> : null}
      {!checking && !open ? (
        <form onSubmit={onUnlock} className="mt-10 grid max-w-md gap-4">
          <label className="block">
            <span className="text-xs tracking-widest text-subtle">DESK KEY</span>
            <input type="password" value={key} onChange={(event) => setKey(event.target.value)} className="field mt-2" />
          </label>
          {error ? <p className="text-sm text-warn">{error}</p> : null}
          <button type="submit" className="btn btn-accent">Open the list</button>
        </form>
      ) : null}
      {!checking && open ? (
        <form onSubmit={onFind}>
          <input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="ESEPI-4821, a company, or an email" className="field" />
          <button type="submit" className="btn btn-accent">{loading ? "Looking" : "Find"}</button>
          <button type="button" onClick={onLock} className="btn btn-quiet">Lock</button>
        </form>
      ) : null}
    </div>
  );
}