chore: moved post loading into tsx component
cfdbebd6
4 file(s) · +375 −263
| 1 | + | import { useEffect, useState } from "react"; |
|
| 2 | + | import MarkdownIt from "markdown-it"; |
|
| 3 | + | ||
| 4 | + | const md = new MarkdownIt({ |
|
| 5 | + | html: false, |
|
| 6 | + | linkify: true, |
|
| 7 | + | typographer: true, |
|
| 8 | + | }); |
|
| 9 | + | ||
| 10 | + | const DID = "did:plc:ia2zdnhjaokf5lazhxrmj6eu"; |
|
| 11 | + | const PDS_URL = "https://polybius.social"; |
|
| 12 | + | ||
| 13 | + | interface Record { |
|
| 14 | + | uri: string; |
|
| 15 | + | value: any; |
|
| 16 | + | type: "document" | "post"; |
|
| 17 | + | } |
|
| 18 | + | ||
| 19 | + | export default function NowUpdates() { |
|
| 20 | + | const [content, setContent] = useState<string>("<p>Loading...</p>"); |
|
| 21 | + | ||
| 22 | + | useEffect(() => { |
|
| 23 | + | async function fetchPosts() { |
|
| 24 | + | try { |
|
| 25 | + | // Fetch new site.standard.document records |
|
| 26 | + | const documentsPromise = fetch( |
|
| 27 | + | `${PDS_URL}/xrpc/com.atproto.repo.listRecords?` + |
|
| 28 | + | new URLSearchParams({ |
|
| 29 | + | repo: DID, |
|
| 30 | + | collection: "site.standard.document", |
|
| 31 | + | limit: "20", |
|
| 32 | + | }), |
|
| 33 | + | ) |
|
| 34 | + | .then((res) => (res.ok ? res.json() : { records: [] })) |
|
| 35 | + | .catch(() => ({ records: [] })); |
|
| 36 | + | ||
| 37 | + | // Fetch old app.bsky.feed.post records for backward compatibility |
|
| 38 | + | const postsPromise = fetch( |
|
| 39 | + | `${PDS_URL}/xrpc/com.atproto.repo.listRecords?` + |
|
| 40 | + | new URLSearchParams({ |
|
| 41 | + | repo: DID, |
|
| 42 | + | collection: "app.bsky.feed.post", |
|
| 43 | + | limit: "20", |
|
| 44 | + | filter: "posts_no_replies", |
|
| 45 | + | }), |
|
| 46 | + | ) |
|
| 47 | + | .then((res) => (res.ok ? res.json() : { records: [] })) |
|
| 48 | + | .catch(() => ({ records: [] })); |
|
| 49 | + | ||
| 50 | + | const [documentsData, postsData] = await Promise.all([ |
|
| 51 | + | documentsPromise, |
|
| 52 | + | postsPromise, |
|
| 53 | + | ]); |
|
| 54 | + | ||
| 55 | + | // Combine and normalize records |
|
| 56 | + | const documents = (documentsData.records || []).map((record: any) => ({ |
|
| 57 | + | ...record, |
|
| 58 | + | type: "document", |
|
| 59 | + | })); |
|
| 60 | + | ||
| 61 | + | const posts = (postsData.records || []) |
|
| 62 | + | .filter((record: any) => !record.value.reply) |
|
| 63 | + | .map((record: any) => ({ |
|
| 64 | + | ...record, |
|
| 65 | + | type: "post", |
|
| 66 | + | })); |
|
| 67 | + | ||
| 68 | + | // Combine all records and sort by date |
|
| 69 | + | const allRecords: Record[] = [...documents, ...posts].sort((a, b) => { |
|
| 70 | + | const dateA = new Date(a.value.publishedAt || a.value.createdAt); |
|
| 71 | + | const dateB = new Date(b.value.publishedAt || b.value.createdAt); |
|
| 72 | + | return dateB.getTime() - dateA.getTime(); // Most recent first |
|
| 73 | + | }); |
|
| 74 | + | ||
| 75 | + | if (allRecords.length === 0) { |
|
| 76 | + | setContent("<p>No recent updates found.</p>"); |
|
| 77 | + | return; |
|
| 78 | + | } |
|
| 79 | + | ||
| 80 | + | const postsHTML = allRecords |
|
| 81 | + | .map((record) => { |
|
| 82 | + | const value = record.value; |
|
| 83 | + | const rkey = record.uri.split("/").pop(); |
|
| 84 | + | ||
| 85 | + | // Render based on record type |
|
| 86 | + | if (record.type === "document") { |
|
| 87 | + | // site.standard.document |
|
| 88 | + | const publishedAt = new Date( |
|
| 89 | + | value.publishedAt, |
|
| 90 | + | ).toLocaleDateString(); |
|
| 91 | + | ||
| 92 | + | // Extract markdown content |
|
| 93 | + | let contentHTML = ""; |
|
| 94 | + | if (value.content && value.content.markdown) { |
|
| 95 | + | contentHTML = md.render(value.content.markdown); |
|
| 96 | + | } else if (value.textContent) { |
|
| 97 | + | contentHTML = `<p>${value.textContent}</p>`; |
|
| 98 | + | } |
|
| 99 | + | ||
| 100 | + | return ` |
|
| 101 | + | <a href="/pds?rkey=${rkey}" class="block border-b pb-6 mb-6 last:border-b-0"> |
|
| 102 | + | <article> |
|
| 103 | + | <h3 class="text-lg font-semibold mb-3">${value.title}</h3> |
|
| 104 | + | <div class="prose prose-invert max-w-none mb-3"> |
|
| 105 | + | ${contentHTML} |
|
| 106 | + | </div> |
|
| 107 | + | <div class="flex items-center gap-2 text-sm text-gray-500"> |
|
| 108 | + | <time>${publishedAt}</time> |
|
| 109 | + | </div> |
|
| 110 | + | </article> |
|
| 111 | + | </a> |
|
| 112 | + | `; |
|
| 113 | + | } else { |
|
| 114 | + | // app.bsky.feed.post (backward compatibility) |
|
| 115 | + | const createdAt = new Date(value.createdAt).toLocaleDateString(); |
|
| 116 | + | ||
| 117 | + | // Handle images |
|
| 118 | + | let imagesHTML = ""; |
|
| 119 | + | if ( |
|
| 120 | + | value.embed && |
|
| 121 | + | value.embed.$type === "app.bsky.embed.images" && |
|
| 122 | + | value.embed.images |
|
| 123 | + | ) { |
|
| 124 | + | const imageElements = value.embed.images |
|
| 125 | + | .map((image: any) => { |
|
| 126 | + | const blobUrl = |
|
| 127 | + | `${PDS_URL}/xrpc/com.atproto.sync.getBlob?` + |
|
| 128 | + | new URLSearchParams({ |
|
| 129 | + | did: DID, |
|
| 130 | + | cid: image.image.ref.$link, |
|
| 131 | + | }); |
|
| 132 | + | ||
| 133 | + | return ` |
|
| 134 | + | <img |
|
| 135 | + | src="${blobUrl}" |
|
| 136 | + | alt="${image.alt || "Image from post"}" |
|
| 137 | + | class="max-w-full h-auto" |
|
| 138 | + | loading="lazy" |
|
| 139 | + | /> |
|
| 140 | + | `; |
|
| 141 | + | }) |
|
| 142 | + | .join(""); |
|
| 143 | + | ||
| 144 | + | imagesHTML = ` |
|
| 145 | + | <div class="mt-3 grid gap-2 ${value.embed.images.length === 1 ? "grid-cols-1" : "grid-cols-2"}"> |
|
| 146 | + | ${imageElements} |
|
| 147 | + | </div> |
|
| 148 | + | `; |
|
| 149 | + | } |
|
| 150 | + | ||
| 151 | + | return ` |
|
| 152 | + | <a href="/pds?rkey=${rkey}" class="block border-b pb-6 mb-6 last:border-b-0"> |
|
| 153 | + | <article> |
|
| 154 | + | <p class="mb-2">${value.text}</p> |
|
| 155 | + | ${imagesHTML} |
|
| 156 | + | <time class="text-sm text-gray-500 mt-2 block">${createdAt}</time> |
|
| 157 | + | </article> |
|
| 158 | + | </a> |
|
| 159 | + | `; |
|
| 160 | + | } |
|
| 161 | + | }) |
|
| 162 | + | .join(""); |
|
| 163 | + | ||
| 164 | + | setContent(` |
|
| 165 | + | <div class="space-y-4"> |
|
| 166 | + | <div>${postsHTML}</div> |
|
| 167 | + | </div> |
|
| 168 | + | `); |
|
| 169 | + | } catch (err) { |
|
| 170 | + | console.error("Error fetching updates:", err); |
|
| 171 | + | setContent( |
|
| 172 | + | "<p>Error loading recent updates. Make sure your PDS is accessible.</p>", |
|
| 173 | + | ); |
|
| 174 | + | } |
|
| 175 | + | } |
|
| 176 | + | ||
| 177 | + | fetchPosts(); |
|
| 178 | + | }, []); |
|
| 179 | + | ||
| 180 | + | return <div dangerouslySetInnerHTML={{ __html: content }} />; |
|
| 181 | + | } |
| 1 | + | import { useEffect, useState } from "react"; |
|
| 2 | + | import MarkdownIt from "markdown-it"; |
|
| 3 | + | ||
| 4 | + | const md = new MarkdownIt({ |
|
| 5 | + | html: false, |
|
| 6 | + | linkify: true, |
|
| 7 | + | typographer: true, |
|
| 8 | + | }); |
|
| 9 | + | ||
| 10 | + | const DID = "did:plc:ia2zdnhjaokf5lazhxrmj6eu"; |
|
| 11 | + | const PDS_URL = "https://polybius.social"; |
|
| 12 | + | ||
| 13 | + | export default function PDSPost() { |
|
| 14 | + | const [content, setContent] = useState<string>("<p>Loading...</p>"); |
|
| 15 | + | ||
| 16 | + | useEffect(() => { |
|
| 17 | + | const urlParams = new URLSearchParams(window.location.search); |
|
| 18 | + | const rkey = urlParams.get("rkey"); |
|
| 19 | + | ||
| 20 | + | async function fetchPost() { |
|
| 21 | + | if (!rkey) { |
|
| 22 | + | setContent("<p>No post specified.</p>"); |
|
| 23 | + | return; |
|
| 24 | + | } |
|
| 25 | + | ||
| 26 | + | try { |
|
| 27 | + | // Try fetching as a document first |
|
| 28 | + | const documentResponse = await fetch( |
|
| 29 | + | `${PDS_URL}/xrpc/com.atproto.repo.getRecord?` + |
|
| 30 | + | new URLSearchParams({ |
|
| 31 | + | repo: DID, |
|
| 32 | + | collection: "site.standard.document", |
|
| 33 | + | rkey: rkey, |
|
| 34 | + | }), |
|
| 35 | + | ); |
|
| 36 | + | ||
| 37 | + | if (documentResponse.ok) { |
|
| 38 | + | const data = await documentResponse.json(); |
|
| 39 | + | const doc = data.value; |
|
| 40 | + | const publishedAt = new Date(doc.publishedAt).toLocaleDateString(); |
|
| 41 | + | ||
| 42 | + | // Extract markdown content |
|
| 43 | + | let contentHTML = ""; |
|
| 44 | + | if (doc.content && doc.content.markdown) { |
|
| 45 | + | contentHTML = md.render(doc.content.markdown); |
|
| 46 | + | } else if (doc.textContent) { |
|
| 47 | + | contentHTML = `<p>${doc.textContent}</p>`; |
|
| 48 | + | } |
|
| 49 | + | ||
| 50 | + | setContent(` |
|
| 51 | + | <article class="max-w-2xl mx-auto"> |
|
| 52 | + | <h1 class="text-2xl font-bold mb-4">${doc.title}</h1> |
|
| 53 | + | <div class="prose prose-invert max-w-none mb-4"> |
|
| 54 | + | ${contentHTML} |
|
| 55 | + | </div> |
|
| 56 | + | <div class="flex items-center justify-between mt-4"> |
|
| 57 | + | <time class="text-sm text-gray-500">${publishedAt}</time> |
|
| 58 | + | <button |
|
| 59 | + | id="share-btn" |
|
| 60 | + | class="text-sm text-gray-500 hover:text-gray-700 transition-colors" |
|
| 61 | + | > |
|
| 62 | + | Share |
|
| 63 | + | </button> |
|
| 64 | + | </div> |
|
| 65 | + | <div class="mt-12"> |
|
| 66 | + | <a class="style-link" href="/now">← Now</a> |
|
| 67 | + | </div> |
|
| 68 | + | </article> |
|
| 69 | + | `); |
|
| 70 | + | ||
| 71 | + | document.title = doc.title; |
|
| 72 | + | return; |
|
| 73 | + | } |
|
| 74 | + | ||
| 75 | + | // Fall back to fetching as a post |
|
| 76 | + | const postResponse = await fetch( |
|
| 77 | + | `${PDS_URL}/xrpc/com.atproto.repo.getRecord?` + |
|
| 78 | + | new URLSearchParams({ |
|
| 79 | + | repo: DID, |
|
| 80 | + | collection: "app.bsky.feed.post", |
|
| 81 | + | rkey: rkey, |
|
| 82 | + | }), |
|
| 83 | + | ); |
|
| 84 | + | ||
| 85 | + | if (!postResponse.ok) { |
|
| 86 | + | throw new Error(`HTTP error! status: ${postResponse.status}`); |
|
| 87 | + | } |
|
| 88 | + | ||
| 89 | + | const data = await postResponse.json(); |
|
| 90 | + | const post = data.value; |
|
| 91 | + | const createdAt = new Date(post.createdAt).toLocaleDateString(); |
|
| 92 | + | ||
| 93 | + | // Handle images |
|
| 94 | + | let imagesHTML = ""; |
|
| 95 | + | if ( |
|
| 96 | + | post.embed && |
|
| 97 | + | post.embed.$type === "app.bsky.embed.images" && |
|
| 98 | + | post.embed.images |
|
| 99 | + | ) { |
|
| 100 | + | const imageElements = post.embed.images |
|
| 101 | + | .map((image: any) => { |
|
| 102 | + | const blobUrl = |
|
| 103 | + | `${PDS_URL}/xrpc/com.atproto.sync.getBlob?` + |
|
| 104 | + | new URLSearchParams({ |
|
| 105 | + | did: DID, |
|
| 106 | + | cid: image.image.ref.$link, |
|
| 107 | + | }); |
|
| 108 | + | ||
| 109 | + | return ` |
|
| 110 | + | <img |
|
| 111 | + | src="${blobUrl}" |
|
| 112 | + | alt="${image.alt || "Image from post"}" |
|
| 113 | + | class="max-w-full h-auto" |
|
| 114 | + | loading="lazy" |
|
| 115 | + | /> |
|
| 116 | + | `; |
|
| 117 | + | }) |
|
| 118 | + | .join(""); |
|
| 119 | + | ||
| 120 | + | imagesHTML = ` |
|
| 121 | + | <div class="mt-3 grid gap-2 ${post.embed.images.length === 1 ? "grid-cols-1" : "grid-cols-2"}"> |
|
| 122 | + | ${imageElements} |
|
| 123 | + | </div> |
|
| 124 | + | `; |
|
| 125 | + | } |
|
| 126 | + | ||
| 127 | + | setContent(` |
|
| 128 | + | <article class="max-w-2xl mx-auto"> |
|
| 129 | + | <p class="mb-2">${post.text}</p> |
|
| 130 | + | ${imagesHTML} |
|
| 131 | + | <div class="flex items-center justify-between mt-4"> |
|
| 132 | + | <time class="text-sm text-gray-500">${createdAt}</time> |
|
| 133 | + | <button |
|
| 134 | + | id="share-btn" |
|
| 135 | + | class="text-sm text-gray-500 hover:text-gray-700 transition-colors" |
|
| 136 | + | > |
|
| 137 | + | Share |
|
| 138 | + | </button> |
|
| 139 | + | </div> |
|
| 140 | + | <div class="mt-12"> |
|
| 141 | + | <a class="style-link" href="/now">← Now</a> |
|
| 142 | + | </div> |
|
| 143 | + | </article> |
|
| 144 | + | `); |
|
| 145 | + | ||
| 146 | + | document.title = post.text; |
|
| 147 | + | } catch (err) { |
|
| 148 | + | console.error("Error fetching post:", err); |
|
| 149 | + | setContent( |
|
| 150 | + | "<p>Error loading post. Make sure your PDS is accessible.</p>", |
|
| 151 | + | ); |
|
| 152 | + | } |
|
| 153 | + | } |
|
| 154 | + | ||
| 155 | + | fetchPost(); |
|
| 156 | + | }, []); |
|
| 157 | + | ||
| 158 | + | // Handle share button click |
|
| 159 | + | useEffect(() => { |
|
| 160 | + | const handleShare = () => { |
|
| 161 | + | const url = window.location.href; |
|
| 162 | + | navigator.clipboard |
|
| 163 | + | .writeText(url) |
|
| 164 | + | .then(() => { |
|
| 165 | + | const btn = document.getElementById("share-btn"); |
|
| 166 | + | if (btn) { |
|
| 167 | + | const originalText = btn.textContent; |
|
| 168 | + | btn.textContent = "Copied!"; |
|
| 169 | + | setTimeout(() => { |
|
| 170 | + | btn.textContent = originalText; |
|
| 171 | + | }, 2000); |
|
| 172 | + | } |
|
| 173 | + | }) |
|
| 174 | + | .catch((err) => { |
|
| 175 | + | console.error("Failed to copy URL:", err); |
|
| 176 | + | alert("Failed to copy URL"); |
|
| 177 | + | }); |
|
| 178 | + | }; |
|
| 179 | + | ||
| 180 | + | // Add event listener after content is rendered |
|
| 181 | + | const btn = document.getElementById("share-btn"); |
|
| 182 | + | if (btn) { |
|
| 183 | + | btn.addEventListener("click", handleShare); |
|
| 184 | + | return () => btn.removeEventListener("click", handleShare); |
|
| 185 | + | } |
|
| 186 | + | }, [content]); |
|
| 187 | + | ||
| 188 | + | return <div dangerouslySetInnerHTML={{ __html: content }} />; |
|
| 189 | + | } |
| 1 | 1 | --- |
|
| 2 | 2 | import PageLayout from "@/layouts/Base"; |
|
| 3 | + | import NowUpdates from "@/components/NowUpdates"; |
|
| 3 | 4 | ||
| 4 | 5 | const meta = { |
|
| 5 | 6 | title: "Now", |
|
| 42 | 43 | <span class="sr-only">RSS</span> |
|
| 43 | 44 | </a> |
|
| 44 | 45 | </div> |
|
| 45 | - | <div id="posts-container"> |
|
| 46 | - | <p>Loading...</p> |
|
| 47 | - | </div> |
|
| 48 | - | <script type="module"> |
|
| 49 | - | import MarkdownIt from 'markdown-it'; |
|
| 50 | - | ||
| 51 | - | const md = new MarkdownIt({ |
|
| 52 | - | html: false, |
|
| 53 | - | linkify: true, |
|
| 54 | - | typographer: true |
|
| 55 | - | }); |
|
| 56 | - | ||
| 57 | - | const DID = 'did:plc:ia2zdnhjaokf5lazhxrmj6eu'; |
|
| 58 | - | const PDS_URL = 'https://polybius.social'; |
|
| 59 | - | ||
| 60 | - | // Fetch both documents and posts for backward compatibility |
|
| 61 | - | async function fetchPosts() { |
|
| 62 | - | try { |
|
| 63 | - | // Fetch new site.standard.document records |
|
| 64 | - | const documentsPromise = fetch( |
|
| 65 | - | `${PDS_URL}/xrpc/com.atproto.repo.listRecords?` + |
|
| 66 | - | new URLSearchParams({ |
|
| 67 | - | repo: DID, |
|
| 68 | - | collection: 'site.standard.document', |
|
| 69 | - | limit: '20' |
|
| 70 | - | }) |
|
| 71 | - | ).then(res => res.ok ? res.json() : { records: [] }).catch(() => ({ records: [] })); |
|
| 72 | - | ||
| 73 | - | // Fetch old app.bsky.feed.post records for backward compatibility |
|
| 74 | - | const postsPromise = fetch( |
|
| 75 | - | `${PDS_URL}/xrpc/com.atproto.repo.listRecords?` + |
|
| 76 | - | new URLSearchParams({ |
|
| 77 | - | repo: DID, |
|
| 78 | - | collection: 'app.bsky.feed.post', |
|
| 79 | - | limit: '20', |
|
| 80 | - | filter: 'posts_no_replies' |
|
| 81 | - | }) |
|
| 82 | - | ).then(res => res.ok ? res.json() : { records: [] }).catch(() => ({ records: [] })); |
|
| 83 | - | ||
| 84 | - | const [documentsData, postsData] = await Promise.all([documentsPromise, postsPromise]); |
|
| 85 | - | ||
| 86 | - | // Combine and normalize records |
|
| 87 | - | const documents = (documentsData.records || []).map(record => ({ |
|
| 88 | - | ...record, |
|
| 89 | - | type: 'document' |
|
| 90 | - | })); |
|
| 91 | - | ||
| 92 | - | const posts = (postsData.records || []) |
|
| 93 | - | .filter(record => !record.value.reply) |
|
| 94 | - | .map(record => ({ |
|
| 95 | - | ...record, |
|
| 96 | - | type: 'post' |
|
| 97 | - | })); |
|
| 98 | - | ||
| 99 | - | // Combine all records and sort by date |
|
| 100 | - | const allRecords = [...documents, ...posts].sort((a, b) => { |
|
| 101 | - | const dateA = new Date(a.value.publishedAt || a.value.createdAt); |
|
| 102 | - | const dateB = new Date(b.value.publishedAt || b.value.createdAt); |
|
| 103 | - | return dateB - dateA; // Most recent first |
|
| 104 | - | }); |
|
| 105 | - | ||
| 106 | - | if (allRecords.length === 0) { |
|
| 107 | - | document.getElementById('posts-container').innerHTML = '<p>No recent updates found.</p>'; |
|
| 108 | - | return; |
|
| 109 | - | } |
|
| 110 | - | ||
| 111 | - | const postsHTML = allRecords.map(record => { |
|
| 112 | - | const value = record.value; |
|
| 113 | - | const rkey = record.uri.split('/').pop(); |
|
| 114 | - | ||
| 115 | - | // Render based on record type |
|
| 116 | - | if (record.type === 'document') { |
|
| 117 | - | // site.standard.document |
|
| 118 | - | const publishedAt = new Date(value.publishedAt).toLocaleDateString(); |
|
| 119 | - | ||
| 120 | - | // Extract markdown content |
|
| 121 | - | let contentHTML = ''; |
|
| 122 | - | if (value.content && value.content.markdown) { |
|
| 123 | - | contentHTML = md.render(value.content.markdown); |
|
| 124 | - | } else if (value.textContent) { |
|
| 125 | - | contentHTML = `<p>${value.textContent}</p>`; |
|
| 126 | - | } |
|
| 127 | - | ||
| 128 | - | return ` |
|
| 129 | - | <div class="block border-b pb-6 mb-6 last:border-b-0"> |
|
| 130 | - | <article> |
|
| 131 | - | <h3 class="text-lg font-semibold mb-3">${value.title}</h3> |
|
| 132 | - | <div class="prose prose-invert max-w-none mb-3"> |
|
| 133 | - | ${contentHTML} |
|
| 134 | - | </div> |
|
| 135 | - | <div class="flex items-center gap-2 text-sm text-gray-500"> |
|
| 136 | - | <time>${publishedAt}</time> |
|
| 137 | - | </div> |
|
| 138 | - | </article> |
|
| 139 | - | </div> |
|
| 140 | - | `; |
|
| 141 | - | } else { |
|
| 142 | - | // app.bsky.feed.post (backward compatibility) |
|
| 143 | - | const createdAt = new Date(value.createdAt).toLocaleDateString(); |
|
| 144 | - | ||
| 145 | - | // Handle images |
|
| 146 | - | let imagesHTML = ''; |
|
| 147 | - | if (value.embed && value.embed.$type === 'app.bsky.embed.images' && value.embed.images) { |
|
| 148 | - | const imageElements = value.embed.images.map(image => { |
|
| 149 | - | const blobUrl = `${PDS_URL}/xrpc/com.atproto.sync.getBlob?` + |
|
| 150 | - | new URLSearchParams({ |
|
| 151 | - | did: DID, |
|
| 152 | - | cid: image.image.ref.$link |
|
| 153 | - | }); |
|
| 154 | - | ||
| 155 | - | return ` |
|
| 156 | - | <img |
|
| 157 | - | src="${blobUrl}" |
|
| 158 | - | alt="${image.alt || 'Image from post'}" |
|
| 159 | - | class="max-w-full h-auto" |
|
| 160 | - | loading="lazy" |
|
| 161 | - | /> |
|
| 162 | - | `; |
|
| 163 | - | }).join(''); |
|
| 164 | - | ||
| 165 | - | imagesHTML = ` |
|
| 166 | - | <div class="mt-3 grid gap-2 ${value.embed.images.length === 1 ? 'grid-cols-1' : 'grid-cols-2'}"> |
|
| 167 | - | ${imageElements} |
|
| 168 | - | </div> |
|
| 169 | - | `; |
|
| 170 | - | } |
|
| 171 | - | ||
| 172 | - | return ` |
|
| 173 | - | <a href="/pds?rkey=${rkey}" class="block border-b pb-6 mb-6 last:border-b-0"> |
|
| 174 | - | <article> |
|
| 175 | - | <p class="mb-2">${value.text}</p> |
|
| 176 | - | ${imagesHTML} |
|
| 177 | - | <time class="text-sm text-gray-500 mt-2 block">${createdAt}</time> |
|
| 178 | - | </article> |
|
| 179 | - | </a> |
|
| 180 | - | `; |
|
| 181 | - | } |
|
| 182 | - | }).join(''); |
|
| 183 | - | ||
| 184 | - | document.getElementById('posts-container').innerHTML = ` |
|
| 185 | - | <div class="space-y-4"> |
|
| 186 | - | <div>${postsHTML}</div> |
|
| 187 | - | </div> |
|
| 188 | - | `; |
|
| 189 | - | } catch (err) { |
|
| 190 | - | console.error('Error fetching updates:', err); |
|
| 191 | - | document.getElementById('posts-container').innerHTML = |
|
| 192 | - | '<p>Error loading recent updates. Make sure your PDS is accessible.</p>'; |
|
| 193 | - | } |
|
| 194 | - | } |
|
| 195 | - | ||
| 196 | - | fetchPosts(); |
|
| 197 | - | </script> |
|
| 46 | + | <NowUpdates client:load /> |
|
| 198 | 47 | </div> |
|
| 199 | 48 | </PageLayout> |
|
| 1 | 1 | --- |
|
| 2 | 2 | import PageLayout from "@/layouts/Base"; |
|
| 3 | + | import PDSPost from "@/components/PDSPost"; |
|
| 4 | + | ||
| 3 | 5 | const meta = { |
|
| 4 | 6 | title: "PDS", |
|
| 5 | 7 | description: "Posts", |
|
| 7 | 9 | --- |
|
| 8 | 10 | ||
| 9 | 11 | <PageLayout meta={meta}> |
|
| 10 | - | <div id="post-container"> |
|
| 11 | - | <p>Loading...</p> |
|
| 12 | - | </div> |
|
| 13 | - | ||
| 14 | - | <script> |
|
| 15 | - | const DID = 'did:plc:ia2zdnhjaokf5lazhxrmj6eu'; |
|
| 16 | - | const PDS_URL = 'https://polybius.social'; |
|
| 17 | - | ||
| 18 | - | const urlParams = new URLSearchParams(window.location.search); |
|
| 19 | - | const rkey = urlParams.get('rkey'); |
|
| 20 | - | ||
| 21 | - | async function fetchPost() { |
|
| 22 | - | if (!rkey) { |
|
| 23 | - | document.getElementById('post-container').innerHTML = '<p>No post specified.</p>'; |
|
| 24 | - | return; |
|
| 25 | - | } |
|
| 26 | - | ||
| 27 | - | try { |
|
| 28 | - | const response = await fetch( |
|
| 29 | - | `${PDS_URL}/xrpc/com.atproto.repo.getRecord?` + |
|
| 30 | - | new URLSearchParams({ |
|
| 31 | - | repo: DID, |
|
| 32 | - | collection: 'app.bsky.feed.post', |
|
| 33 | - | rkey: rkey |
|
| 34 | - | }) |
|
| 35 | - | ); |
|
| 36 | - | ||
| 37 | - | if (!response.ok) { |
|
| 38 | - | throw new Error(`HTTP error! status: ${response.status}`); |
|
| 39 | - | } |
|
| 40 | - | ||
| 41 | - | const data = await response.json(); |
|
| 42 | - | const post = data.value; |
|
| 43 | - | const createdAt = new Date(post.createdAt).toLocaleDateString(); |
|
| 44 | - | ||
| 45 | - | // Handle images |
|
| 46 | - | let imagesHTML = ''; |
|
| 47 | - | if (post.embed && post.embed.$type === 'app.bsky.embed.images' && post.embed.images) { |
|
| 48 | - | const imageElements = post.embed.images.map(image => { |
|
| 49 | - | // Construct blob URL - images are stored as blobs on the PDS |
|
| 50 | - | const blobUrl = `${PDS_URL}/xrpc/com.atproto.sync.getBlob?` + |
|
| 51 | - | new URLSearchParams({ |
|
| 52 | - | did: DID, |
|
| 53 | - | cid: image.image.ref.$link |
|
| 54 | - | }); |
|
| 55 | - | ||
| 56 | - | return ` |
|
| 57 | - | <img |
|
| 58 | - | src="${blobUrl}" |
|
| 59 | - | alt="${image.alt || 'Image from post'}" |
|
| 60 | - | class="max-w-full h-auto" |
|
| 61 | - | loading="lazy" |
|
| 62 | - | /> |
|
| 63 | - | `; |
|
| 64 | - | }).join(''); |
|
| 65 | - | ||
| 66 | - | imagesHTML = ` |
|
| 67 | - | <div class="mt-3 grid gap-2 ${post.embed.images.length === 1 ? 'grid-cols-1' : 'grid-cols-2'}"> |
|
| 68 | - | ${imageElements} |
|
| 69 | - | </div> |
|
| 70 | - | `; |
|
| 71 | - | } |
|
| 72 | - | ||
| 73 | - | document.getElementById('post-container').innerHTML = ` |
|
| 74 | - | <article class="max-w-2xl mx-auto"> |
|
| 75 | - | <p class="mb-2">${post.text}</p> |
|
| 76 | - | ${imagesHTML} |
|
| 77 | - | <div class="flex items-center justify-between mt-4"> |
|
| 78 | - | <time class="text-sm text-gray-500">${createdAt}</time> |
|
| 79 | - | <button |
|
| 80 | - | id="share-btn" |
|
| 81 | - | class="text-sm text-gray-500 hover:text-gray-700 transition-colors" |
|
| 82 | - | onclick="copyURL()" |
|
| 83 | - | > |
|
| 84 | - | Share |
|
| 85 | - | </button> |
|
| 86 | - | </div> |
|
| 87 | - | <div class="mt-12"> |
|
| 88 | - | <a class="style-link" href="/now">← Now</a> |
|
| 89 | - | </div> |
|
| 90 | - | </article> |
|
| 91 | - | `; |
|
| 92 | - | ||
| 93 | - | document.title = post.text; |
|
| 94 | - | } catch (err) { |
|
| 95 | - | console.error('Error fetching post:', err); |
|
| 96 | - | document.getElementById('post-container').innerHTML = |
|
| 97 | - | '<p>Error loading post. Make sure your PDS is accessible.</p>'; |
|
| 98 | - | } |
|
| 99 | - | } |
|
| 100 | - | ||
| 101 | - | fetchPost(); |
|
| 102 | - | ||
| 103 | - | // Copy URL function |
|
| 104 | - | window.copyURL = function() { |
|
| 105 | - | const url = window.location.href; |
|
| 106 | - | navigator.clipboard.writeText(url).then(() => { |
|
| 107 | - | const btn = document.getElementById('share-btn'); |
|
| 108 | - | const originalText = btn.textContent; |
|
| 109 | - | btn.textContent = 'Copied!'; |
|
| 110 | - | setTimeout(() => { |
|
| 111 | - | btn.textContent = originalText; |
|
| 112 | - | }, 2000); |
|
| 113 | - | }).catch(err => { |
|
| 114 | - | console.error('Failed to copy URL:', err); |
|
| 115 | - | alert('Failed to copy URL'); |
|
| 116 | - | }); |
|
| 117 | - | }; |
|
| 118 | - | </script> |
|
| 119 | - | ||
| 12 | + | <PDSPost client:load /> |
|
| 120 | 13 | </PageLayout> |
|