| 1 | import { useEffect } from "react"; |
| 2 | import { data, redirect, useNavigate } from "react-router"; |
| 3 | |
| 4 | import type { Route } from "./+types/home"; |
| 5 | import { DESKTOP_COOKIE, DESKTOP_COOKIE_SECONDS, DESKTOP_QUERY, DESKTOP_VALUE, fromDesktopApp, launchedFromDesktopApp, rememberDesktopApp } from "../lib/desktop"; |
| 6 | import { cookie } from "../lib/github"; |
| 7 | import { desktopBridge } from "../lib/notify-client"; |
| 8 | import { page } from "../lib/meta"; |
| 9 | import { application, organization } from "../lib/structured-data"; |
| 10 | import { WORKSPACE_COOKIE, chosenWorkspace } from "../lib/workspace-choice"; |
| 11 | import { readCookie } from "../lib/mission"; |
| 12 | import { Landing } from "../components/landing"; |
| 13 | import { getViewer } from "../lib/session.server"; |
| 14 | import { homePath } from "../lib/workspace-nav"; |
| 15 | |
| 16 | export function meta(args: Route.MetaArgs) { |
| 17 | return [ |
| 18 | ...page(args, { |
| 19 | title: "g1t · Where your team's code lands", |
| 20 | description: |
| 21 | "Repositories, pull requests, required checks, a merge queue and deployments, with chat for your team. Connect the coding agents you already use through MCP and the API. Chat is free on every plan, never per seat.", |
| 22 | }), |
| 23 | organization(), |
| 24 | application(20), |
| 25 | ]; |
| 26 | } |
| 27 | |
| 28 | /** |
| 29 | * g1t.sh: the landing page for a visitor. Someone signed in has a Home in |
| 30 | * their workspace (routes/workspace/home.tsx); the query goes with them. |
| 31 | * |
| 32 | * The desktop app is for using g1t, so a visitor there goes to the sign-in |
| 33 | * page instead (lib/desktop.ts). Its launcher opens `/?app=desktop`, which |
| 34 | * sets the cookie that says so. |
| 35 | */ |
| 36 | export async function loader({ context, request }: Route.LoaderArgs) { |
| 37 | const viewer = getViewer(context); |
| 38 | const url = new URL(request.url); |
| 39 | const launched = launchedFromDesktopApp(url); |
| 40 | const headers = new Headers(); |
| 41 | if (launched) { |
| 42 | headers.set("set-cookie", cookie(DESKTOP_COOKIE, DESKTOP_VALUE, DESKTOP_COOKIE_SECONDS)); |
| 43 | url.searchParams.delete(DESKTOP_QUERY); |
| 44 | } |
| 45 | if (!viewer) { |
| 46 | if (launched || fromDesktopApp(request)) throw redirect("/login", { headers }); |
| 47 | return data({ signedIn: false as const }); |
| 48 | } |
| 49 | const slug = chosenWorkspace(viewer.workspaces ?? [], readCookie(request.headers.get("cookie"), WORKSPACE_COOKIE))?.slug ?? null; |
| 50 | // A workspace-less account is sent to make one by the session (lib/session.server.ts). |
| 51 | if (!slug) throw redirect("/workspaces/new", { headers }); |
| 52 | throw redirect(homePath(slug, url.search), { headers }); |
| 53 | } |
| 54 | |
| 55 | export default function Home() { |
| 56 | const navigate = useNavigate(); |
| 57 | // A build of the desktop app older than `?app=desktop` opens `/` as it |
| 58 | // is: the page sets the cookie itself and moves on to signing in. |
| 59 | useEffect(() => { |
| 60 | if (!desktopBridge()) return; |
| 61 | rememberDesktopApp(); |
| 62 | navigate("/login", { replace: true }); |
| 63 | }, [navigate]); |
| 64 | return <Landing />; |
| 65 | } |