devist 0.14.0

Project bootstrap CLI for AI-assisted development. Spin up new projects from templates, manage backends, and keep your codebase comprehensible.
import LanguageSwitch from "@/components/LanguageSwitch";
import { Button } from "@/components/ui/button";
import { useAuth } from "@/contexts/AuthContext";
import { useI18n } from "@/i18n/I18nProvider";
import {
  ArrowRight,
  Boxes,
  Eye,
  Github,
  LayoutDashboard,
  Terminal,
} from "lucide-react";
import { Link } from "react-router-dom";

const REPO_URL = "https://github.com/WebchemistCorp/devist";

export default function Landing() {
  const { session } = useAuth();

  return (
    <div className="min-h-screen bg-background text-foreground">
      <Header signedIn={!!session} />
      <main>
        <Hero signedIn={!!session} />
        <About />
        <Workflow />
      </main>
      <Footer />
    </div>
  );
}

function Header({ signedIn }: { signedIn: boolean }) {
  const { t } = useI18n();
  return (
    <header className="sticky top-0 z-10 border-b bg-background/80 backdrop-blur">
      <div className="mx-auto max-w-6xl px-6 h-14 flex items-center justify-between">
        <Link to="/" className="flex items-center gap-2 font-semibold">
          <span className="text-base">devist</span>
          <span className="text-xs text-muted-foreground hidden sm:inline">
            {t("landing.tagline")}
          </span>
        </Link>
        <nav className="flex items-center gap-2">
          <LanguageSwitch />
          <a
            href={REPO_URL}
            target="_blank"
            rel="noreferrer"
            className="text-sm text-muted-foreground hover:text-foreground p-2 rounded-md hover:bg-muted"
            title="GitHub"
          >
            <Github size={16} />
          </a>
          {signedIn ? (
            <Link to="/dashboard">
              <Button size="sm">{t("common.openDashboard")}</Button>
            </Link>
          ) : (
            <Link to="/login">
              <Button size="sm" variant="ghost">
                {t("common.signIn")}
              </Button>
            </Link>
          )}
        </nav>
      </div>
    </header>
  );
}

function Hero({ signedIn }: { signedIn: boolean }) {
  const { t } = useI18n();
  return (
    <section className="mx-auto max-w-6xl px-6 pt-20 pb-16">
      <div className="max-w-3xl">
        <h1 className="text-4xl sm:text-5xl font-bold tracking-tight">
          {t("landing.tagline")}
        </h1>
        <p className="mt-5 text-lg text-muted-foreground leading-relaxed">
          {t("landing.hero.subhead")}
        </p>
        <div className="mt-8 flex items-center gap-3 flex-wrap">
          <Link to={signedIn ? "/dashboard" : "/login"}>
            <Button size="lg" className="gap-2">
              {signedIn
                ? t("common.openDashboard")
                : t("landing.hero.getStarted")}
              <ArrowRight size={16} />
            </Button>
          </Link>
          <a href={REPO_URL} target="_blank" rel="noreferrer">
            <Button size="lg" variant="outline" className="gap-2">
              <Github size={16} />
              {t("landing.hero.viewGitHub")}
            </Button>
          </a>
        </div>

        <pre className="mt-10 rounded-lg border bg-muted/40 p-4 text-sm overflow-x-auto">
          <code>
            <span className="text-muted-foreground">
              {t("landing.hero.codeComment.install")}
            </span>
            {"\n"}
            cargo install devist{"\n\n"}
            <span className="text-muted-foreground">
              {t("landing.hero.codeComment.scaffold")}
            </span>
            {"\n"}
            devist init my-app --template react-vite{"\n"}
            devist start my-app --dev
          </code>
        </pre>
      </div>
    </section>
  );
}

function About() {
  const { t } = useI18n();
  const cards = [
    {
      icon: <Boxes size={20} />,
      title: t("landing.about.templates.title"),
      body: t("landing.about.templates.body"),
    },
    {
      icon: <Eye size={20} />,
      title: t("landing.about.worker.title"),
      body: t("landing.about.worker.body"),
    },
    {
      icon: <LayoutDashboard size={20} />,
      title: t("landing.about.dashboard.title"),
      body: t("landing.about.dashboard.body"),
    },
  ];

  return (
    <section className="border-t bg-muted/20">
      <div className="mx-auto max-w-6xl px-6 py-16">
        <div className="max-w-2xl">
          <h2 className="text-2xl sm:text-3xl font-bold tracking-tight">
            {t("landing.about.title")}
          </h2>
          <p className="mt-3 text-muted-foreground">
            {t("landing.about.subtitle")}
          </p>
        </div>
        <div className="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
          {cards.map((c) => (
            <div
              key={c.title}
              className="rounded-lg border bg-card p-5 space-y-3"
            >
              <div className="inline-flex items-center justify-center w-9 h-9 rounded-md bg-foreground/5">
                {c.icon}
              </div>
              <h3 className="font-semibold">{c.title}</h3>
              <p className="text-sm text-muted-foreground leading-relaxed">
                {c.body}
              </p>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

function Workflow() {
  const { t } = useI18n();
  const steps = [
    {
      n: "1",
      title: t("landing.workflow.install.title"),
      cmd: "cargo install devist",
      hint: t("landing.workflow.install.hint"),
    },
    {
      n: "2",
      title: t("landing.workflow.scaffold.title"),
      cmd: "devist init my-app --template react-vite",
      hint: t("landing.workflow.scaffold.hint"),
    },
    {
      n: "3",
      title: t("landing.workflow.startWorker.title"),
      cmd: "devist worker start",
      hint: t("landing.workflow.startWorker.hint"),
    },
  ];

  return (
    <section className="border-t">
      <div className="mx-auto max-w-6xl px-6 py-16">
        <div className="max-w-2xl">
          <h2 className="text-2xl sm:text-3xl font-bold tracking-tight">
            {t("landing.workflow.title")}
          </h2>
          <p className="mt-3 text-muted-foreground">
            {t("landing.workflow.subtitle")}
          </p>
        </div>
        <ol className="mt-10 space-y-5">
          {steps.map((s) => (
            <li
              key={s.n}
              className="flex gap-5 items-start rounded-lg border bg-card p-5"
            >
              <div className="shrink-0 inline-flex items-center justify-center w-8 h-8 rounded-full bg-foreground text-background text-sm font-semibold">
                {s.n}
              </div>
              <div className="flex-1 min-w-0 space-y-2">
                <h3 className="font-semibold">{s.title}</h3>
                <pre className="rounded border bg-muted/40 px-3 py-2 text-sm overflow-x-auto">
                  <code className="flex items-center gap-2">
                    <Terminal
                      size={14}
                      className="shrink-0 text-muted-foreground"
                    />
                    {s.cmd}
                  </code>
                </pre>
                <p className="text-sm text-muted-foreground">{s.hint}</p>
              </div>
            </li>
          ))}
        </ol>
      </div>
    </section>
  );
}

function Footer() {
  const { t } = useI18n();
  return (
    <footer className="border-t">
      <div className="mx-auto max-w-6xl px-6 py-8 flex items-center justify-between text-sm text-muted-foreground">
        <span>{t("footer.attribution")}</span>
        <div className="flex items-center gap-4">
          <a
            href={REPO_URL}
            target="_blank"
            rel="noreferrer"
            className="hover:text-foreground"
          >
            {t("footer.github")}
          </a>
          <a
            href={`${REPO_URL}/blob/main/LICENSE`}
            target="_blank"
            rel="noreferrer"
            className="hover:text-foreground"
          >
            {t("footer.license")}
          </a>
        </div>
      </div>
    </footer>
  );
}