the universal sandbox runtime for agents and humans. pocketenv.io
sandbox openclaw agent claude-code vercel-sandbox deno-sandbox cloudflare-sandbox atproto sprites daytona
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

Handle OAuth token, auth UI and route guards

Use URL search params for OAuth callback and set cli flag.
Home reads did from query, fetches /token, stores token and
redirects to /projects. Main redirects unauthenticated users to
/. Navbar, SignIn and Sidebar updates: sign out clears token and
redirects to /, authenticated state toggles menus, logo links to
/projects, modal close/reset and button/style tweaks.

+77 -21
+6 -1
apps/api/src/bsky/index.ts
··· 123 123 return; 124 124 } 125 125 126 - res.redirect(`${env.FRONTEND_URL}?did=${did}&cli=${cli}`); 126 + const url = new URL(env.FRONTEND_URL); 127 + url.searchParams.set("did", did); 128 + if (cli) { 129 + url.searchParams.set("cli", "1"); 130 + } 131 + res.redirect(url.toString()); 127 132 }); 128 133 129 134 app.get("/token", async (req, res) => {
+6 -3
apps/web/src/components/navbar/Navbar.tsx
··· 25 25 }; 26 26 27 27 const onSignOut = () => { 28 + localStorage.removeItem("token"); 28 29 setOpen(false); 29 - navigate({ to: "/signin" }); 30 + navigate({ to: "/" }); 30 31 }; 31 32 32 33 useEffect(() => { ··· 47 48 document.removeEventListener("mousedown", handleClickOutside); 48 49 }; 49 50 }, [open]); 51 + 52 + const isAuthenticated = !!localStorage.getItem("token"); 50 53 51 54 return ( 52 55 <nav className="navbar bg-base-100 h-[65px]"> ··· 90 93 Docs 91 94 </a> 92 95 </div> 93 - {false && ( 96 + {isAuthenticated && ( 94 97 <div 95 98 ref={dropdownRef} 96 99 className={`dropdown relative inline-flex [--auto-close:inside] [--offset:8] [--placement:bottom-end] ${ ··· 176 179 </ul> 177 180 </div> 178 181 )} 179 - {true && ( 182 + {!isAuthenticated && ( 180 183 <button 181 184 onClick={() => setSignInModalOpen(true)} 182 185 className="btn btn-block bg-blue-600/20 border-none text-blue-500 font-extrabold w-[100px]"
+5 -3
apps/web/src/components/sidebar/Sidebar.tsx
··· 21 21 tabIndex="-1" 22 22 > 23 23 <div className="drawer-body px-2 pt-4 bg-base-100"> 24 - <div className="mb-[30px] ml-[5px]"> 25 - <img src={Logo} className="max-h-[40px] mr-[15px]" /> 26 - </div> 24 + <Link to="/projects"> 25 + <div className="mb-[30px] ml-[5px]"> 26 + <img src={Logo} className="max-h-[40px] mr-[15px]" /> 27 + </div> 28 + </Link> 27 29 <ul className="menu p-0"> 28 30 <li> 29 31 <Link
+19 -5
apps/web/src/components/signin/SignIn.tsx
··· 1 1 import { useEffect, useRef, useState } from "react"; 2 2 import { API_URL } from "../../consts"; 3 + import { useSearch } from "@tanstack/react-router"; 3 4 4 5 export type SignInProps = { 5 6 isOpen: boolean; ··· 54 55 > 55 56 <div className="overlay-animation-target modal-dialog overlay-open:mt-4 overlay-open:duration-300 mt-12 transition-all ease-out"> 56 57 <div className="modal-content"> 57 - <div className="modal-header"></div> 58 + <div className="modal-header"> 59 + <button 60 + type="button" 61 + className="btn btn-text btn-circle btn-sm absolute end-3 top-3" 62 + aria-label="Close" 63 + onClick={() => { 64 + onClose(); 65 + setHandle(""); 66 + }} 67 + > 68 + <span className="icon-[tabler--x] size-4"></span> 69 + </button> 70 + </div> 58 71 <div className="modal-body"> 59 72 <div className="flex flex-col items-center gap-6 w-[400px]"> 60 73 <div className="form-control w-full"> 61 74 <label className="label"> 62 75 <span className="label-text text-[15px]">Handle</span> 63 76 </label> 64 - <div className="input input-bordered w-full input-lg text-[15px] font-semibold bg-transparent focus-within:border-pink-500! outline-none!"> 77 + <div className="input input-bordered w-full input-lg text-[15px] font-semibold bg-transparent"> 65 78 <span className="label-text my-auto text-[16px] opacity-50 mr-[10px]"> 66 79 @ 67 80 </span> ··· 77 90 </div> 78 91 79 92 <button 80 - className="btn btn-lg font-bold bg-pink-500 border-none w-full" 93 + className="btn btn-lg font-bold btn-primary border-none w-full" 81 94 onClick={onSignIn} 82 95 > 83 96 Sign In ··· 86 99 Don't have an atproto handle yet? You can create one at 87 100 <a 88 101 href={`${API_URL}/login?prompt=create`} 89 - className="text-pink-400" 102 + className="text-primary" 90 103 > 91 104 selfhosted.social 92 105 </a> 93 106 ,{" "} 94 107 <a 95 108 href="https://bsky.app/" 96 - className="text-pink-400" 109 + className="text-primary" 97 110 target="_blank" 98 111 > 99 112 Bluesky ··· 113 126 style={{ zIndex: 79 }} 114 127 className="overlay-backdrop transition duration-300 fixed inset-0 bg-base-300/60 overflow-y-auto opacity-75" 115 128 onClick={() => { 129 + setHandle(""); 116 130 onClose(); 117 131 }} 118 132 ></div>
+7 -1
apps/web/src/layouts/Main.tsx
··· 1 1 import type React from "react"; 2 - import { useRouterState } from "@tanstack/react-router"; 2 + import { useNavigate, useRouterState } from "@tanstack/react-router"; 3 3 import Navbar from "../components/navbar"; 4 4 import Sidebar from "../components/sidebar"; 5 5 ··· 9 9 10 10 function Main({ children }: MainProps) { 11 11 const routerState = useRouterState(); 12 + const navigate = useNavigate(); 12 13 const pathname = routerState.location.pathname; 14 + const isAuthenticated = !!localStorage.getItem("token"); 15 + 16 + if (!isAuthenticated) { 17 + navigate({ to: "/" }); 18 + } 13 19 14 20 const getTitle = (path: string): string => { 15 21 if (path === "/") return "Projects";
+30 -1
apps/web/src/pages/home/Home.tsx
··· 1 1 import Navbar from "./Navbar"; 2 2 import NewProject from "../../components/newproject"; 3 - import { useState } from "react"; 3 + import { useEffect, useState } from "react"; 4 + import { useNavigate, useSearch } from "@tanstack/react-router"; 5 + import { API_URL } from "../../consts"; 4 6 5 7 function Home() { 8 + const { did } = useSearch({ from: "/" }); 9 + const navigate = useNavigate(); 6 10 const [modalOpen, setModalOpen] = useState(false); 7 11 const banner = ` 8 12 ____ __ __ ··· 12 16 /_/ \\____/\\___/_/|_|\\___/\\__/\\___/_/ /_/|___/ 13 17 14 18 `; 19 + 20 + const isAuthenticated = !!localStorage.getItem("token"); 21 + 22 + if (isAuthenticated) { 23 + navigate({ to: "/projects" }); 24 + } 25 + 26 + useEffect(() => { 27 + if (did) { 28 + fetch(`${API_URL}/token`, { 29 + headers: { 30 + "session-did": did, 31 + }, 32 + }) 33 + .then((res) => { 34 + if (res.ok) { 35 + return res.json(); 36 + } 37 + }) 38 + .then(({ token }) => { 39 + localStorage.setItem("token", token); 40 + navigate({ to: "/projects" }); 41 + }); 42 + } 43 + }, [did, navigate]); 15 44 return ( 16 45 <> 17 46 <div className="flex flex-col min-h-screen bg-base-100">
+4 -7
apps/web/src/pages/signin/SignIn.tsx
··· 20 20 <label className="label"> 21 21 <span className="label-text text-[15px]">Handle</span> 22 22 </label> 23 - <div className="input input-bordered w-full input-lg text-[15px] font-semibold bg-transparent focus-within:border-pink-500! outline-none!"> 23 + <div className="input input-bordered w-full input-lg text-[15px] font-semibold bg-transparent "> 24 24 <span className="label-text my-auto text-[16px] opacity-50 mr-[10px]"> 25 25 @ 26 26 </span> ··· 35 35 </div> 36 36 37 37 <button 38 - className="btn btn-lg font-bold bg-pink-500 border-none w-full" 38 + className="btn btn-lg font-bold btn-primary border-none w-full" 39 39 onClick={onSignIn} 40 40 > 41 41 Sign In 42 42 </button> 43 43 <p className="text-center text-white/70"> 44 44 Don't have an atproto handle yet? You can create one at 45 - <a 46 - href={`${API_URL}/login?prompt=create`} 47 - className="text-pink-400" 48 - > 45 + <a href={`${API_URL}/login?prompt=create`} className="text-primary"> 49 46 selfhosted.social 50 47 </a> 51 48 ,{" "} 52 49 <a 53 50 href="https://bsky.app/" 54 - className="text-pink-400" 51 + className="text-primary" 55 52 target="_blank" 56 53 > 57 54 Bluesky