SPB Git

Repository Public

start-view.tsx 5.6 KB 102 lines tsx

1'use client';2import { useRouter } from 'next/navigation';3import { FolderGit2, FolderTree, GitBranch, ListChecks, Sparkles, SquareTerminal } from 'lucide-react';4import { useAuthStatus, useProjects, useSessions } from '@/lib/queries';5import { useUi } from '@/stores/ui';6import { useCurrentProject } from '@/hooks/use-current';7import { useNow } from '@/hooks/use-viewport';8import { fmtRelative } from '@/lib/format';9import { LIVE_SESSION_STATES } from '@cr/shared';10import { Button } from '@/components/ui/button';11import { IllusFolder, IllusTerminal } from '@/components/ui/illustrations';12import { greeting } from './mobile-home';13import { HostStrip, JumpBackIn, NeedsYou, SectionTitle, TaskActivity } from './sections';14import { useBrand } from '@/stores/branding';1516/** Desktop start view: shown in the workspace when no terminal is open, and at /home/. Real data only. */17export function StartView() {18  const brand = useBrand();19  const router = useRouter();20  const auth = useAuthStatus();21  const sessions = useSessions().data ?? [];22  const projects = useProjects();23  const project = useCurrentProject();24  const openNew = useUi((s) => s.openNewSession);25  const now = useNow(30_000);26  const live = sessions.filter((s) => LIVE_SESSION_STATES.includes(s.state)).length;27  const recent = [...(projects.data ?? [])].sort((a, b) => Number(b.pinned) - Number(a.pinned) || (b.lastActivityAt ?? '').localeCompare(a.lastActivityAt ?? '')).slice(0, 6);28  const username = auth.data?.user?.username;29  return (30    <div className="h-full overflow-y-auto" data-testid="start-view">31      <div className="mx-auto flex max-w-6xl flex-col gap-7 px-8 py-8">32        <header className="flex flex-wrap items-end justify-between gap-4">33          <div>34            <h1 className="text-[26px] font-bold leading-tight tracking-tight" data-testid="greeting">35              {greeting(new Date(now).getHours())}{username ? `, ${username}` : ''}36            </h1>37            <p className="tnum mt-1 text-sm text-text-2">{live === 0 ? 'Nothing is running right now.' : `${live} session${live === 1 ? '' : 's'} running on the server.`}</p>38          </div>39          <div className="flex flex-wrap gap-2">40            <Button variant="brand" className="h-10 px-4" onClick={() => openNew(project?.id ?? null)} data-testid="empty-new-session">41              <Sparkles className="size-4" /> New session42            </Button>43            <Button className="h-10" onClick={() => router.push(project ? `/files/?p=${project.id}` : '/files/')}><FolderTree className="size-4" /> Files</Button>44            <Button className="h-10" onClick={() => router.push('/tasks/')}><ListChecks className="size-4" /> Tasks</Button>45          </div>46        </header>47        <div className="grid gap-7 lg:grid-cols-[minmax(0,2fr)_minmax(280px,1fr)]">48          <div className="flex min-w-0 flex-col gap-7">49            <NeedsYou />50            <JumpBackIn51              empty={52                <div className="card flex items-center gap-6 px-8 py-7" data-testid="no-session">53                  <IllusTerminal />54                  <div>55                    <p className="text-base font-semibold tracking-tight">No sessions yet</p>56                    <p className="mt-1 max-w-md text-[13px] leading-relaxed text-text-2">Start a {brand.agentName} or shell session. It runs on the server inside tmux and keeps going after you close this browser.</p>57                  </div>58                </div>59              }60            />61            <section aria-label="Projects">62              <SectionTitle href="/projects/">Projects</SectionTitle>63              {projects.isPending ? null : recent.length === 0 ? (64                <div className="card flex items-center gap-6 px-8 py-6">65                  <IllusFolder />66                  <div>67                    <p className="text-base font-semibold tracking-tight">No projects yet</p>68                    <p className="mt-1 text-[13px] text-text-2">Register a directory or clone a repository to give sessions a home.</p>69                    <Button className="mt-3" onClick={() => router.push('/projects/')}>Add a project</Button>70                  </div>71                </div>72              ) : (73                <ul className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">74                  {recent.map((p) => (75                    <li key={p.id}>76                      <button onClick={() => openNew(p.id)} className="card pressable flex w-full items-center gap-3 p-3.5 text-left">77                        <span className="flex size-9 shrink-0 items-center justify-center rounded-[12px] bg-accent/10 text-accent"><FolderGit2 className="size-[18px]" aria-hidden /></span>78                        <span className="min-w-0 flex-1">79                          <span className="block truncate text-[13.5px] font-semibold">{p.name}</span>80                          <span className="tnum flex items-center gap-1 truncate text-xs text-muted">81                            {p.gitBranch && <><GitBranch className="size-3" aria-hidden /><span className="font-mono">{p.gitBranch}</span> · </>}82                            {p.lastActivityAt ? fmtRelative(p.lastActivityAt, now) : 'no activity yet'}83                          </span>84                        </span>85                        <SquareTerminal className="size-4 shrink-0 text-faint" aria-hidden />86                      </button>87                    </li>88                  ))}89                </ul>90              )}91            </section>92          </div>93          <aside className="flex min-w-0 flex-col gap-7">94            <HostStrip />95            <TaskActivity />96          </aside>97        </div>98      </div>99    </div>100  );101}102
Files main
Menu

Theme