A web app for writing and sharing 301+ character Bluesky posts.

Pop up Login and Move to Bsky Post Lexicon

Minito 426b63cc 2dd1321b

+87 -25
+80 -7
src/routes/index.tsx
··· 1 1 import { createFileRoute } from '@tanstack/react-router' 2 - import { useState } from 'react' 2 + import { useState, useEffect } from 'react' 3 3 import '../App.css' 4 4 import Login from '../components/Login' 5 5 import { useAuth } from "../providers/UnifiedAuthProvider"; 6 6 import { AtUri } from '@atproto/api'; 7 + import type { text } from 'node:stream/consumers'; 7 8 8 9 export const Route = createFileRoute('/')({ 9 10 component: RouteComponent, 10 11 }) 11 12 12 13 function RouteComponent() { 13 - const { agent } = useAuth(); 14 + const { agent, status } = useAuth(); 14 15 15 16 const [postText, setPostText] = useState('') 17 + const [showLoginPrompt, setShowLoginPrompt] = useState(false) 16 18 const [showSuccessPopup, setShowSuccessPopup] = useState(false) 17 19 const [postUri, setPostUri] = useState('') 18 20 const charCount = postText.length 19 21 20 22 const handleSubmit = async () => { 21 - if (!agent) { 22 - console.error("Agent not available"); 23 + if (!agent || status !== 'signedIn') { 24 + setShowLoginPrompt(true); 23 25 return; 24 26 } 25 27 ··· 27 29 // Create the record and let the server generate the rkey (TID) 28 30 const response = await agent.com.atproto.repo.createRecord({ 29 31 repo: agent.assertDid, 30 - collection: 'app.skeetlonger.post', 32 + collection: 'app.bsky.feed.post', 31 33 record: { 32 - post: postText, 34 + text: postText, 35 + $type: "app.bsky.feed.post", 33 36 createdAt: new Date().toISOString() 34 - } 37 + }, 38 + validate: false 35 39 }); 36 40 37 41 // Parse the URI to extract the rkey ··· 52 56 } 53 57 }; 54 58 59 + useEffect(() => { 60 + if (status === 'signedIn' && agent && postText.length > 0 && showLoginPrompt) { 61 + setShowLoginPrompt(false); 62 + handleSubmit(); 63 + } 64 + }, [status, agent]); 65 + 55 66 return ( 56 67 <> 57 68 <div style={{ position: 'fixed', top: '1rem', right: '1rem', zIndex: 9999 }}> 58 69 <Login compact={true} /> 59 70 </div> 71 + 72 + {/* Login Prompt Popup */} 73 + {showLoginPrompt && ( 74 + <div 75 + style={{ 76 + position: 'fixed', 77 + top: 0, 78 + left: 0, 79 + right: 0, 80 + bottom: 0, 81 + backgroundColor: 'rgba(0, 0, 0, 0.5)', 82 + display: 'flex', 83 + alignItems: 'center', 84 + justifyContent: 'center', 85 + zIndex: 10000 86 + }} 87 + onClick={() => setShowLoginPrompt(false)} 88 + > 89 + <div 90 + style={{ 91 + backgroundColor: '#1a1a1a', 92 + borderRadius: '0.75rem', 93 + padding: '2rem', 94 + maxWidth: '32rem', 95 + width: '90%', 96 + border: '1px solid #374151', 97 + boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.3)' 98 + }} 99 + onClick={(e) => e.stopPropagation()} 100 + > 101 + <h2 style={{ fontSize: '1.5rem', fontWeight: 'bold', color: '#f59e0b', marginBottom: '1rem', textAlign: 'center' }}> 102 + Login Required 103 + </h2> 104 + <p style={{ fontSize: '1rem', color: '#d1d5db', marginBottom: '1.5rem', textAlign: 'center', lineHeight: '1.5' }}> 105 + You need to log in to post your content. Your post will be submitted automatically after you successfully log in. 106 + </p> 107 + <div style={{ backgroundColor: '#111827', padding: '1.5rem', borderRadius: '0.5rem', border: '1px solid #374151' }}> 108 + <Login compact={false} /> 109 + </div> 110 + <div style={{ display: 'flex', justifyContent: 'center', marginTop: '1rem' }}> 111 + <button 112 + onClick={() => setShowLoginPrompt(false)} 113 + style={{ 114 + padding: '0.625rem 1.5rem', 115 + borderRadius: '0.5rem', 116 + backgroundColor: '#374151', 117 + color: 'white', 118 + fontWeight: '600', 119 + border: 'none', 120 + cursor: 'pointer', 121 + transition: 'background-color 0.2s', 122 + fontSize: '0.875rem' 123 + }} 124 + onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4b5563'} 125 + onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#374151'} 126 + > 127 + Cancel 128 + </button> 129 + </div> 130 + </div> 131 + </div> 132 + )} 60 133 61 134 {/* Success Popup */} 62 135 {showSuccessPopup && (
+7 -18
src/routes/post/$uri.tsx
··· 43 43 // Parse the AT URI 44 44 const atUri = new AtUri(decodedUri) 45 45 46 - // Validate it's a skeetlonger post 47 - if (atUri.collection !== 'app.skeetlonger.post') { 48 - setError("Invalid post URI - not a SkeetLonger post") 46 + // Validate it's a bsky post 47 + if (atUri.collection !== 'app.bsky.feed.post') { 48 + setError("Invalid post URI - not a bsky post") 49 49 setLoading(false) 50 50 return 51 51 } 52 52 53 - // // We need to resolve the DID to find the user's PDS 54 - // const resolverAgent = new AtpAgent({ service: 'https://bsky.social' }) 55 - // const didDoc = await resolverAgent.com.atproto.identity.resolveHandle({ 56 - // handle: atUri.host 57 - // }) 58 - 59 53 if (!isAtprotoDid(atUri.host)) { 60 54 throw new Error("Not a valid DID identifier"); 61 55 } ··· 63 57 const doc = await didDocumentResolver.resolve(atUri.host); 64 58 65 59 const pdsUrl = getPdsEndpoint(doc); 66 - 67 - // // Get the PDS endpoint from the DID document 68 - // // For now, we'll need to make a separate call to get the service endpoint 69 - //const pdsUrl = `https://pds.minito.dev` // This should be resolved from the DID document 70 - 71 - // Create an agent pointing to the user's PDS 60 + 72 61 if (!pdsUrl) { 73 62 throw new Error("No PDS found"); 74 63 } ··· 76 65 const fetchAgent = new AtpAgent({ service: pdsUrl }) 77 66 78 67 // Fetch the record from their PDS 68 + // This should be more involved, it should check a db, then something like constellation 69 + // if neither has the record, then we hit the PDS. 79 70 const recordResponse = await fetchAgent.com.atproto.repo.getRecord({ 80 71 repo: atUri.host, 81 72 collection: atUri.collection, ··· 176 167 marginBottom: '1rem', 177 168 textAlign: 'left' 178 169 }}> 179 - {post.value.post} 170 + {post.value.text} 180 171 </div> 181 172 182 173 {/* Post Metadata */} ··· 202 193 </p> 203 194 </div> 204 195 </div> 205 - 206 196 207 - 208 197 <style>{` 209 198 @keyframes spin { 210 199 to { transform: rotate(360deg); }