A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1import { useState } from 'react';
2import { useAuth } from '../lib/auth/useAuth';
3import { displayNameFor, authorPath } from '../lib/auth/profile';
4import { appBarNav, type AppBarContext } from '../lib/auth/nav';
5import { skypressMark } from '../lib/brand/skypress-mark';
6
7/** SkyPress mark — shares the inline-SVG source with Logo.astro (Astro components
8 * can't render in a React island). `currentColor` lets it follow the --sun token. */
9function LogoMark() {
10 return (
11 <span
12 className="app-bar__mark"
13 // eslint-disable-next-line react/no-danger -- static, app-authored SVG markup; no user input
14 dangerouslySetInnerHTML={ { __html: skypressMark( 24 ) } }
15 />
16 );
17}
18
19function NavIcon( { name }: { name: 'feather' | 'publications' } ) {
20 if ( name === 'feather' ) {
21 return (
22 <svg className="app-bar__navicon" viewBox="0 0 24 24" width={ 18 } height={ 18 } fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
23 <path d="M20.24 12.24a6 6 0 0 0-8.49-8.49L5 10.5V19h8.5z" />
24 <line x1="16" y1="8" x2="2" y2="22" />
25 <line x1="17.5" y1="15" x2="9" y2="15" />
26 </svg>
27 );
28 }
29 return (
30 <svg className="app-bar__navicon" viewBox="0 0 24 24" width={ 18 } height={ 18 } fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
31 <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
32 <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
33 </svg>
34 );
35}
36
37/**
38 * The shared top bar for the editor + dashboard islands. Logo on the left;
39 * contextual nav + account + sign-out on the right. Rendered inside AuthProvider
40 * in every auth state: logo-only while loading or signed out, + contextual nav,
41 * account, and sign-out once signed in. The cross-link to Publications / Write is
42 * gated on auth so signed-out visitors aren't pointed at editor-only routes.
43 */
44export default function AppBar( { current }: { current: AppBarContext } ) {
45 const { status, handle, displayName, avatar, did, signOut } = useAuth();
46 const [ avatarOk, setAvatarOk ] = useState( true );
47 const nav = appBarNav( current );
48 const signedIn = status === 'signed-in' && Boolean( did );
49
50 const viewerName = did
51 ? displayNameFor( { did, handle, displayName, avatar } )
52 : '';
53 const profileHref = authorPath( handle );
54
55 return (
56 <header className="app-bar">
57 <a className="app-bar__home" href="/" aria-label="SkyPress home">
58 <LogoMark />
59 <span className="app-bar__word">SkyPress</span>
60 </a>
61
62 <span className="app-bar__spacer" />
63
64 { signedIn && (
65 <a className="app-bar__nav" href={ nav.href }>
66 <NavIcon name={ nav.icon } />
67 { nav.label }
68 </a>
69 ) }
70
71 { signedIn && (
72 <>
73 <IdentityBlock
74 href={ profileHref }
75 name={ viewerName }
76 handle={ handle }
77 avatar={ avatar }
78 avatarOk={ avatarOk }
79 onAvatarError={ () => setAvatarOk( false ) }
80 />
81 <button type="button" className="app-bar__signout" onClick={ () => void signOut() }>
82 Sign out
83 </button>
84 </>
85 ) }
86 </header>
87 );
88}
89
90/** Avatar + name + @handle. The whole block links to the public author page when a handle is known. */
91function IdentityBlock( {
92 href,
93 name,
94 handle,
95 avatar,
96 avatarOk,
97 onAvatarError,
98}: {
99 href: string | null;
100 name: string;
101 handle: string | null;
102 avatar: string | null;
103 avatarOk: boolean;
104 onAvatarError: () => void;
105} ) {
106 const inner = (
107 <>
108 { avatar && avatarOk ? (
109 <img
110 className="app-bar__avatar"
111 src={ avatar }
112 alt=""
113 width={ 30 }
114 height={ 30 }
115 onError={ onAvatarError }
116 />
117 ) : (
118 <span className="app-bar__avatar app-bar__avatar--fallback" aria-hidden="true">
119 { name.charAt( 0 ).toUpperCase() }
120 </span>
121 ) }
122 <span className="app-bar__who">
123 <strong className="app-bar__name">{ name }</strong>
124 { handle && <span className="app-bar__handle">@{ handle }</span> }
125 </span>
126 </>
127 );
128
129 return href ? (
130 <a className="app-bar__identity" href={ href }>
131 { inner }
132 </a>
133 ) : (
134 <span className="app-bar__identity">{ inner }</span>
135 );
136}