Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/apursley2012/digital-domain/llms.txt

Use this file to discover all available pages before exploring further.

The Guestbook page recreates one of the most distinctively 90s features of personal homepages: the public sign-in board where visitors left their handle, a brief message, and their presence was permanently recorded for all to see. In Digital Domain this feature is fully interactive — new entries are submitted through a Win98-styled form, added to the top of the list in real time, and assigned a randomly selected emoji avatar. The page is routed at /testimonials in the React Router config.

Layout

The page uses a responsive three-column grid:
<div className="max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-6 items-start">
  <div className="md:col-span-2 space-y-4">  {/* Entries list */}
  <div className="md:col-span-1">            {/* Sign form */}
</div>
On mobile, both panels stack vertically. On md and above, the entries list occupies two-thirds of the width and the sign form occupies one third — a layout that matches classic guestbook pages of the era.

Guestbook Header

The entries column opens with a header bar:
<div className="bg-black p-2 border-4 border-win-gray shadow-win-out text-center">
  <h1 className="font-vt323 text-4xl text-retro-yellow">
    ~*~ MY GUESTBOOK ~*~
  </h1>
</div>
Visual appearance: A black panel with the full Win98 raised-border treatment (border-4 border-win-gray shadow-win-out). The title ”~ MY GUESTBOOK ~” renders in VT323 at size 4xl in bright retro yellow — the tildes and asterisks are part of the text string, not separate elements, replicating the decorative ASCII embellishments that surrounded headings on 90s personal pages.

Pre-Seeded Entries

Three starter entries are defined in the initial state value (Me in the compiled source):
const initialEntries = [
  {
    id: 1,
    name: "CoolHacker99",
    date: "11/02/2025",
    message: "First! Awesome site dude. Love the marquee.",
    avatar: "😎",
  },
  {
    id: 2,
    name: "WebMaster_Dan",
    date: "11/01/2025",
    message: "Your HTML is valid but your jokes are terrible. 10/10.",
    avatar: "🤓",
  },
  {
    id: 3,
    name: "Mom",
    date: "10/28/2025",
    message: "Very nice honey, but when are you coming over for dinner?",
    avatar: "👩",
  },
];
These are passed directly as the initial value to useState:
const [entries, setEntries] = useState(initialEntries);

Entry Card Rendering

Each guestbook entry is rendered as a horizontal flex card:
<div className="bg-white border-2 border-win-gray shadow-win-out p-3 flex gap-4">
  {/* Avatar column */}
  <div className="flex flex-col items-center gap-1 min-w-[80px]">
    <div className="w-12 h-12 bg-gray-200 border border-gray-400 flex items-center justify-center text-2xl shadow-win-in">
      {entry.avatar}
    </div>
    <span className="font-vt323 text-sm text-center break-all">{entry.name}</span>
  </div>

  {/* Message column */}
  <div className="flex-1 font-comic">
    <div className="text-xs text-gray-500 mb-2 border-b border-dashed border-gray-300 pb-1">
      Signed on: {entry.date}
    </div>
    <p className="text-sm">{entry.message}</p>
  </div>
</div>
Visual appearance: Each card is a white Win98 raised-border panel. The left column (80px minimum width) contains a 48×48px square avatar box with an inset-shadow border (shadow-win-in) displaying the emoji at text-2xl, and the handle below it in font-vt323 text-sm. The right column shows a dashed-border date header in grey text-xs, and the message body in font-comic text-sm. New entries prepended to the top of the list appear above the original three entries.
The entries list container uses max-h-[60vh] overflow-y-auto pr-2 — a maximum height of 60% of the viewport with vertical scroll and a small right padding to offset the scrollbar.

Sign Form

The right-column sign form is wrapped in a Window component:
<Window
  title="Sign_Guestbook.exe"
  icon={<UserPlus size={14} />}
  defaultSize={{ width: "100%", height: "auto" }}
  className="!static"
>
  <form onSubmit={handleSubmit} className="p-4 bg-white font-comic space-y-4">
Visual appearance: The form window title bar displays a UserPlus icon alongside “Sign_Guestbook.exe”. Inside, two fields and a submit button are stacked vertically with space-y-4. Both inputs use the standard Win98 inset-shadow field styling (border-2 border-win-gray shadow-win-in p-1 text-sm focus:outline-none focus:bg-yellow-50).

Handle/Name Input

<label className="block text-sm font-bold mb-1">Handle/Name:</label>
<input
  value={name}
  onChange={(e) => setName(e.target.value)}
  required
  maxLength={20}
  className="w-full border-2 border-win-gray shadow-win-in p-1 text-sm focus:outline-none focus:bg-yellow-50"
/>
The input is a controlled component bound to the name state variable. A maxLength={20} attribute enforces a username length limit.

Message Textarea

<label className="block text-sm font-bold mb-1">Message:</label>
<textarea
  value={message}
  onChange={(e) => setMessage(e.target.value)}
  required
  rows={4}
  className="w-full border-2 border-win-gray shadow-win-in p-1 text-sm focus:outline-none focus:bg-yellow-50"
/>
A 4-row controlled textarea bound to the message state variable.

Submit Button

<BeveledButton type="submit" className="w-full flex justify-center items-center gap-2">
  <BookOpen size={16} /> Sign It!
</BeveledButton>
Full-width BeveledButton using the Lucide BookOpen icon.

Form Submission Handler

The handleSubmit function validates, creates a new entry, and prepends it to the list:
const handleSubmit = (e) => {
  e.preventDefault();
  if (!name || !message) return; // guard against empty fields

  const newEntry = {
    id: Date.now(),           // unique ID from timestamp
    name,
    date: new Date().toLocaleDateString(),
    message,
    avatar: ["👽", "👾", "🤖", "👻", "🤠"][Math.floor(Math.random() * 5)],
  };

  setEntries([newEntry, ...entries]); // prepend to top
  setName("");                        // reset name input
  setMessage("");                     // reset message textarea
};
Form validation uses an early return guard for empty fields rather than relying solely on the required HTML attribute. If either name or message is falsy, the function exits without adding an entry or clearing the fields. The Date.now() call generates a unique numeric ID for the new entry’s React key prop.

Random Avatar Assignment

When a new entry is submitted, one of five emoji is selected at random:
const avatarPool = ["👽", "👾", "🤖", "👻", "🤠"];
const avatar = avatarPool[Math.floor(Math.random() * 5)];
The five options are: 👽 alien, 👾 space invader, 🤖 robot, 👻 ghost, 🤠 cowboy. All are in keeping with the geek / internet culture theme of the site.

State Summary

State VariableInitial ValueUpdated By
entriesinitialEntries (3 items)handleSubmit (prepend new entry)
name""Handle input onChange
message""Message textarea onChange

Component Dependencies

ComponentSourceUsage
Window (p)components/Window.jsSign_Guestbook.exe form frame
BeveledButton (d)components/BeveledButton.js”Sign It!” submit button
user-plus (Lucide)lucide-reactWindow title bar icon
book-open (Lucide)lucide-react”Sign It!” button icon
useStateReact 18Entries list + form field state

Build docs developers (and LLMs) love