Repository Public
1'use client';2import Link from 'next/link';3import { useQueryClient } from '@tanstack/react-query';4import { motion } from 'motion/react';5import { FolderGit2, Plus, SquareTerminal } from 'lucide-react';6import { useAuthStatus, useProjects, useSessions } from '@/lib/queries';7import { useUi } from '@/stores/ui';8import { useCurrentProject } from '@/hooks/use-current';9import { useNow } from '@/hooks/use-viewport';10import { LIVE_SESSION_STATES } from '@cr/shared';11import { PullToRefresh } from '@/components/ui/pull-to-refresh';12import { IllusDone, IllusTerminal } from '@/components/ui/illustrations';13import { NotificationsBell } from '@/components/shell/notifications';14import { ConnectionIndicator } from '@/components/shell/connection-indicator';15import { Logo } from '@/components/shell/logo';16import { haptic } from '@/lib/haptic';17import { HostStrip, JumpBackIn, NeedsYou, SectionTitle, stagger, TaskActivity } from './sections';18import { useBrand } from '@/stores/branding';1920export function greeting(hour: number): string {21 if (hour < 5) return 'Good night';22 if (hour < 12) return 'Good morning';23 if (hour < 18) return 'Good afternoon';24 return 'Good evening';25}2627/** Phone landing screen. Every value shown comes from the API; empty sections say so. */28export function MobileHome() {29 const brand = useBrand();30 const qc = useQueryClient();31 const auth = useAuthStatus();32 const sessions = useSessions().data ?? [];33 const projects = useProjects().data ?? [];34 const project = useCurrentProject();35 const openNew = useUi((s) => s.openNewSession);36 const now = useNow(60_000);37 const live = sessions.filter((s) => LIVE_SESSION_STATES.includes(s.state)).length;38 const waiting = sessions.filter((s) => s.state === 'WAITING_FOR_INPUT').length;39 const username = auth.data?.user?.username;40 const summary = live === 0 ? 'Nothing is running right now.' : `${live} session${live === 1 ? '' : 's'} running${waiting ? ` · ${waiting} need${waiting === 1 ? 's' : ''} you` : ''}`;4142 return (43 <PullToRefresh onRefresh={() => qc.invalidateQueries()} className="h-full" testId="home">44 <header className="bg-brand pt-safe relative overflow-hidden rounded-b-[30px] shadow-float">45 <svg aria-hidden viewBox="0 0 390 220" preserveAspectRatio="none" className="absolute inset-0 size-full opacity-[.16]">46 <circle cx="340" cy="20" r="110" fill="#fff" /><circle cx="40" cy="230" r="90" fill="#fff" />47 </svg>48 <div className="px-safe relative px-5 pb-6 pt-3">49 <div className="flex items-center gap-2">50 <Logo className="size-8 rounded-[10px] ring-1 ring-white/35" />51 <span className="flex-1 text-[15px] font-semibold text-white/95">{brand.product}</span>52 <span className="rounded-full bg-white/18 px-1 text-white [&_*]:!text-white"><ConnectionIndicator compact className="px-1.5 py-2" /></span>53 <span className="rounded-full bg-white/18 text-white [&_button]:!text-white [&_button:hover]:!bg-white/15"><NotificationsBell /></span>54 </div>55 <motion.div {...stagger(0)}>56 <h1 className="mt-6 text-[30px] font-bold leading-[1.1] tracking-tight text-white" data-testid="greeting">57 {greeting(new Date(now).getHours())}{username ? <>,<br />{username}</> : ''}58 </h1>59 <p className="tnum mt-2 text-[15px] text-white/85" data-testid="home-summary">{summary}</p>60 </motion.div>61 <motion.div {...stagger(1)} className="mt-5 flex gap-2.5">62 <button onClick={() => { haptic(10); openNew(project?.id ?? null); }} data-testid="home-new-session" className="pressable flex h-[52px] flex-1 items-center justify-center gap-2 rounded-full bg-white text-[16px] font-semibold text-brand-ink shadow-float">63 <Plus className="size-5" strokeWidth={2.6} aria-hidden /> New {brand.short} session64 </button>65 <Link href="/terminal/" aria-label="Open terminal" onClick={() => haptic(6)} className="pressable flex size-[52px] shrink-0 items-center justify-center rounded-full bg-white/18 text-white ring-1 ring-white/30">66 <SquareTerminal className="size-[22px]" aria-hidden />67 </Link>68 </motion.div>69 </div>70 </header>71 <div className="px-safe flex flex-col gap-7 px-5 pb-10 pt-6">72 <NeedsYou />73 <JumpBackIn74 empty={75 <div className="card flex flex-col items-center gap-1 px-6 py-7 text-center">76 <IllusTerminal />77 <p className="mt-1 text-[16px] font-semibold tracking-tight">No sessions yet</p>78 <p className="text-[14px] leading-relaxed text-text-2">Start one above — it keeps running on the server after you close the app.</p>79 </div>80 }81 />82 <TaskActivity83 empty={84 <div className="card flex items-center gap-4 p-4">85 <span className="shrink-0 origin-left scale-[.62] [margin:-18px_-44px_-18px_0]"><IllusDone /></span>86 <p className="text-[14px] leading-relaxed text-text-2"><span className="block font-semibold text-text">No tasks in the last hour</span>Builds and long commands you start show up here, live.</p>87 </div>88 }89 />90 <HostStrip />91 {projects.length > 0 && (92 <section aria-label="Projects">93 <SectionTitle href="/projects/">Projects</SectionTitle>94 <ul className="no-scrollbar -mx-5 flex gap-2 overflow-x-auto px-5 pb-1">95 {projects.slice(0, 8).map((p) => (96 <li key={p.id} className="shrink-0">97 <button onClick={() => openNew(p.id)} className="pressable flex h-11 items-center gap-2 rounded-full bg-surface px-4 text-[14px] font-medium shadow-card">98 <FolderGit2 className="size-4 text-accent" aria-hidden /> {p.name}99 </button>100 </li>101 ))}102 </ul>103 </section>104 )}105 </div>106 </PullToRefresh>107 );108}109