tangled
alpha
login
or
join now
minito.dev
/
SkeetLonger.app
1
fork
atom
A web app for writing and sharing 301+ character Bluesky posts.
1
fork
atom
overview
issues
pulls
pipelines
Checking in UI
Minito
4 months ago
9830f505
25f6c49d
+76
-43
3 changed files
expand all
collapse all
unified
split
public
client-metadata.json
src
App.tsx
components
Login.tsx
+6
-6
public/client-metadata.json
···
1
1
{
2
2
-
"client_id": "https://local3768forumtest.whey.party/client-metadata.json",
2
2
+
"client_id": "https://7d3e-198-51-100-42.ngrok-free.app/client-metadata.json",
3
3
"client_name": "SkeetLonger",
4
4
-
"client_uri": "https://local3768forumtest.whey.party",
5
5
-
"logo_uri": "https://local3768forumtest.whey.party/logo192.png",
6
6
-
"tos_uri": "https://local3768forumtest.whey.party/terms-of-service",
7
7
-
"policy_uri": "https://local3768forumtest.whey.party/privacy-policy",
4
4
+
"client_uri": "https://7d3e-198-51-100-42.ngrok-free.app",
5
5
+
"logo_uri": "https://7d3e-198-51-100-42.ngrok-free.app/logo192.png",
6
6
+
"tos_uri": "https://7d3e-198-51-100-42.ngrok-free.app/terms-of-service",
7
7
+
"policy_uri": "https://7d3e-198-51-100-42.ngrok-free.app/privacy-policy",
8
8
"redirect_uris": [
9
9
-
"https://local3768forumtest.whey.party/callback"
9
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
2
-
import reactLogo from './assets/react.svg'
3
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
9
-
const [count, setCount] = useState(0)
7
7
+
const [postText, setPostText] = useState('')
8
8
+
const charCount = postText.length
9
9
+
10
10
+
const handleSubmit = async (e: React.FormEvent) => {
11
11
+
e.preventDefault();
12
12
+
// setError(null);
13
13
+
// try {
14
14
+
// localStorage.setItem("lastHandle", user);
15
15
+
// await loginWithPassword(user, password, `https://${serviceURL}`);
16
16
+
// } catch (err) {
17
17
+
// setError("Login failed. Check your handle and App Password.");
18
18
+
// }
19
19
+
};
10
20
11
21
return (
12
12
-
<>
13
13
-
{/* <div>
14
14
-
<a href="https://vite.dev" target="_blank">
15
15
-
<img src={viteLogo} className="logo" alt="Vite logo" />
16
16
-
</a>
17
17
-
<a href="https://react.dev" target="_blank">
18
18
-
<img src={reactLogo} className="logo react" alt="React logo" />
19
19
-
</a>
20
20
-
</div> */}
21
21
-
<h1>SkeetLonger</h1>
22
22
-
23
23
-
<div className="card">
22
22
+
<UnifiedAuthProvider>
23
23
+
<div style={{ position: 'fixed', top: '1rem', right: '1rem', zIndex: 9999 }}>
24
24
+
<Login compact={true} />
25
25
+
</div>
24
26
25
25
-
<UnifiedAuthProvider>
26
26
-
<Login compact={false}></Login>
27
27
-
</UnifiedAuthProvider>
28
28
-
27
27
+
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '5rem 1rem', backgroundColor: '#242424' }}>
28
28
+
<div style={{ width: '100%', maxWidth: '72rem' }}>
29
29
+
{/* Title */}
30
30
+
<h1 style={{ fontSize: '2.25rem', fontWeight: 'bold', textAlign: 'center', marginBottom: '0.5rem', color: '#f3f4f6' }}>
31
31
+
SkeetLonger
32
32
+
</h1>
33
33
+
<p style={{ textAlign: 'center', marginBottom: '2rem', color: '#9ca3af' }}>
34
34
+
Post longer content to Bluesky
35
35
+
</p>
29
36
30
30
-
{/* <button onClick={() => setCount((count) => count + 1)}>
31
31
-
count is {count}
32
32
-
</button>
33
33
-
<p>
34
34
-
Edit <code>src/App.tsx</code> and save to test HMR
35
35
-
</p> */}
37
37
+
{/* Text Editor Card */}
38
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
39
+
<textarea
40
40
+
value={postText}
41
41
+
onChange={(e) => setPostText(e.target.value)}
42
42
+
placeholder="What's on your mind? Write as much as you'd like..."
43
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
44
+
className="placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
45
45
+
/>
46
46
+
47
47
+
{/* Footer with character count and post button */}
48
48
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: '1rem' }}>
49
49
+
<div style={{ fontSize: '0.875rem' }}>
50
50
+
<span style={{ fontWeight: '500', color: charCount > 300 ? '#3b82f6' : '#9ca3af' }}>
51
51
+
{charCount} characters
52
52
+
</span>
53
53
+
</div>
54
54
+
55
55
+
<button
56
56
+
type='submit'
57
57
+
disabled={charCount < 300}
58
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
59
+
onMouseEnter={(e) => { if (charCount !== 0) e.currentTarget.style.backgroundColor = '#1d4ed8'; }}
60
60
+
onMouseLeave={(e) => { if (charCount !== 0) e.currentTarget.style.backgroundColor = '#2563eb'; }}
61
61
+
>
62
62
+
Post
63
63
+
</button>
64
64
+
</div>
65
65
+
</div>
66
66
+
</div>
36
67
</div>
37
37
-
{/* <p className="read-the-docs">
38
38
-
Click on the Vite and React logos to learn more
39
39
-
</p> */}
40
40
-
</>
68
68
+
</UnifiedAuthProvider>
41
69
)
42
70
}
43
71
44
44
-
export default App
72
72
+
export default App
+12
-7
src/components/Login.tsx
···
200
200
};
201
201
202
202
return (
203
203
-
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
203
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
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
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
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
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
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
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
233
-
<div className={`rounded-full bg-gray-300 dark:bg-gray-700 ${large ? 'w-10 h-10' : 'w-[30px] h-[30px]'}`} />
233
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
244
-
<img src={profile?.avatar} alt="avatar" className={`object-cover rounded-full ${large ? 'w-10 h-10' : 'w-[30px] h-[30px]'}`} />
244
244
+
<img
245
245
+
src={profile?.avatar}
246
246
+
alt="avatar"
247
247
+
className={`object-cover rounded-full ${large ? 'w-10 h-10' : 'w-6 h-6'}`}
248
248
+
style={{ maxWidth: '10rem', maxHeight: '10rem' }}
249
249
+
/>
245
250
<div className="flex flex-col items-start text-left">
246
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
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>