Convex-only Slice 1: clients talk to Convex, Flue is a private worker (#20)

This commit was merged in pull request #20.
This commit is contained in:
2026-07-27 16:03:36 +00:00
parent cc47007fa9
commit cb7484912c
141 changed files with 1547 additions and 17812 deletions
+8 -25
View File
@@ -1,29 +1,12 @@
import { useWebAuth } from "@code/auth/web";
import { Navigate, Outlet, useLocation } from "react-router";
import { Outlet } from "react-router";
import { requireAuthToken } from "@/lib/auth.server";
import type { Route } from "./+types/layout";
export const loader = ({ request }: Route.LoaderArgs) =>
requireAuthToken(request);
export default function AppLayout() {
const auth = useWebAuth();
const location = useLocation();
if (auth.status === "loading") {
return (
<div className="grid min-h-svh place-items-center text-sm text-muted-foreground">
Loading…
</div>
);
}
if (auth.status === "inconsistent") {
return (
<div className="grid min-h-svh place-items-center text-sm text-muted-foreground">
Your session could not be verified
</div>
);
}
if (auth.status === "unauthenticated") {
return <Navigate replace state={{ from: location.pathname }} to="/login" />;
}
return <Outlet />;
}
@@ -1,12 +0,0 @@
import { SliceOnePage } from "@/components/slice-one/slice-one-page";
import type { Route } from "./+types/page";
export const meta = (_args: Route.MetaArgs) => [
{ title: "Zopu" },
{ content: "Chat with Zopu", name: "description" },
];
export default function MobileChatPage() {
return <SliceOnePage />;
}
@@ -1,15 +0,0 @@
import { MobileFlowPage } from "@/components/mobile-workspace/mobile-flow-page";
import type { Route } from "./+types/page";
export const meta = (_args: Route.MetaArgs) => [
{ title: "Work chat | Zopu" },
{
content: "Chat with Zopu in the context of a selected work unit",
name: "description",
},
];
export default function MobileWorkChatPage() {
return <MobileFlowPage screen="work-chat" />;
}
@@ -1,9 +0,0 @@
import { Outlet } from "react-router";
export default function MobileProductLayout() {
return (
<div className="min-h-svh bg-[#11110f]">
<Outlet />
</div>
);
}
@@ -1,15 +0,0 @@
import { MobileFlowPage } from "@/components/mobile-workspace/mobile-flow-page";
import type { Route } from "./+types/page";
export const meta = (_args: Route.MetaArgs) => [
{ title: "Active work | Zopu" },
{
content: "Browse active work and expand a work unit in place",
name: "description",
},
];
export default function MobileWorkListPage() {
return <MobileFlowPage screen="work-list" />;
}
@@ -1,15 +0,0 @@
import { MobileFlowPage } from "@/components/mobile-workspace/mobile-flow-page";
import type { Route } from "./+types/page";
export const meta = (_args: Route.MetaArgs) => [
{ title: "Work unit | Zopu" },
{
content: "Review the state, next milestone, and activity for a work unit",
name: "description",
},
];
export default function MobileWorkUnitPage() {
return <MobileFlowPage screen="work-unit-detail" />;
}
-123
View File
@@ -1,123 +0,0 @@
import { api } from "@code/backend/convex/_generated/api";
import type { Id } from "@code/backend/convex/_generated/dataModel";
import { Button } from "@code/ui/components/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@code/ui/components/card";
import { Checkbox } from "@code/ui/components/checkbox";
import { Input } from "@code/ui/components/input";
import { useMutation, useQuery } from "convex/react";
import { Loader2, Trash2 } from "lucide-react";
import { useState } from "react";
import type { FormEvent, ReactNode } from "react";
export default function Todos() {
const [newTodoText, setNewTodoText] = useState("");
const todos = useQuery(api.todos.getAll);
const createTodo = useMutation(api.todos.create);
const toggleTodo = useMutation(api.todos.toggle);
const deleteTodo = useMutation(api.todos.deleteTodo);
const handleAddTodo = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const text = newTodoText.trim();
if (!text) {
return;
}
await createTodo({ text });
setNewTodoText("");
};
const handleToggleTodo = (id: Id<"todos">, currentCompleted: boolean) => {
void toggleTodo({ completed: !currentCompleted, id });
};
const handleDeleteTodo = (id: Id<"todos">) => {
void deleteTodo({ id });
};
let todoContent: ReactNode;
if (todos === undefined) {
todoContent = (
<div className="flex justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
);
} else if (todos.length === 0) {
todoContent = (
<p className="py-4 text-center">No todos yet. Add one above!</p>
);
} else {
todoContent = (
<ul className="space-y-2">
{todos.map((todo) => (
<li
className="flex items-center justify-between rounded-md border p-2"
key={todo._id}
>
<div className="flex items-center space-x-2">
<Checkbox
checked={todo.completed}
id={`todo-${todo._id}`}
onCheckedChange={() =>
handleToggleTodo(todo._id, todo.completed)
}
/>
<label
className={
todo.completed
? "text-muted-foreground line-through"
: undefined
}
htmlFor={`todo-${todo._id}`}
>
{todo.text}
</label>
</div>
<Button
aria-label="Delete todo"
onClick={() => handleDeleteTodo(todo._id)}
size="icon"
variant="ghost"
>
<Trash2 className="h-4 w-4" />
</Button>
</li>
))}
</ul>
);
}
return (
<div className="mx-auto w-full max-w-md py-10">
<Card>
<CardHeader>
<CardTitle>Todo List</CardTitle>
<CardDescription>Manage your tasks efficiently</CardDescription>
</CardHeader>
<CardContent>
<form
className="mb-6 flex items-center space-x-2"
onSubmit={handleAddTodo}
>
<Input
onChange={(event) => setNewTodoText(event.target.value)}
placeholder="Add a new task..."
value={newTodoText}
/>
<Button disabled={!newTodoText.trim()} type="submit">
Add
</Button>
</form>
{todoContent}
</CardContent>
</Card>
</div>
);
}
+8 -16
View File
@@ -1,21 +1,13 @@
import { useWebAuth } from "@code/auth/web";
import { Navigate, Outlet } from "react-router";
import { Outlet } from "react-router";
import { redirectAuthenticated } from "@/lib/auth.server";
import type { Route } from "./+types/layout";
export const loader = ({ request }: Route.LoaderArgs) =>
redirectAuthenticated(request);
export default function AuthLayout() {
const auth = useWebAuth();
if (auth.status === "loading") {
return (
<div className="grid min-h-svh place-items-center text-sm text-muted-foreground">
Loading…
</div>
);
}
if (auth.status === "authenticated") {
return <Navigate replace to="/" />;
}
return (
<main className="grid min-h-svh place-items-center bg-muted/30 p-6 md:p-10">
<div className="w-full max-w-sm">
+10 -2
View File
@@ -1,5 +1,5 @@
import { LoginForm } from "@code/auth/web";
import { useNavigate } from "react-router";
import { useNavigate, useSearchParams } from "react-router";
import type { Route } from "./+types/page";
@@ -10,6 +10,14 @@ export const meta = (_args: Route.MetaArgs) => [
export default function LoginRoute() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const returnTo = searchParams.get("returnTo");
return <LoginForm onSuccess={() => navigate("/", { replace: true })} />;
return (
<LoginForm
onSuccess={() =>
navigate(returnTo?.startsWith("/") ? returnTo : "/", { replace: true })
}
/>
);
}
@@ -1,16 +0,0 @@
import { StandaloneChatPage } from "@/components/standalone-chat/standalone-chat-page";
import type { Route } from "./+types/page";
export const meta = (_args: Route.MetaArgs) => [
{
content: "width=device-width, initial-scale=1, viewport-fit=cover",
name: "viewport",
},
{ content: "Chat with Zopu", name: "description" },
{ title: "Zopu Chat" },
];
export default function Page() {
return <StandaloneChatPage />;
}