Skip to content

Commit 6c34da9

Browse files
committed
feat: Lark landing page — unique dark design with amber accent
- Space Grotesk + JetBrains Mono typography - Dark blue-gray base (#0c0e14) with warm amber (#f5a623) accent - Animated terminal demo showing agent collaboration - 2-col hero with eyebrow badge - Stats bar, 3x2 feature grid, 3-step how-it-works - Accordion FAQ, CTA banner, newsletter section - Lark bird SVG logo (singing in flight) - Sticky nav with blur-on-scroll - react-router-dom v7, Tailwind v4, Vite
1 parent 88ddac3 commit 6c34da9

17 files changed

Lines changed: 1457 additions & 366 deletions

package-lock.json

Lines changed: 599 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"tailwindcss": "^4.3.0"
2323
},
2424
"devDependencies": {
25+
"@tailwindcss/vite": "^4.3.0",
2526
"@types/react": "^19.0.0",
2627
"@types/react-dom": "^19.0.0",
2728
"@types/react-syntax-highlighter": "^15.5.13",

public/favicon.svg

Lines changed: 14 additions & 0 deletions
Loading

public/logo.svg

Lines changed: 30 additions & 0 deletions
Loading

src/App.tsx

Lines changed: 96 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import { createBrowserRouter, RouterProvider, Navigate } from "react-router-dom";
12
import { useState, useCallback, useEffect } from "react";
2-
import { Routes, Route, Navigate, useNavigate, useParams } from "react-router-dom";
33
import { api } from "./hooks/useApi";
44
import { useAuth } from "./hooks/useAuth";
55
import { useWebSocket } from "./hooks/useWebSocket";
@@ -14,10 +14,19 @@ import { WorkflowsPage } from "./pages/WorkflowsPage";
1414
import { BillingPage } from "./pages/BillingPage";
1515
import { IntegrationsPage } from "./pages/IntegrationsPage";
1616
import { SettingsPage } from "./pages/SettingsPage";
17+
import { LandingPage } from "./pages/LandingPage";
18+
19+
function WorkspaceLayout() {
20+
const { loggedIn, logout } = useAuth();
21+
// Extract workspaceId and channelId from URL manually since we're in a splat route
22+
const path = window.location.pathname;
23+
const parts = path.split("/").filter(Boolean);
24+
// URL pattern: /ws/:workspaceId/ch/:channelId
25+
const workspaceId = parts[1] || undefined;
26+
const channelId = parts[2] === "ch" ? parts[3] : undefined;
27+
28+
const navigate = (url: string) => { window.location.href = url; };
1729

18-
function WorkspaceLayout({ loggedIn, onLogout }: { loggedIn: boolean; onLogout: () => void }) {
19-
const { workspaceId, channelId } = useParams<{ workspaceId: string; channelId?: string }>();
20-
const navigate = useNavigate();
2130
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
2231
const [workspace, setWorkspace] = useState<Workspace | null>(null);
2332
const [channels, setChannels] = useState<Channel[]>([]);
@@ -29,16 +38,12 @@ function WorkspaceLayout({ loggedIn, onLogout }: { loggedIn: boolean; onLogout:
2938
switch (msg.type) {
3039
case "message.new": {
3140
const m = msg.data as Message;
32-
if (m.channel_id === channel?.id) {
33-
setMessages((prev) => [...prev, m]);
34-
}
41+
if (m.channel_id === channel?.id) setMessages((prev) => [...prev, m]);
3542
break;
3643
}
3744
case "message.edit": {
3845
const m = msg.data as Message;
39-
if (m.channel_id === channel?.id) {
40-
setMessages((prev) => prev.map((x) => (x.id === m.id ? { ...x, ...m } : x)));
41-
}
46+
if (m.channel_id === channel?.id) setMessages((prev) => prev.map((x) => (x.id === m.id ? { ...x, ...m } : x)));
4247
break;
4348
}
4449
case "message.delete": {
@@ -51,14 +56,10 @@ function WorkspaceLayout({ loggedIn, onLogout }: { loggedIn: boolean; onLogout:
5156

5257
useWebSocket(handleWSEvent);
5358

54-
// Load workspaces
5559
useEffect(() => {
56-
if (loggedIn) {
57-
api<Workspace[]>("/v1/workspaces").then(setWorkspaces).catch((e) => console.error("Failed to load workspaces:", e));
58-
}
60+
if (loggedIn) api<Workspace[]>("/v1/workspaces").then(setWorkspaces).catch(() => {});
5961
}, [loggedIn]);
6062

61-
// Select workspace from URL
6263
useEffect(() => {
6364
if (!workspaceId || workspaces.length === 0) return;
6465
const ws = workspaces.find((w) => w.id === workspaceId);
@@ -67,161 +68,123 @@ function WorkspaceLayout({ loggedIn, onLogout }: { loggedIn: boolean; onLogout:
6768
setChannel(null);
6869
setMessages([]);
6970
setActiveThread(null);
70-
api<Channel[]>(`/v1/workspaces/${ws.id}/channels`).then(setChannels).catch((e) => console.error("Failed to load channels:", e));
71+
api<Channel[]>(`/v1/workspaces/${ws.id}/channels`).then(setChannels).catch(() => {});
7172
}
7273
}, [workspaceId, workspaces]);
7374

74-
// Select channel from URL
7575
useEffect(() => {
7676
if (!channelId || channels.length === 0) return;
7777
const ch = channels.find((c) => c.id === channelId);
7878
if (ch && ch.id !== channel?.id) {
7979
setChannel(ch);
8080
setActiveThread(null);
81-
api<Message[]>(`/v1/channels/${ch.id}/messages?limit=50`).then((m) => setMessages(m || [])).catch((e) => console.error("Failed to load messages:", e));
81+
api<Message[]>(`/v1/channels/${ch.id}/messages?limit=50`).then((m) => setMessages(m || [])).catch(() => {});
8282
}
8383
}, [channelId, channels]);
8484

85-
const handleSelectWorkspace = (ws: Workspace) => {
86-
navigate(`/ws/${ws.id}`);
87-
};
88-
89-
const handleSelectChannel = (ch: Channel) => {
90-
navigate(`/ws/${workspaceId}/ch/${ch.id}`);
91-
};
92-
93-
const handleSendMessage = async (content: string, contentType?: string) => {
94-
if (!channel) return;
95-
await api(`/v1/channels/${channel.id}/messages`, {
96-
method: "POST",
97-
body: JSON.stringify({ content, content_type: contentType || "text" }),
98-
});
99-
};
100-
101-
const handleEditMessage = async (msg: Message) => {
102-
const newContent = prompt("Edit message:", msg.content);
103-
if (newContent === null || newContent === msg.content) return;
104-
await api(`/v1/messages/${msg.id}`, {
105-
method: "PATCH",
106-
body: JSON.stringify({ content: newContent }),
107-
});
108-
};
109-
85+
if (!loggedIn) return <Navigate to="/login" replace />;
11086
if (!workspaceId) {
111-
if (workspaces.length > 0) {
112-
return <Navigate to={`/ws/${workspaces[0].id}`} replace />;
113-
}
114-
return (
115-
<div className="flex-1 flex items-center justify-center text-gray-400">
116-
No workspaces available
117-
</div>
118-
);
87+
if (workspaces.length > 0) return <Navigate to={`/ws/${workspaces[0].id}`} replace />;
88+
return <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", color: "#565B73" }}>No workspaces available</div>;
11989
}
12090

91+
// Check which sub-page to show
92+
const subPage = parts[2]; // what comes after /ws/:id/
93+
94+
const renderContent = () => {
95+
if (subPage === "notifications") return <NotificationsPage />;
96+
if (subPage === "calls") return <CallsPage />;
97+
if (subPage === "workflows") return <WorkflowsPage />;
98+
if (subPage === "billing") return <BillingPage />;
99+
if (subPage === "integrations") return <IntegrationsPage />;
100+
if (subPage === "settings") return <SettingsPage />;
101+
if (subPage === "ch" && channel) {
102+
return (
103+
<div style={{ display: "flex", flex: 1, minWidth: 0 }}>
104+
<ChannelView channel={channel} messages={messages}
105+
onSendMessage={async (c, ct) => {
106+
if (channel) await api(`/v1/channels/${channel.id}/messages`, { method: "POST", body: JSON.stringify({ content: c, content_type: ct || "text" }) });
107+
}}
108+
onEditMessage={async (msg) => {
109+
const nc = prompt("Edit message:", msg.content);
110+
if (nc !== null && nc !== msg.content) await api(`/v1/messages/${msg.id}`, { method: "PATCH", body: JSON.stringify({ content: nc }) });
111+
}}
112+
onOpenThread={(id) => setActiveThread(id)}
113+
/>
114+
{activeThread && <ThreadPanel parentMessageId={activeThread} onClose={() => setActiveThread(null)} />}
115+
</div>
116+
);
117+
}
118+
// Default: try to show first channel
119+
if (channels.length > 0 && !subPage) {
120+
return <Navigate to={`/ws/${workspaceId}/ch/${channels[0].id}`} replace />;
121+
}
122+
return <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", color: "#565B73" }}>Select a channel</div>;
123+
};
124+
121125
return (
122-
<div className="flex h-screen bg-white">
126+
<div style={{ display: "flex", height: "100vh", background: "#111318" }}>
123127
<Sidebar
124-
workspaces={workspaces}
125-
workspace={workspace}
126-
channels={channels}
127-
channel={channel}
128+
workspaces={workspaces} workspace={workspace} channels={channels} channel={channel}
128129
workspaceId={workspaceId}
129-
onSelectWorkspace={handleSelectWorkspace}
130-
onSelectChannel={handleSelectChannel}
131-
onLogout={onLogout}
130+
onSelectWorkspace={(ws) => navigate(`/ws/${ws.id}`)}
131+
onSelectChannel={(ch) => navigate(`/ws/${workspaceId}/ch/${ch.id}`)}
132+
onLogout={() => { logout(); navigate("/"); }}
132133
/>
133-
<Routes>
134-
<Route
135-
path="ch/:channelId"
136-
element={
137-
channel ? (
138-
<div className="flex flex-1 min-w-0">
139-
<ChannelView
140-
channel={channel}
141-
messages={messages}
142-
onSendMessage={handleSendMessage}
143-
onEditMessage={handleEditMessage}
144-
onOpenThread={setActiveThread}
145-
/>
146-
{activeThread && (
147-
<ThreadPanel
148-
parentMessageId={activeThread}
149-
onClose={() => setActiveThread(null)}
150-
/>
151-
)}
152-
</div>
153-
) : (
154-
<div className="flex-1 flex items-center justify-center text-gray-400">
155-
Select a channel
156-
</div>
157-
)
158-
}
159-
/>
160-
<Route path="notifications" element={<NotificationsPage />} />
161-
<Route path="calls" element={<CallsPage />} />
162-
<Route path="workflows" element={<WorkflowsPage />} />
163-
<Route path="billing" element={<BillingPage />} />
164-
<Route path="integrations" element={<IntegrationsPage />} />
165-
<Route path="settings" element={<SettingsPage />} />
166-
<Route
167-
index
168-
element={
169-
channels.length > 0 ? (
170-
<Navigate to={`ch/${channels[0].id}`} replace />
171-
) : (
172-
<div className="flex-1 flex items-center justify-center text-gray-400">
173-
No channels in this workspace
174-
</div>
175-
)
176-
}
177-
/>
178-
</Routes>
134+
{renderContent()}
179135
</div>
180136
);
181137
}
182138

183-
export default function App() {
184-
const { loggedIn, login, logout } = useAuth();
185-
const navigate = useNavigate();
139+
function AppRoot() {
140+
const { loggedIn, login } = useAuth();
186141

187-
// Check for OAuth callback token in URL hash
188142
useEffect(() => {
189143
const hash = window.location.hash;
190-
if (hash.startsWith("#token=")) {
191-
const token = hash.slice(7);
144+
if (hash && hash.startsWith("#token=")) {
145+
const t = hash.slice(7);
192146
window.location.hash = "";
193-
login(token).then((ok) => {
194-
if (ok) {
195-
api<Workspace[]>("/v1/workspaces").then((ws) => {
196-
if (ws.length > 0) navigate(`/ws/${ws[0].id}`);
197-
});
198-
}
147+
login(t).then((ok) => {
148+
if (ok) api<Workspace[]>("/v1/workspaces").then((ws) => { if (ws.length > 0) window.location.href = `/ws/${ws[0].id}`; });
199149
});
200150
}
201-
}, [login, navigate]);
151+
}, [login]);
152+
153+
if (!loggedIn) {
154+
return <LandingPage />;
155+
}
156+
157+
return <WorkspaceLayout />;
158+
}
202159

160+
function LoginRoute() {
161+
const { login } = useAuth();
203162
const handleLogin = async (token: string) => {
204163
const ok = await login(token);
205164
if (ok) {
206165
const ws = await api<Workspace[]>("/v1/workspaces");
207-
if (ws.length > 0) navigate(`/ws/${ws[0].id}`);
166+
if (ws.length > 0) window.location.href = `/ws/${ws[0].id}`;
208167
}
209168
return ok;
210169
};
170+
return <LoginView onLogin={handleLogin} />;
171+
}
211172

212-
const handleLogout = () => {
213-
logout();
214-
navigate("/");
215-
};
216-
217-
if (!loggedIn) {
218-
return <LoginView onLogin={handleLogin} />;
219-
}
173+
const router = createBrowserRouter([
174+
{ path: "/", element: <AppRoot /> },
175+
{ path: "/login", element: <LoginRoute /> },
176+
{ path: "/ws", element: <WorkspaceLayout /> },
177+
{ path: "/ws/:workspaceId", element: <WorkspaceLayout /> },
178+
{ path: "/ws/:workspaceId/ch/:channelId", element: <WorkspaceLayout /> },
179+
{ path: "/ws/:workspaceId/notifications", element: <WorkspaceLayout /> },
180+
{ path: "/ws/:workspaceId/calls", element: <WorkspaceLayout /> },
181+
{ path: "/ws/:workspaceId/workflows", element: <WorkspaceLayout /> },
182+
{ path: "/ws/:workspaceId/billing", element: <WorkspaceLayout /> },
183+
{ path: "/ws/:workspaceId/integrations", element: <WorkspaceLayout /> },
184+
{ path: "/ws/:workspaceId/settings", element: <WorkspaceLayout /> },
185+
{ path: "*", element: <Navigate to="/" replace /> },
186+
]);
220187

221-
return (
222-
<Routes>
223-
<Route path="/ws/*" element={<WorkspaceLayout loggedIn={loggedIn} onLogout={handleLogout} />} />
224-
<Route path="*" element={<Navigate to="/ws" replace />} />
225-
</Routes>
226-
);
188+
export default function App() {
189+
return <RouterProvider router={router} />;
227190
}

0 commit comments

Comments
 (0)