an appview-less Bluesky client using Constellation and PDS Queries reddwarf.app
frontend spa bluesky reddwarf microcosm

material 3 initial design

rimar1337 ff2894e7 bfa92e50

+12
package-lock.json
··· 32 32 "@iconify-icon/react": "^3.0.1", 33 33 "@iconify-json/material-symbols": "^1.2.42", 34 34 "@iconify-json/mdi": "^1.2.3", 35 + "@iconify/json": "^2.2.396", 35 36 "@svgr/core": "^8.1.0", 36 37 "@svgr/plugin-jsx": "^8.1.0", 37 38 "@testing-library/dom": "^10.4.0", ··· 1681 1682 "license": "Apache-2.0", 1682 1683 "dependencies": { 1683 1684 "@iconify/types": "*" 1685 + } 1686 + }, 1687 + "node_modules/@iconify/json": { 1688 + "version": "2.2.396", 1689 + "resolved": "https://registry.npmjs.org/@iconify/json/-/json-2.2.396.tgz", 1690 + "integrity": "sha512-tijg77JFuYIt32S9N8p7La8C0zp9zKZsX6UP8ip5GVB1F6Mp3pZA5Vc5eAquTY50NoDJX58U6z4Qn3d6Wyossg==", 1691 + "dev": true, 1692 + "license": "MIT", 1693 + "dependencies": { 1694 + "@iconify/types": "*", 1695 + "pathe": "^2.0.0" 1684 1696 } 1685 1697 }, 1686 1698 "node_modules/@iconify/types": {
+1
package.json
··· 36 36 "@iconify-icon/react": "^3.0.1", 37 37 "@iconify-json/material-symbols": "^1.2.42", 38 38 "@iconify-json/mdi": "^1.2.3", 39 + "@iconify/json": "^2.2.396", 39 40 "@svgr/core": "^8.1.0", 40 41 "@svgr/plugin-jsx": "^8.1.0", 41 42 "@testing-library/dom": "^10.4.0",
+1
src/auto-imports.d.ts
··· 8 8 declare global { 9 9 const IconMaterialSymbolsAccountCircle: typeof import('~icons/material-symbols/account-circle.jsx').default 10 10 const IconMaterialSymbolsAccountCircleOutline: typeof import('~icons/material-symbols/account-circle-outline.jsx').default 11 + const IconMaterialSymbolsArrowBack: typeof import('~icons/material-symbols/arrow-back.jsx').default 11 12 const IconMaterialSymbolsHome: typeof import('~icons/material-symbols/home.jsx').default 12 13 const IconMaterialSymbolsHomeOutline: typeof import('~icons/material-symbols/home-outline.jsx').default 13 14 const IconMaterialSymbolsNotifications: typeof import('~icons/material-symbols/notifications.jsx').default
+29
src/components/Header.tsx
··· 1 + import { Link, useRouter } from "@tanstack/react-router"; 2 + 3 + export function Header({ 4 + backButtonCallback, 5 + title 6 + }: { 7 + backButtonCallback?: () => void; 8 + title?: string; 9 + }) { 10 + const router = useRouter(); 11 + //const what = router.history. 12 + return ( 13 + <div className="flex items-center gap-4 px-4 py-3 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700"> 14 + {backButtonCallback ? (<Link 15 + to=".." 16 + //className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg" 17 + className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg" 18 + onClick={(e) => { 19 + e.preventDefault(); 20 + backButtonCallback(); 21 + }} 22 + aria-label="Go back" 23 + > 24 + <IconMaterialSymbolsArrowBack className="w-6 h-6" /> 25 + </Link>) : (<div className="w-[0px]" />)} 26 + <span className="text-[21px] font-roboto">{title}</span> 27 + </div> 28 + ); 29 + }
+5 -4
src/components/InfiniteCustomFeed.tsx
··· 1 1 import * as React from "react"; 2 + 2 3 //import { useInView } from "react-intersection-observer"; 3 4 import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; 4 5 import { useAuth } from "~/providers/UnifiedAuthProvider"; 5 6 import { 6 - useQueryArbitrary, 7 - useQueryIdentity, 8 7 useInfiniteQueryFeedSkeleton, 8 + // useQueryArbitrary, 9 + // useQueryIdentity, 9 10 } from "~/utils/useQuery"; 10 11 11 12 interface InfiniteCustomFeedProps { ··· 112 113 <button 113 114 onClick={handleRefresh} 114 115 disabled={isRefetching} 115 - className="sticky lg:bottom-6 bottom-24 ml-4 w-[42px] h-[42px] z-10 bg-gray-500 hover:bg-gray-600 text-gray-50 p-[9px] rounded-full shadow-lg transition-transform duration-200 ease-in-out hover:scale-110 disabled:bg-gray-400 disabled:cursor-not-allowed" 116 + className="sticky lg:bottom-4 bottom-22 ml-4 w-[42px] h-[42px] z-10 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-50 p-[9px] rounded-full shadow-lg transition-transform duration-200 ease-in-out hover:scale-110 disabled:bg-gray-400 disabled:cursor-not-allowed" 116 117 aria-label="Refresh feed" 117 118 > 118 - {isRefetching ? <RefreshIcon className="h-6 w-6 animate-spin" /> : <RefreshIcon className="h-6 w-6" />} 119 + {isRefetching ? <RefreshIcon className="h-6 w-6 text-gray-600 dark:text-gray-400 animate-spin" /> : <RefreshIcon className="h-6 w-6 text-gray-600 dark:text-gray-400" />} 119 120 </button> 120 121 </> 121 122 );
+143 -57
src/components/Login.tsx
··· 1 1 // src/components/Login.tsx 2 - import React, { useEffect, useState, useRef } from "react"; 2 + import { Agent } from "@atproto/api"; 3 + import React, { useEffect, useRef, useState } from "react"; 4 + 3 5 import { useAuth } from "~/providers/UnifiedAuthProvider"; 4 - import { Agent } from "@atproto/api"; 5 6 6 7 // --- 1. The Main Component (Orchestrator with `compact` prop) --- 7 - export default function Login({ compact = false }: { compact?: boolean }) { 8 + export default function Login({ 9 + compact = false, 10 + popup = false, 11 + }: { 12 + compact?: boolean; 13 + popup?: boolean; 14 + }) { 8 15 const { status, agent, logout } = useAuth(); 9 16 10 17 // Loading state can be styled differently based on the prop ··· 33 40 // Large view 34 41 if (!compact) { 35 42 return ( 36 - <div className="p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-6 mx-4"> 43 + <div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-6 mx-4"> 37 44 <div className="flex flex-col items-center justify-center text-center"> 38 45 <p className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-100"> 39 46 You are logged in! ··· 41 48 <ProfileThing agent={agent} large /> 42 49 <button 43 50 onClick={logout} 44 - className="bg-gray-600 mt-4 hover:bg-gray-700 text-white rounded px-6 py-2 font-semibold text-base transition-colors" 51 + className="bg-gray-600 mt-4 hover:bg-gray-700 text-white rounded-full px-6 py-2 font-semibold text-base transition-colors" 45 52 > 46 53 Log out 47 54 </button> ··· 67 74 if (!compact) { 68 75 // Large view renders the form directly in the card 69 76 return ( 70 - <div className="p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-6 mx-4"> 77 + <div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-6 mx-4"> 71 78 <UnifiedLoginForm /> 72 79 </div> 73 80 ); 74 81 } 75 82 76 83 // Compact view renders a button that toggles the form in a dropdown 77 - return <CompactLoginButton />; 84 + return <CompactLoginButton popup={popup} />; 78 85 } 79 86 80 87 // --- 2. The Reusable, Self-Contained Login Form Component --- ··· 83 90 84 91 return ( 85 92 <div> 86 - <div className="flex border-b border-gray-200 dark:border-gray-700 mb-4"> 93 + <div className="flex bg-gray-300 rounded-full dark:bg-gray-700 mb-4"> 87 94 <TabButton 88 95 label="OAuth" 89 96 active={mode === "oauth"} ··· 103 110 // --- 3. Helper components for layouts, forms, and UI --- 104 111 105 112 // A new component to contain the logic for the compact dropdown 106 - const CompactLoginButton = () => { 113 + const CompactLoginButton = ({popup}:{popup?: boolean}) => { 107 114 const [showForm, setShowForm] = useState(false); 108 115 const formRef = useRef<HTMLDivElement>(null); 109 116 ··· 125 132 <div className="relative" ref={formRef}> 126 133 <button 127 134 onClick={() => setShowForm(!showForm)} 128 - className="text-sm bg-gray-600 hover:bg-gray-700 text-white rounded px-3 py-1 font-medium transition-colors" 135 + className="text-sm bg-gray-600 hover:bg-gray-700 text-white rounded-full px-3 py-1 font-medium transition-colors" 129 136 > 130 137 Log in 131 138 </button> 132 139 {showForm && ( 133 - <div className="absolute top-full right-0 mt-2 w-80 bg-white dark:bg-gray-900 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 z-50"> 140 + <div className={`absolute ${popup ? `bottom-[calc(100%)]` :`top-full`} right-0 mt-2 w-80 bg-white dark:bg-gray-900 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 z-50`}> 134 141 <UnifiedLoginForm /> 135 142 </div> 136 143 )} ··· 138 145 ); 139 146 }; 140 147 141 - const TabButton = ({ label, active, onClick }: { label: string; active: boolean; onClick: () => void; }) => ( 148 + const TabButton = ({ 149 + label, 150 + active, 151 + onClick, 152 + }: { 153 + label: string; 154 + active: boolean; 155 + onClick: () => void; 156 + }) => ( 142 157 <button 143 158 onClick={onClick} 144 - className={`px-4 py-2 text-sm font-medium transition-colors ${ 159 + className={`px-4 py-2 text-sm font-medium transition-colors rounded-full flex-1 ${ 145 160 active 146 - ? "text-gray-600 dark:text-gray-200 border-b-2 border-gray-500" 147 - : "text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200" 161 + ? "text-gray-950 dark:text-gray-200 border-gray-500 bg-gray-400 dark:bg-gray-500" 162 + : "text-gray-600 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200" 148 163 }`} 149 164 > 150 165 {label} ··· 169 184 }; 170 185 return ( 171 186 <form onSubmit={handleSubmit} className="flex flex-col gap-3"> 172 - <p className="text-xs text-gray-500 dark:text-gray-400">Sign in with AT. Your password is never shared.</p> 173 - <input type="text" placeholder="handle.bsky.social" value={handle} onChange={(e) => setHandle(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" /> 174 - <button type="submit" className="bg-gray-600 hover:bg-gray-700 text-white rounded px-4 py-2 font-medium text-sm transition-colors">Log in</button> 187 + <p className="text-xs text-gray-500 dark:text-gray-400"> 188 + Sign in with AT. Your password is never shared. 189 + </p> 190 + <input 191 + type="text" 192 + placeholder="handle.bsky.social" 193 + value={handle} 194 + onChange={(e) => setHandle(e.target.value)} 195 + className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" 196 + /> 197 + <button 198 + type="submit" 199 + className="bg-gray-600 hover:bg-gray-700 text-white rounded-full px-4 py-2 font-medium text-sm transition-colors" 200 + > 201 + Log in 202 + </button> 175 203 </form> 176 204 ); 177 205 }; ··· 201 229 202 230 return ( 203 231 <form onSubmit={handleSubmit} className="flex flex-col gap-3"> 204 - <p className="text-xs text-red-500 dark:text-red-400">Warning: Less secure. Use an App Password.</p> 205 - <input type="text" placeholder="handle.bsky.social" value={user} onChange={(e) => setUser(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" autoComplete="username" /> 206 - <input type="password" placeholder="App Password" value={password} onChange={(e) => setPassword(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" autoComplete="current-password" /> 207 - <input type="text" placeholder="PDS (e.g., bsky.social)" value={serviceURL} onChange={(e) => setServiceURL(e.target.value)} className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" /> 232 + <p className="text-xs text-red-500 dark:text-red-400"> 233 + Warning: Less secure. Use an App Password. 234 + </p> 235 + <input 236 + type="text" 237 + placeholder="handle.bsky.social" 238 + value={user} 239 + onChange={(e) => setUser(e.target.value)} 240 + className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" 241 + autoComplete="username" 242 + /> 243 + <input 244 + type="password" 245 + placeholder="App Password" 246 + value={password} 247 + onChange={(e) => setPassword(e.target.value)} 248 + className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" 249 + autoComplete="current-password" 250 + /> 251 + <input 252 + type="text" 253 + placeholder="PDS (e.g., bsky.social)" 254 + value={serviceURL} 255 + onChange={(e) => setServiceURL(e.target.value)} 256 + className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-gray-500" 257 + /> 208 258 {error && <p className="text-xs text-red-500">{error}</p>} 209 - <button type="submit" className="bg-gray-600 hover:bg-gray-700 text-white rounded px-4 py-2 font-medium text-sm transition-colors">Log in</button> 259 + <button 260 + type="submit" 261 + className="bg-gray-600 hover:bg-gray-700 text-white rounded-full px-4 py-2 font-medium text-sm transition-colors" 262 + > 263 + Log in 264 + </button> 210 265 </form> 211 266 ); 212 267 }; 213 268 214 269 // --- Profile Component (now supports a `large` prop for styling) --- 215 - export const ProfileThing = ({ agent, large = false }: { agent: Agent | null; large?: boolean }) => { 216 - const [profile, setProfile] = useState<any>(null); 270 + export const ProfileThing = ({ 271 + agent, 272 + large = false, 273 + }: { 274 + agent: Agent | null; 275 + large?: boolean; 276 + }) => { 277 + const [profile, setProfile] = useState<any>(null); 217 278 218 - useEffect(() => { 219 - const fetchUser = async () => { 220 - const did = (agent as any)?.session?.did ?? (agent as any)?.assertDid; 221 - if (!did) return; 222 - try { 223 - const res = await agent!.getProfile({ actor: did }); 224 - setProfile(res.data); 225 - } catch (e) { console.error("Failed to fetch profile", e); } 226 - }; 227 - if (agent) fetchUser(); 228 - }, [agent]); 229 - 230 - if (!profile) { 231 - return ( // Skeleton loader 232 - <div className={`flex items-center gap-2.5 animate-pulse ${large ? 'mb-1' : ''}`}> 233 - <div className={`rounded-full bg-gray-300 dark:bg-gray-700 ${large ? 'w-10 h-10' : 'w-[30px] h-[30px]'}`} /> 234 - <div className="flex flex-col gap-2"> 235 - <div className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? 'h-4 w-28' : 'h-3 w-20'}`} /> 236 - <div className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? 'h-4 w-20' : 'h-3 w-16'}`} /> 237 - </div> 238 - </div> 239 - ); 279 + useEffect(() => { 280 + const fetchUser = async () => { 281 + const did = (agent as any)?.session?.did ?? (agent as any)?.assertDid; 282 + if (!did) return; 283 + try { 284 + const res = await agent!.getProfile({ actor: did }); 285 + setProfile(res.data); 286 + } catch (e) { 287 + console.error("Failed to fetch profile", e); 240 288 } 241 - 242 - return ( 243 - <div className={`flex flex-row items-center gap-2.5 ${large ? 'mb-1' : ''}`}> 244 - <img src={profile?.avatar} alt="avatar" className={`object-cover rounded-full ${large ? 'w-10 h-10' : 'w-[30px] h-[30px]'}`} /> 245 - <div className="flex flex-col items-start text-left"> 246 - <div className={`font-medium ${large ? 'text-gray-800 dark:text-gray-100 text-md' : 'text-gray-800 dark:text-gray-100 text-sm'}`}>{profile?.displayName}</div> 247 - <div className={` ${large ? 'text-gray-500 dark:text-gray-400 text-sm' : 'text-gray-500 dark:text-gray-400 text-xs'}`}>@{profile?.handle}</div> 248 - </div> 289 + }; 290 + if (agent) fetchUser(); 291 + }, [agent]); 292 + 293 + if (!profile) { 294 + return ( 295 + // Skeleton loader 296 + <div 297 + className={`flex items-center gap-2.5 animate-pulse ${large ? "mb-1" : ""}`} 298 + > 299 + <div 300 + className={`rounded-full bg-gray-300 dark:bg-gray-700 ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`} 301 + /> 302 + <div className="flex flex-col gap-2"> 303 + <div 304 + className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? "h-4 w-28" : "h-3 w-20"}`} 305 + /> 306 + <div 307 + className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? "h-4 w-20" : "h-3 w-16"}`} 308 + /> 249 309 </div> 250 - ); 251 - }; 310 + </div> 311 + ); 312 + } 313 + 314 + return ( 315 + <div 316 + className={`flex flex-row items-center gap-2.5 ${large ? "mb-1" : ""}`} 317 + > 318 + <img 319 + src={profile?.avatar} 320 + alt="avatar" 321 + className={`object-cover rounded-full ${large ? "w-10 h-10" : "w-[30px] h-[30px]"}`} 322 + /> 323 + <div className="flex flex-col items-start text-left"> 324 + <div 325 + className={`font-medium ${large ? "text-gray-800 dark:text-gray-100 text-md" : "text-gray-800 dark:text-gray-100 text-sm"}`} 326 + > 327 + {profile?.displayName} 328 + </div> 329 + <div 330 + className={` ${large ? "text-gray-500 dark:text-gray-400 text-sm" : "text-gray-500 dark:text-gray-400 text-xs"}`} 331 + > 332 + @{profile?.handle} 333 + </div> 334 + </div> 335 + </div> 336 + ); 337 + };
+403 -122
src/routes/__root.tsx
··· 5 5 import type { QueryClient } from "@tanstack/react-query"; 6 6 import { 7 7 createRootRouteWithContext, 8 - Link, 8 + // Link, 9 9 // Outlet, 10 10 Scripts, 11 11 useLocation, ··· 176 176 )} 177 177 178 178 <div className="min-h-screen flex justify-center bg-gray-50 dark:bg-gray-950"> 179 - <nav className="hidden lg:flex h-screen w-[250px] flex-col gap-2 p-4 dark:border-gray-800 sticky top-0 self-start"> 179 + <nav className="hidden lg:flex h-screen w-[250px] flex-col gap-0 p-4 dark:border-gray-800 sticky top-0 self-start"> 180 180 <div className="flex items-center gap-3 mb-4"> 181 181 <img src="/redstar.png" alt="Red Dwarf Logo" className="w-8 h-8" /> 182 182 <span className="font-extrabold text-2xl tracking-tight text-gray-900 dark:text-gray-100"> ··· 186 186 </span> */} 187 187 </span> 188 188 </div> 189 - <Link 189 + <MaterialNavItem 190 + InactiveIcon={ 191 + <IconMaterialSymbolsHomeOutline className="w-6 h-6" /> 192 + } 193 + ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />} 194 + active={isHome} 195 + onClickCallbback={() => 196 + navigate({ 197 + to: "/", 198 + //params: { did: agent.assertDid }, 199 + }) 200 + } 201 + text="Home" 202 + /> 203 + 204 + <MaterialNavItem 205 + InactiveIcon={ 206 + <IconMaterialSymbolsNotificationsOutline className="w-6 h-6" /> 207 + } 208 + ActiveIcon={ 209 + <IconMaterialSymbolsNotifications className="w-6 h-6" /> 210 + } 211 + active={isNotifications} 212 + onClickCallbback={() => 213 + navigate({ 214 + to: "/notifications", 215 + //params: { did: agent.assertDid }, 216 + }) 217 + } 218 + text="Notifications" 219 + /> 220 + <MaterialNavItem 221 + InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 222 + ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 223 + active={location.pathname.startsWith("/feeds")} 224 + onClickCallbback={() => 225 + navigate({ 226 + to: "/feeds", 227 + //params: { did: agent.assertDid }, 228 + }) 229 + } 230 + text="Feeds" 231 + /> 232 + <MaterialNavItem 233 + InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 234 + ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 235 + active={location.pathname.startsWith("/search")} 236 + onClickCallbback={() => 237 + navigate({ 238 + to: "/search", 239 + //params: { did: agent.assertDid }, 240 + }) 241 + } 242 + text="Search" 243 + /> 244 + <MaterialNavItem 245 + InactiveIcon={ 246 + <IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" /> 247 + } 248 + ActiveIcon={ 249 + <IconMaterialSymbolsAccountCircle className="w-6 h-6" /> 250 + } 251 + active={isProfile ?? false} 252 + onClickCallbback={() => { 253 + if (authed && agent && agent.assertDid) { 254 + //window.location.href = `/profile/${agent.assertDid}`; 255 + navigate({ 256 + to: "/profile/$did", 257 + params: { did: agent.assertDid }, 258 + }); 259 + } 260 + }} 261 + text="Profile" 262 + /> 263 + <MaterialNavItem 264 + InactiveIcon={ 265 + <IconMaterialSymbolsSettingsOutline className="w-6 h-6" /> 266 + } 267 + ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />} 268 + active={location.pathname.startsWith("/settings")} 269 + onClickCallbback={() => 270 + navigate({ 271 + to: "/settings", 272 + //params: { did: agent.assertDid }, 273 + }) 274 + } 275 + text="Settings" 276 + /> 277 + <div className="flex flex-row items-center justify-center mt-3"> 278 + <MaterialPillButton 279 + InactiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 280 + ActiveIcon={<IconMdiPencilOutline className="w-6 h-6" />} 281 + //active={true} 282 + onClickCallbback={() => setPostOpen(true)} 283 + text="Post" 284 + /> 285 + </div> 286 + {/* <Link 190 287 to="/" 191 288 className={ 192 289 `py-2 px-4 hover:bg-gray-100 dark:hover:bg-gray-900 text-xl flex items-center gap-3 ` + ··· 260 357 <IconMaterialSymbolsAccountCircleOutline width={28} height={28} /> 261 358 ) : ( 262 359 <IconMaterialSymbolsAccountCircle width={28} height={28} /> 263 - ) 264 - } 360 + )} 265 361 <span>Profile</span> 266 362 </button> 267 363 <Link ··· 276 372 <IconMaterialSymbolsSettings width={28} height={28} /> 277 373 )} 278 374 <span>Settings</span> 279 - </Link> 280 - <button 375 + </Link> */} 376 + {/* <button 281 377 className="mt-4 w-full flex items-center justify-center gap-3 py-3 px-0 mb-3 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100 text-xl font-bold rounded-full transition-colors shadow" 282 378 onClick={() => setPostOpen(true)} 283 379 type="button" ··· 288 384 className="text-gray-600 dark:text-gray-400" 289 385 /> 290 386 <span>Post</span> 291 - </button> 387 + </button> */} 292 388 <div className="flex-1"></div> 293 389 <a 294 390 href="https://tangled.sh/@whey.party/red-dwarf" ··· 319 415 </div> 320 416 </nav> 321 417 322 - <button 323 - className="lg:hidden fixed bottom-20 right-6 z-50 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 text-blue-600 dark:text-blue-400 rounded-full shadow-lg w-16 h-16 flex items-center justify-center border-4 border-white dark:border-gray-950 transition-all" 324 - style={{ boxShadow: "0 4px 24px 0 rgba(0,0,0,0.12)" }} 325 - onClick={() => setPostOpen(true)} 326 - type="button" 327 - aria-label="Create Post" 328 - > 329 - <IconMdiPencilOutline 330 - width={24} 331 - height={24} 332 - className="text-gray-600 dark:text-gray-400" 333 - /> 334 - </button> 418 + {agent?.did && ( 419 + <button 420 + className="lg:hidden fixed bottom-22 right-4 z-50 bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 rounded-2xl w-14 h-14 flex items-center justify-center transition-all" 421 + style={{ boxShadow: "0 4px 24px 0 rgba(0,0,0,0.12)" }} 422 + onClick={() => setPostOpen(true)} 423 + type="button" 424 + aria-label="Create Post" 425 + > 426 + <IconMdiPencilOutline 427 + width={24} 428 + height={24} 429 + className="text-gray-600 dark:text-gray-400" 430 + /> 431 + </button> 432 + )} 335 433 336 434 <main className="w-full max-w-[600px] lg:border-x border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-950 pb-16 lg:pb-0"> 337 - <div className="lg:hidden flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-950"> 338 - <div className="flex items-center gap-2"> 339 - <img 340 - src="/redstar.png" 341 - alt="Red Dwarf Logo" 342 - className="w-6 h-6" 343 - /> 344 - <span className="font-bold text-lg text-gray-900 dark:text-gray-100"> 345 - Red Dwarf{" "} 346 - {/* <span className="text-gray-500 dark:text-gray-400 text-sm"> 347 - lite 348 - </span> */} 349 - </span> 350 - </div> 351 - <div className="flex items-center gap-2"> 352 - <Login compact={true} /> 353 - </div> 354 - </div> 355 - 356 435 {children} 357 436 </main> 358 437 ··· 368 447 </aside> 369 448 </div> 370 449 371 - <nav className="lg:hidden fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-950 border-t border-gray-200 dark:border-gray-700 z-40"> 372 - <div className="flex justify-around items-center py-2"> 373 - <Link 374 - to="/" 375 - className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 376 - isHome 377 - ? "text-gray-900 dark:text-gray-100" 378 - : "text-gray-600 dark:text-gray-400" 379 - }`} 380 - > 381 - {!isHome ? ( 382 - <IconMaterialSymbolsHomeOutline width={24} height={24} /> 383 - ) : ( 384 - <IconMaterialSymbolsHome width={24} height={24} /> 385 - )} 386 - <span className="text-xs mt-1">Home</span> 387 - </Link> 388 - <Link 389 - to="/search" 390 - className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 391 - location.pathname.startsWith("/search") 392 - ? "text-gray-900 dark:text-gray-100" 393 - : "text-gray-600 dark:text-gray-400" 394 - }`} 395 - > 396 - {!location.pathname.startsWith("/search") ? ( 397 - <IconMaterialSymbolsSearch width={24} height={24} /> 398 - ) : ( 399 - <IconMaterialSymbolsSearch width={24} height={24} /> 400 - )} 401 - <span className="text-xs mt-1">Search</span> 402 - </Link> 403 - <Link 404 - to="/notifications" 405 - className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 406 - isNotifications 407 - ? "text-gray-900 dark:text-gray-100" 408 - : "text-gray-600 dark:text-gray-400" 409 - }`} 410 - > 411 - {!isNotifications ? ( 412 - <IconMaterialSymbolsNotificationsOutline width={24} height={24} /> 413 - ) : ( 414 - <IconMaterialSymbolsNotifications width={24} height={24} /> 415 - )} 416 - <span className="text-xs mt-1">Notifications</span> 417 - </Link> 418 - <button 419 - className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 420 - isProfile 421 - ? "text-gray-900 dark:text-gray-100" 422 - : "text-gray-600 dark:text-gray-400" 423 - }`} 424 - onClick={() => { 425 - if (authed && agent && agent.assertDid) { 426 - //window.location.href = `/profile/${agent.assertDid}`; 450 + {agent?.did ? ( 451 + <nav className="lg:hidden fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-950 border-t border-gray-200 dark:border-gray-700 z-40"> 452 + <div className="flex justify-around items-center p-2"> 453 + <MaterialNavItem 454 + small 455 + InactiveIcon={ 456 + <IconMaterialSymbolsHomeOutline className="w-6 h-6" /> 457 + } 458 + ActiveIcon={<IconMaterialSymbolsHome className="w-6 h-6" />} 459 + active={isHome} 460 + onClickCallbback={() => 461 + navigate({ 462 + to: "/", 463 + //params: { did: agent.assertDid }, 464 + }) 465 + } 466 + text="Home" 467 + /> 468 + {/* <Link 469 + to="/" 470 + className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 471 + isHome 472 + ? "text-gray-900 dark:text-gray-100" 473 + : "text-gray-600 dark:text-gray-400" 474 + }`} 475 + > 476 + {!isHome ? ( 477 + <IconMaterialSymbolsHomeOutline width={24} height={24} /> 478 + ) : ( 479 + <IconMaterialSymbolsHome width={24} height={24} /> 480 + )} 481 + <span className="text-xs mt-1">Home</span> 482 + </Link> */} 483 + <MaterialNavItem 484 + small 485 + InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 486 + ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />} 487 + active={location.pathname.startsWith("/search")} 488 + onClickCallbback={() => 489 + navigate({ 490 + to: "/search", 491 + //params: { did: agent.assertDid }, 492 + }) 493 + } 494 + text="Search" 495 + /> 496 + {/* <Link 497 + to="/search" 498 + className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 499 + location.pathname.startsWith("/search") 500 + ? "text-gray-900 dark:text-gray-100" 501 + : "text-gray-600 dark:text-gray-400" 502 + }`} 503 + > 504 + {!location.pathname.startsWith("/search") ? ( 505 + <IconMaterialSymbolsSearch width={24} height={24} /> 506 + ) : ( 507 + <IconMaterialSymbolsSearch width={24} height={24} /> 508 + )} 509 + <span className="text-xs mt-1">Search</span> 510 + </Link> */} 511 + <MaterialNavItem 512 + small 513 + InactiveIcon={ 514 + <IconMaterialSymbolsNotificationsOutline className="w-6 h-6" /> 515 + } 516 + ActiveIcon={ 517 + <IconMaterialSymbolsNotifications className="w-6 h-6" /> 518 + } 519 + active={isNotifications} 520 + onClickCallbback={() => 521 + navigate({ 522 + to: "/notifications", 523 + //params: { did: agent.assertDid }, 524 + }) 525 + } 526 + text="Notifications" 527 + /> 528 + {/* <Link 529 + to="/notifications" 530 + className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 531 + isNotifications 532 + ? "text-gray-900 dark:text-gray-100" 533 + : "text-gray-600 dark:text-gray-400" 534 + }`} 535 + > 536 + {!isNotifications ? ( 537 + <IconMaterialSymbolsNotificationsOutline 538 + width={24} 539 + height={24} 540 + /> 541 + ) : ( 542 + <IconMaterialSymbolsNotifications width={24} height={24} /> 543 + )} 544 + <span className="text-xs mt-1">Notifications</span> 545 + </Link> */} 546 + <MaterialNavItem 547 + small 548 + InactiveIcon={ 549 + <IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" /> 550 + } 551 + ActiveIcon={ 552 + <IconMaterialSymbolsAccountCircle className="w-6 h-6" /> 553 + } 554 + active={isProfile ?? false} 555 + onClickCallbback={() => { 556 + if (authed && agent && agent.assertDid) { 557 + //window.location.href = `/profile/${agent.assertDid}`; 558 + navigate({ 559 + to: "/profile/$did", 560 + params: { did: agent.assertDid }, 561 + }); 562 + } 563 + }} 564 + text="Profile" 565 + /> 566 + {/* <button 567 + className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 568 + isProfile 569 + ? "text-gray-900 dark:text-gray-100" 570 + : "text-gray-600 dark:text-gray-400" 571 + }`} 572 + onClick={() => { 573 + if (authed && agent && agent.assertDid) { 574 + //window.location.href = `/profile/${agent.assertDid}`; 575 + navigate({ 576 + to: "/profile/$did", 577 + params: { did: agent.assertDid }, 578 + }); 579 + } 580 + }} 581 + type="button" 582 + > 583 + <IconMaterialSymbolsAccountCircleOutline width={24} height={24} /> 584 + <span className="text-xs mt-1">Profile</span> 585 + </button> */} 586 + <MaterialNavItem 587 + small 588 + InactiveIcon={ 589 + <IconMaterialSymbolsSettingsOutline className="w-6 h-6" /> 590 + } 591 + ActiveIcon={<IconMaterialSymbolsSettings className="w-6 h-6" />} 592 + active={location.pathname.startsWith("/settings")} 593 + onClickCallbback={() => 427 594 navigate({ 428 - to: "/profile/$did", 429 - params: { did: agent.assertDid }, 430 - }); 595 + to: "/settings", 596 + //params: { did: agent.assertDid }, 597 + }) 431 598 } 432 - }} 433 - type="button" 434 - > 435 - <IconMaterialSymbolsAccountCircleOutline width={24} height={24} /> 436 - <span className="text-xs mt-1">Profile</span> 437 - </button> 438 - <Link 439 - to="/settings" 440 - className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 441 - location.pathname.startsWith("/settings") 442 - ? "text-gray-900 dark:text-gray-100" 443 - : "text-gray-600 dark:text-gray-400" 444 - }`} 445 - > 446 - {!location.pathname.startsWith("/settings") ? ( 447 - <IconMaterialSymbolsSettingsOutline width={24} height={24} /> 448 - ) : ( 449 - <IconMaterialSymbolsSettings width={24} height={24} /> 450 - )} 451 - <span className="text-xs mt-1">Settings</span> 452 - </Link> 599 + text="Settings" 600 + /> 601 + {/* <Link 602 + to="/settings" 603 + className={`flex flex-col items-center py-2 px-3 rounded-lg transition-colors flex-1 ${ 604 + location.pathname.startsWith("/settings") 605 + ? "text-gray-900 dark:text-gray-100" 606 + : "text-gray-600 dark:text-gray-400" 607 + }`} 608 + > 609 + {!location.pathname.startsWith("/settings") ? ( 610 + <IconMaterialSymbolsSettingsOutline width={24} height={24} /> 611 + ) : ( 612 + <IconMaterialSymbolsSettings width={24} height={24} /> 613 + )} 614 + <span className="text-xs mt-1">Settings</span> 615 + </Link> */} 616 + </div> 617 + </nav> 618 + ) : ( 619 + <div className="lg:hidden flex items-center fixed bottom-0 left-0 right-0 justify-between px-4 py-3 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-950 z-10"> 620 + <div className="flex items-center gap-2"> 621 + <img src="/redstar.png" alt="Red Dwarf Logo" className="w-6 h-6" /> 622 + <span className="font-bold text-lg text-gray-900 dark:text-gray-100"> 623 + Red Dwarf{" "} 624 + {/* <span className="text-gray-500 dark:text-gray-400 text-sm"> 625 + lite 626 + </span> */} 627 + </span> 628 + </div> 629 + <div className="flex items-center gap-2"> 630 + <Login compact={true} popup={true} /> 631 + </div> 453 632 </div> 454 - </nav> 633 + )} 455 634 456 - <TanStackRouterDevtools position="bottom-right" /> 635 + <TanStackRouterDevtools position="bottom-left" /> 457 636 <Scripts /> 458 637 </> 459 638 ); 460 639 } 640 + 641 + function MaterialNavItem({ 642 + InactiveIcon, 643 + ActiveIcon, 644 + text, 645 + active, 646 + onClickCallbback, 647 + small, 648 + }: { 649 + InactiveIcon: React.ReactElement; 650 + ActiveIcon: React.ReactElement; 651 + text: string; 652 + active: boolean; 653 + onClickCallbback: () => void; 654 + small?: boolean; 655 + }) { 656 + if (small) 657 + return ( 658 + <button 659 + className={`flex flex-col items-center rounded-lg transition-colors flex-1 gap-1 ${ 660 + active 661 + ? "text-gray-900 dark:text-gray-100" 662 + : "text-gray-600 dark:text-gray-400" 663 + }`} 664 + onClick={() => { 665 + onClickCallbback(); 666 + }} 667 + > 668 + <div 669 + className={`px-4 py-1 rounded-full flex items-center justify-center ${active ? " bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 hover:dark:bg-gray-700" : "hover:bg-gray-50 hover:dark:bg-gray-900"}`} 670 + > 671 + {active ? ActiveIcon : InactiveIcon} 672 + </div> 673 + <span 674 + className={`text-[12.8px] text-roboto ${active ? "font-medium" : ""}`} 675 + > 676 + {text} 677 + </span> 678 + </button> 679 + ); 680 + 681 + return ( 682 + <button 683 + className={`flex flex-row h-12 min-h-12 max-h-12 px-4 py-0.5 w-full items-center rounded-full transition-colors flex-1 gap-1 ${ 684 + active 685 + ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600" 686 + : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800" 687 + }`} 688 + onClick={() => { 689 + onClickCallbback(); 690 + }} 691 + > 692 + <div className={`mr-4 ${active ? " " : " "}`}> 693 + {active ? ActiveIcon : InactiveIcon} 694 + </div> 695 + <span 696 + className={`text-[16px] text-roboto ${active ? "font-medium" : ""}`} 697 + > 698 + {text} 699 + </span> 700 + </button> 701 + ); 702 + } 703 + 704 + function MaterialPillButton({ 705 + InactiveIcon, 706 + ActiveIcon, 707 + text, 708 + //active, 709 + onClickCallbback, 710 + small, 711 + }: { 712 + InactiveIcon: React.ReactElement; 713 + ActiveIcon: React.ReactElement; 714 + text: string; 715 + //active: boolean; 716 + onClickCallbback: () => void; 717 + small?: boolean; 718 + }) { 719 + const active = false; 720 + return ( 721 + <button 722 + className={`flex border border-gray-400 dark:border-gray-400 flex-row h-12 min-h-12 max-h-12 px-4 py-0.5 items-center rounded-full transition-colors gap-1 ${ 723 + active 724 + ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600" 725 + : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800" 726 + }`} 727 + onClick={() => { 728 + onClickCallbback(); 729 + }} 730 + > 731 + <div className={`mr-2 ${active ? " " : " "}`}> 732 + {active ? ActiveIcon : InactiveIcon} 733 + </div> 734 + <span 735 + className={`text-[16px] text-roboto ${active ? "font-medium" : ""}`} 736 + > 737 + {text} 738 + </span> 739 + </button> 740 + ); 741 + }
+23 -13
src/routes/index.tsx
··· 3 3 import * as React from "react"; 4 4 import { useEffect, useLayoutEffect } from "react"; 5 5 6 + import { Header } from "~/components/Header"; 6 7 import { InfiniteCustomFeed } from "~/components/InfiniteCustomFeed"; 7 8 import { useAuth } from "~/providers/UnifiedAuthProvider"; 8 9 import { ··· 353 354 <div 354 355 className={`relative flex flex-col divide-y divide-gray-200 dark:divide-gray-800 ${hidden && "hidden"}`} 355 356 > 356 - <div className="flex items-center gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700 overflow-x-auto overflow-y-hidden scroll-thin"> 357 - {savedFeeds.length > 0 ? ( 358 - savedFeeds.map((item: any, idx: number) => { 357 + {savedFeeds.length > 0 ? ( 358 + <div className="flex items-center px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700 overflow-x-auto overflow-y-hidden scroll-thin"> 359 + {savedFeeds.map((item: any, idx: number) => { 359 360 const label = item.value.split("/").pop() || item.value; 360 361 const isActive = selectedFeed === item.value; 361 362 return ( ··· 363 364 key={item.value || idx} 364 365 className={`px-3 py-1 rounded-full whitespace-nowrap font-medium transition-colors ${ 365 366 isActive 366 - ? "bg-gray-500 text-white" 367 - : item.pinned 368 - ? "bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200" 369 - : "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-200" 367 + ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600" 368 + : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800" 369 + // ? "bg-gray-500 text-white" 370 + // : item.pinned 371 + // ? "bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200" 372 + // : "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-200" 370 373 }`} 371 374 onClick={() => setSelectedFeed(item.value)} 372 375 title={item.value} 373 376 > 374 377 {label} 375 378 {item.pinned && ( 376 - <span className="ml-1 text-xs text-gray-700 dark:text-gray-200"> 379 + <span 380 + className={`ml-1 text-xs ${ 381 + isActive 382 + ? "text-gray-900 dark:text-gray-100 hover:bg-gray-300 dark:bg-gray-700 bg-gray-200 hover:dark:bg-gray-600" 383 + : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 hover:dark:bg-gray-800" 384 + }`} 385 + > 377 386 378 387 </span> 379 388 )} 380 389 </button> 381 390 ); 382 - }) 383 - ) : ( 384 - <span className="text-xl font-bold ml-2">Home</span> 385 - )} 386 - </div> 391 + })} 392 + </div> 393 + ) : ( 394 + // <span className="text-xl font-bold ml-2">Home</span> 395 + <Header title="Home" /> 396 + )} 387 397 {/* {isFeedLoading && <div className="p-4 text-gray-500">Loading...</div>} 388 398 {feedError && <div className="p-4 text-red-500">{feedError.message}</div>} 389 399 {!isFeedLoading && !feedError && feed.length === 0 && (
+14 -3
src/routes/profile.$did/index.tsx
··· 1 1 import { useQueryClient } from "@tanstack/react-query"; 2 - import { createFileRoute, Link } from "@tanstack/react-router"; 2 + import { createFileRoute } from "@tanstack/react-router"; 3 3 import React from "react"; 4 4 5 + import { Header } from "~/components/Header"; 5 6 import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; 6 7 import { useAuth } from "~/providers/UnifiedAuthProvider"; 7 8 import { toggleFollow, useGetFollowState } from "~/utils/followState"; ··· 104 105 105 106 return ( 106 107 <> 107 - <div className="flex gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700"> 108 + <Header 109 + title={`Profile`} 110 + backButtonCallback={() => { 111 + if (window.history.length > 1) { 112 + window.history.back(); 113 + } else { 114 + window.location.assign("/"); 115 + } 116 + }} 117 + /> 118 + {/* <div className="flex gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700"> 108 119 <Link 109 120 to=".." 110 121 className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg" ··· 121 132 122 133 </Link> 123 134 <span className="text-xl font-bold ml-2">Profile</span> 124 - </div> 135 + </div> */} 125 136 126 137 {/* Profile Header */} 127 138 <div className="w-full max-w-2xl mx-auto overflow-hidden relative bg-gray-100 dark:bg-gray-900">
+14 -31
src/routes/profile.$did/post.$rkey.tsx
··· 2 2 import { createFileRoute } from "@tanstack/react-router"; 3 3 import React, { useLayoutEffect } from "react"; 4 4 5 + import { Header } from "~/components/Header"; 5 6 import { UniversalPostRendererATURILoader } from "~/components/UniversalPostRenderer"; 6 7 //import { usePersistentStore } from '~/providers/PersistentStoreProvider'; 7 8 import { ··· 296 297 297 298 return ( 298 299 <> 299 - <div className="flex items-center gap-2 px-4 py-2 h-[52px] sticky top-0 bg-white dark:bg-gray-950 z-10 border-b border-gray-200 dark:border-gray-700"> 300 - {!nopics ? ( 301 - <button 302 - //to=".." 303 - className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg" 304 - onClick={(e) => { 305 - e.preventDefault(); 306 - if (window.history.length > 1) { 307 - window.history.back(); 308 - } else { 309 - window.location.assign("/"); 300 + <Header 301 + title={`Post`} 302 + backButtonCallback={ 303 + nopics 304 + ? nopics 305 + : () => { 306 + if (window.history.length > 1) { 307 + window.history.back(); 308 + } else { 309 + window.location.assign("/"); 310 + } 310 311 } 311 - }} 312 - aria-label="Go back" 313 - > 314 - 315 - </button> 316 - ) : ( 317 - <button 318 - //to=".." 319 - className="px-3 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-900 font-bold text-lg" 320 - onClick={(e) => { 321 - e.preventDefault(); 322 - nopics(); 323 - }} 324 - aria-label="Go back" 325 - > 326 - 327 - </button> 328 - )} 329 - <span className="text-xl font-bold ml-2">Post</span> 330 - </div> 312 + } 313 + /> 331 314 332 315 {parentsLoading && ( 333 316 <div className="text-center text-gray-500 dark:text-gray-400 flex flex-row">
+8
src/styles/app.css
··· 1 + @import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto:ital,wght@0,100..900;1,100..900&family=Spectral+SC:wght@500&display=swap'); 1 2 @import "tailwindcss"; 2 3 3 4 /* @theme { ··· 78 79 color: rgb(29, 122, 242); 79 80 word-break: break-all; 80 81 } 82 + } 83 + 84 + .font-inter { 85 + font-family: "Inter", sans-serif; 86 + } 87 + .font-roboto { 88 + font-family: "Roboto", sans-serif; 81 89 }
+2 -1
vite.config.ts
··· 39 39 IconsResolver({ 40 40 prefix: 'Icon', 41 41 extension: 'jsx', 42 + enabledCollections: ['mdi','material-symbols'], 42 43 }), 43 44 ], 44 45 dts: 'src/auto-imports.d.ts', 45 46 }), 46 47 Icons({ 47 - autoInstall: true, 48 + //autoInstall: true, 48 49 compiler: 'jsx', 49 50 jsx: 'react' 50 51 }),