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

Checking in UI

Minito 9830f505 25f6c49d

+76 -43
+6 -6
public/client-metadata.json
··· 1 1 { 2 - "client_id": "https://local3768forumtest.whey.party/client-metadata.json", 2 + "client_id": "https://7d3e-198-51-100-42.ngrok-free.app/client-metadata.json", 3 3 "client_name": "SkeetLonger", 4 - "client_uri": "https://local3768forumtest.whey.party", 5 - "logo_uri": "https://local3768forumtest.whey.party/logo192.png", 6 - "tos_uri": "https://local3768forumtest.whey.party/terms-of-service", 7 - "policy_uri": "https://local3768forumtest.whey.party/privacy-policy", 4 + "client_uri": "https://7d3e-198-51-100-42.ngrok-free.app", 5 + "logo_uri": "https://7d3e-198-51-100-42.ngrok-free.app/logo192.png", 6 + "tos_uri": "https://7d3e-198-51-100-42.ngrok-free.app/terms-of-service", 7 + "policy_uri": "https://7d3e-198-51-100-42.ngrok-free.app/privacy-policy", 8 8 "redirect_uris": [ 9 - "https://local3768forumtest.whey.party/callback" 9 + "https://7d3e-198-51-100-42.ngrok-free.app/callback" 10 10 ], 11 11 "scope": "atproto transition:generic", 12 12 "grant_types": [
+58 -30
src/App.tsx
··· 1 1 import { useState } from 'react' 2 - import reactLogo from './assets/react.svg' 3 - import viteLogo from '/vite.svg' 4 2 import './App.css' 5 3 import Login from './components/Login' 6 4 import { UnifiedAuthProvider } from './providers/UnifiedAuthProvider' 7 5 8 6 function App() { 9 - const [count, setCount] = useState(0) 7 + const [postText, setPostText] = useState('') 8 + const charCount = postText.length 9 + 10 + const handleSubmit = async (e: React.FormEvent) => { 11 + e.preventDefault(); 12 + // setError(null); 13 + // try { 14 + // localStorage.setItem("lastHandle", user); 15 + // await loginWithPassword(user, password, `https://${serviceURL}`); 16 + // } catch (err) { 17 + // setError("Login failed. Check your handle and App Password."); 18 + // } 19 + }; 10 20 11 21 return ( 12 - <> 13 - {/* <div> 14 - <a href="https://vite.dev" target="_blank"> 15 - <img src={viteLogo} className="logo" alt="Vite logo" /> 16 - </a> 17 - <a href="https://react.dev" target="_blank"> 18 - <img src={reactLogo} className="logo react" alt="React logo" /> 19 - </a> 20 - </div> */} 21 - <h1>SkeetLonger</h1> 22 - 23 - <div className="card"> 22 + <UnifiedAuthProvider> 23 + <div style={{ position: 'fixed', top: '1rem', right: '1rem', zIndex: 9999 }}> 24 + <Login compact={true} /> 25 + </div> 24 26 25 - <UnifiedAuthProvider> 26 - <Login compact={false}></Login> 27 - </UnifiedAuthProvider> 28 - 27 + <div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '5rem 1rem', backgroundColor: '#242424' }}> 28 + <div style={{ width: '100%', maxWidth: '72rem' }}> 29 + {/* Title */} 30 + <h1 style={{ fontSize: '2.25rem', fontWeight: 'bold', textAlign: 'center', marginBottom: '0.5rem', color: '#f3f4f6' }}> 31 + SkeetLonger 32 + </h1> 33 + <p style={{ textAlign: 'center', marginBottom: '2rem', color: '#9ca3af' }}> 34 + Post longer content to Bluesky 35 + </p> 29 36 30 - {/* <button onClick={() => setCount((count) => count + 1)}> 31 - count is {count} 32 - </button> 33 - <p> 34 - Edit <code>src/App.tsx</code> and save to test HMR 35 - </p> */} 37 + {/* Text Editor Card */} 38 + <div style={{ padding: '1.5rem', borderRadius: '0.75rem', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)', border: '1px solid #e5e7eb', backgroundColor: '#1a1a1a', borderColor: '#374151' }}> 39 + <textarea 40 + value={postText} 41 + onChange={(e) => setPostText(e.target.value)} 42 + placeholder="What's on your mind? Write as much as you'd like..." 43 + style={{ width: '32rem', height: '32rem', padding: '0.75rem 1rem', borderRadius: '0.5rem', border: '1px solid #4b5563', fontSize: '1rem', lineHeight: '1.75', resize: 'none', backgroundColor: '#111827', color: '#f3f4f6', boxSizing: 'border-box' }} 44 + className="placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500" 45 + /> 46 + 47 + {/* Footer with character count and post button */} 48 + <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: '1rem' }}> 49 + <div style={{ fontSize: '0.875rem' }}> 50 + <span style={{ fontWeight: '500', color: charCount > 300 ? '#3b82f6' : '#9ca3af' }}> 51 + {charCount} characters 52 + </span> 53 + </div> 54 + 55 + <button 56 + type='submit' 57 + disabled={charCount < 300} 58 + style={{ padding: '0.625rem 1.5rem', borderRadius: '0.5rem', fontSize: '0.875rem', fontWeight: '600', border: 'none', cursor: charCount === 0 ? 'not-allowed' : 'pointer', transition: 'background-color 0.2s', backgroundColor: charCount === 0 ? '#6b7280' : '#2563eb', color: 'white' }} 59 + onMouseEnter={(e) => { if (charCount !== 0) e.currentTarget.style.backgroundColor = '#1d4ed8'; }} 60 + onMouseLeave={(e) => { if (charCount !== 0) e.currentTarget.style.backgroundColor = '#2563eb'; }} 61 + > 62 + Post 63 + </button> 64 + </div> 65 + </div> 66 + </div> 36 67 </div> 37 - {/* <p className="read-the-docs"> 38 - Click on the Vite and React logos to learn more 39 - </p> */} 40 - </> 68 + </UnifiedAuthProvider> 41 69 ) 42 70 } 43 71 44 - export default App 72 + export default App
+12 -7
src/components/Login.tsx
··· 200 200 }; 201 201 202 202 return ( 203 - <form onSubmit={handleSubmit} className="flex flex-col gap-3"> 203 + <form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}> 204 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" /> 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" style={{ display: 'block', width: '100%' }} /> 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" style={{ display: 'block', width: '100%' }} /> 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" style={{ display: 'block', width: '100%' }} /> 208 208 {error && <p className="text-xs text-red-500">{error}</p>} 209 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> 210 210 </form> ··· 230 230 if (!profile) { 231 231 return ( // Skeleton loader 232 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]'}`} /> 233 + <div className={`rounded-full bg-gray-300 dark:bg-gray-700 ${large ? 'w-10 h-10' : 'w-6 h-6'}`} /> 234 234 <div className="flex flex-col gap-2"> 235 235 <div className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? 'h-4 w-28' : 'h-3 w-20'}`} /> 236 236 <div className={`bg-gray-300 dark:bg-gray-700 rounded ${large ? 'h-4 w-20' : 'h-3 w-16'}`} /> ··· 241 241 242 242 return ( 243 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]'}`} /> 244 + <img 245 + src={profile?.avatar} 246 + alt="avatar" 247 + className={`object-cover rounded-full ${large ? 'w-10 h-10' : 'w-6 h-6'}`} 248 + style={{ maxWidth: '10rem', maxHeight: '10rem' }} 249 + /> 245 250 <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> 251 + <div className={`font-medium ${large ? 'text-gray-800 dark:text-gray-100 text-md' : 'text-gray-800 dark:text-gray-100 text-xs'}`}>{profile?.displayName}</div> 247 252 <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 253 </div> 249 254 </div>