8-bit Quest Log

PreviousNext

A retro-styled quest log component for tracking game missions and tasks with accordion functionality and status indicators.

Docs
8bitcnblock

Preview

Loading preview…
components/ui/8bit/quest-log.tsx
"use client";

import * as React from "react";

import { cn } from "@/lib/utils";

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/8bit/accordion";
import { Badge } from "@/components/ui/8bit/badge";
import {
  Card,
  CardContent,
  CardHeader,
  CardTitle,
} from "@/components/ui/8bit/card";
import { ScrollArea } from "@/components/ui/8bit/scroll-area";

import "./styles/retro.css";

export type QuestStatus = "active" | "completed" | "failed" | "pending";

export interface Quest {
  id: string;
  title: string;
  description: string;
  status: QuestStatus;
  shortDescription?: string;
}

export interface QuestLogProps {
  quests: Quest[];
  className?: string;
  maxHeight?: string;
  showEmptyState?: boolean;
  emptyStateMessage?: string;
}

const getStatusBadgeVariant = (status: QuestStatus) => {
  switch (status) {
    case "active":
      return "default";
    case "completed":
      return "secondary";
    case "failed":
      return "destructive";
    case "pending":
      return "outline";
    default:
      return "outline";
  }
};

function QuestItem({ quest }: { quest: Quest }) {
  const shortDescription =
    quest.shortDescription ||
    (quest.description.length > 100
      ? `${quest.description.substring(0, 100)}...`
      : quest.description);

  return (
    <AccordionItem
      className="border-b-2 border-foreground dark:border-ring"
      value={quest.id}
    >
      <AccordionTrigger className="hover:no-underline py-3 px-4">
        <div className="flex items-center gap-3 w-full">
          <div className="flex-1 text-left">
            <div className="flex flex-col sm:flex-row items-center justify-between sm:pr-10 gap-3">
              <div className="flex flex-col gap-1">
                <h3 className="text-sm font-medium text-center sm:text-left">
                  {quest.title}
                </h3>
                <p className="text-xs text-muted-foreground mt-1">
                  {shortDescription}
                </p>
              </div>

              <Badge
                variant={getStatusBadgeVariant(quest.status)}
                className="text-[9px]"
              >
                {quest.status.toUpperCase()}
              </Badge>
            </div>
          </div>
        </div>
      </AccordionTrigger>
      <AccordionContent className="px-4 pb-3">
        <div className="pt-2">
          <p className="text-xs text-muted-foreground leading-relaxed">
            {quest.description}
          </p>
        </div>
      </AccordionContent>
    </AccordionItem>
  );
}

function EmptyState({ message }: { message: string }) {
  return (
    <div className="flex flex-col items-center justify-center px-4 text-center">
      <p className="text-sm text-muted-foreground">{message}</p>
    </div>
  );
}

export function QuestLog({
  quests,
  className,
  showEmptyState = true,
  emptyStateMessage = "No quests available.",
}: QuestLogProps) {
  const activeQuests = quests.filter((quest) => quest.status === "active");
  const sortedQuests = [
    ...activeQuests,
    ...quests.filter((quest) => quest.status !== "active"),
  ];

  return (
    <Card className={cn("w-full", className)}>
      <CardHeader className="pb-3">
        <CardTitle className="flex items-center gap-2 text-base justify-between">
          Quest Log
          {activeQuests.length > 0 && (
            <Badge variant="default" className="ml-2">
              {activeQuests.length} Active
            </Badge>
          )}
        </CardTitle>
      </CardHeader>
      <CardContent className="p-0">
        {quests.length === 0 && showEmptyState ? (
          <EmptyState message={emptyStateMessage} />
        ) : (
          <ScrollArea className="w-full h-[400px]">
            <Accordion type="multiple" className="w-full">
              {sortedQuests.map((quest) => (
                <QuestItem key={quest.id} quest={quest} />
              ))}
            </Accordion>
          </ScrollArea>
        )}
      </CardContent>
    </Card>
  );
}

export default QuestLog;

Installation

npx shadcn@latest add @8bitcn/quest-log

Usage

import { QuestLog } from "@/components/quest-log"
<QuestLog />