A personal website powered by Astro and ATProto

cleaned up atproto service and working grain galleries

+2 -3
scripts/discover-collections.ts
··· 46 46 } 47 47 48 48 // Run if called directly 49 - if (require.main === module) { 50 - main(); 51 - } 49 + // Run the main function 50 + main();
+5 -7
src/components/content/BlueskyFeed.astro
··· 1 1 --- 2 - import { AtprotoClient } from '../../lib/atproto/client'; 2 + import { AtprotoBrowser } from '../../lib/atproto/atproto-browser'; 3 3 import { loadConfig } from '../../lib/config/site'; 4 4 import BlueskyPost from './BlueskyPost.astro'; 5 5 ··· 13 13 const { feedUri, limit = 10, showAuthor = true, showTimestamp = true } = Astro.props; 14 14 15 15 const config = loadConfig(); 16 - const client = new AtprotoClient(config.atproto.pdsUrl); 16 + const browser = new AtprotoBrowser(); 17 17 18 18 // Fetch feed data with error handling 19 19 let blueskyPosts: any[] = []; 20 20 try { 21 - const records = await client.getFeed(feedUri, limit); 22 - const filteredRecords = client.filterSupportedRecords(records); 21 + const records = await browser.getFeed(feedUri, limit); 22 + const filteredRecords = records.filter(r => r.value?.$type === 'app.bsky.feed.post'); 23 23 24 24 // Only render Bluesky posts 25 - blueskyPosts = filteredRecords.filter(record => 26 - record.value && record.value.$type === 'app.bsky.feed.post' 27 - ); 25 + blueskyPosts = filteredRecords; 28 26 } catch (error) { 29 27 console.error('Error fetching feed:', error); 30 28 blueskyPosts = [];
+9 -4
src/components/content/BlueskyPost.astro
··· 1 1 --- 2 2 import type { BlueskyPost } from '../../lib/types/atproto'; 3 + import { extractCidFromBlobRef, blobCdnUrl } from '../../lib/atproto/blob-url'; 4 + import { loadConfig } from '../../lib/config/site'; 3 5 4 6 interface Props { 5 7 post: BlueskyPost; ··· 22 24 }); 23 25 }; 24 26 25 - // Helper function to get image URL from blob reference 26 - const getImageUrl = (imageRef: string) => { 27 - return `https://bsky.social/xrpc/com.atproto.sync.getBlob?did=did:plc:6ayddqghxhciedbaofoxkcbs&cid=${imageRef}`; 27 + const getImageUrl = (ref: unknown) => { 28 + const cid = extractCidFromBlobRef(ref); 29 + if (!cid) return ''; 30 + const did = loadConfig().atproto.did; 31 + if (!did) return ''; 32 + return blobCdnUrl(did, cid); 28 33 }; 29 34 30 35 // Helper function to render images ··· 34 39 return ( 35 40 <div class={`grid gap-2 ${images.length === 1 ? 'grid-cols-1' : images.length === 2 ? 'grid-cols-2' : 'grid-cols-3'}`}> 36 41 {images.map((image: any) => { 37 - const imageUrl = getImageUrl(image.image.ref.$link); 42 + const imageUrl = getImageUrl(image.image?.ref); 38 43 return ( 39 44 <div class="relative"> 40 45 <img
+13 -18
src/components/content/ContentFeed.astro
··· 1 1 --- 2 - import { AtprotoClient } from '../../lib/atproto/client'; 2 + import { AtprotoBrowser } from '../../lib/atproto/atproto-browser'; 3 3 import { loadConfig } from '../../lib/config/site'; 4 4 import type { AtprotoRecord } from '../../lib/types/atproto'; 5 + import { extractCidFromBlobRef, blobCdnUrl } from '../../lib/atproto/blob'; 5 6 6 7 interface Props { 7 8 handle: string; ··· 22 23 } = Astro.props; 23 24 24 25 const config = loadConfig(); 25 - const client = new AtprotoClient(config.atproto.pdsUrl); 26 + const browser = new AtprotoBrowser(); 26 27 27 28 // Helper function to get image URL from blob reference 28 - const getImageUrl = (imageRef: string) => { 29 - return `https://bsky.social/xrpc/com.atproto.sync.getBlob?did=did:plc:6ayddqghxhciedbaofoxkcbs&cid=${imageRef}`; 29 + const getImageUrl = (imageRef: unknown) => { 30 + const cid = extractCidFromBlobRef(imageRef); 31 + if (!cid) return ''; 32 + const did = config.atproto.did; 33 + if (!did) return ''; 34 + return blobCdnUrl(did, cid); 30 35 }; 31 36 32 37 // Helper function to format date ··· 42 47 let records: AtprotoRecord[] = []; 43 48 try { 44 49 if (feedUri) { 45 - records = await client.getFeed(feedUri, limit); 50 + records = await browser.getFeed(feedUri, limit); 46 51 } else { 47 - records = await client.getRecords(handle, collection, limit); 52 + const res = await browser.getCollectionRecords(handle, collection, limit); 53 + records = res?.records ?? []; 48 54 } 49 55 50 56 } catch (error) { ··· 72 78 {post.embed.$type === 'app.bsky.embed.images' && post.embed.images && ( 73 79 <div class={`grid gap-2 ${post.embed.images.length === 1 ? 'grid-cols-1' : post.embed.images.length === 2 ? 'grid-cols-2' : 'grid-cols-3'}`}> 74 80 {post.embed.images.map((image: any) => { 75 - // Handle both string and BlobRef object formats 76 - let imageRef; 77 - if (typeof image.image?.ref === 'string') { 78 - imageRef = image.image.ref; 79 - } else if (image.image?.ref?.$link) { 80 - imageRef = image.image.ref.$link; 81 - } else if (image.image?.ref?.toString) { 82 - // Handle BlobRef object 83 - imageRef = image.image.ref.toString(); 84 - } 85 - 86 - const imageUrl = imageRef ? getImageUrl(imageRef) : ''; 81 + const imageUrl = getImageUrl(image.image?.ref); 87 82 return ( 88 83 <div class="relative"> 89 84 <img
+30 -3
src/components/content/GrainGalleryDisplay.astro
··· 105 105 class="w-full h-48 object-cover rounded-lg transition-transform duration-200 group-hover:scale-105" 106 106 loading="lazy" 107 107 /> 108 - {(image.alt || image.caption) && ( 109 - <div class="absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white text-sm p-2 rounded-b-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200"> 110 - {image.alt || image.caption} 108 + {(image.alt || image.caption || image.exif) && ( 109 + <div class="absolute bottom-0 left-0 right-0 bg-black/60 text-white text-xs p-2 rounded-b-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 space-y-1"> 110 + {image.alt || image.caption ? ( 111 + <div class="font-medium">{image.alt || image.caption}</div> 112 + ) : null} 113 + {image.exif && ( 114 + <div class="grid grid-cols-2 gap-x-2 gap-y-0.5"> 115 + {image.exif.make && image.exif.model && ( 116 + <div class="col-span-2">{image.exif.make} {image.exif.model}</div> 117 + )} 118 + {image.exif.lensMake && image.exif.lensModel && ( 119 + <div class="col-span-2">{image.exif.lensMake} {image.exif.lensModel}</div> 120 + )} 121 + {image.exif.fNumber && ( 122 + <div>ƒ/{image.exif.fNumber}</div> 123 + )} 124 + {image.exif.exposureTime && ( 125 + <div>{image.exif.exposureTime}s</div> 126 + )} 127 + {image.exif.iSO && ( 128 + <div>ISO {image.exif.iSO}</div> 129 + )} 130 + {image.exif.focalLengthIn35mmFormat && ( 131 + <div>{image.exif.focalLengthIn35mmFormat}mm</div> 132 + )} 133 + {image.exif.dateTimeOriginal && ( 134 + <div class="col-span-2">{new Date(image.exif.dateTimeOriginal).toLocaleDateString()}</div> 135 + )} 136 + </div> 137 + )} 111 138 </div> 112 139 )} 113 140 </div>
+24
src/lib/atproto/atproto-browser.ts
··· 202 202 return []; 203 203 } 204 204 } 205 + 206 + // Get posts from an appview feed URI 207 + async getFeed(feedUri: string, limit: number = 20): Promise<AtprotoRecord[]> { 208 + try { 209 + const response = await this.agent.api.app.bsky.feed.getFeed({ 210 + feed: feedUri, 211 + limit, 212 + }); 213 + 214 + const records: AtprotoRecord[] = response.data.feed.map((item: any) => ({ 215 + uri: item.post.uri, 216 + cid: item.post.cid, 217 + value: item.post.record, 218 + indexedAt: item.post.indexedAt, 219 + collection: item.post.uri.split('/')[2] || 'unknown', 220 + $type: (item.post.record?.$type as string) || 'unknown', 221 + })); 222 + 223 + return records; 224 + } catch (error) { 225 + console.error('Error fetching feed:', error); 226 + return []; 227 + } 228 + } 205 229 }
-233
src/lib/atproto/client.ts
··· 1 - import { AtpAgent } from '@atproto/api'; 2 - import type { AtprotoRecord } from '../types/atproto'; 3 - 4 - // Simple in-memory cache with TTL 5 - class AtprotoCache { 6 - private cache = new Map<string, { data: any; timestamp: number }>(); 7 - private ttl = 5 * 60 * 1000; // 5 minutes 8 - 9 - set(key: string, data: any): void { 10 - this.cache.set(key, { data, timestamp: Date.now() }); 11 - } 12 - 13 - get(key: string): any | null { 14 - const item = this.cache.get(key); 15 - if (!item) return null; 16 - 17 - if (Date.now() - item.timestamp > this.ttl) { 18 - this.cache.delete(key); 19 - return null; 20 - } 21 - 22 - return item.data; 23 - } 24 - 25 - clear(): void { 26 - this.cache.clear(); 27 - } 28 - } 29 - 30 - export class AtprotoClient { 31 - private agent: AtpAgent; 32 - private cache: AtprotoCache; 33 - 34 - constructor(pdsUrl: string = 'https://bsky.social') { 35 - this.agent = new AtpAgent({ service: pdsUrl }); 36 - this.cache = new AtprotoCache(); 37 - } 38 - 39 - async resolveHandle(handle: string): Promise<string | null> { 40 - const cacheKey = `handle:${handle}`; 41 - const cached = this.cache.get(cacheKey); 42 - if (cached) return cached; 43 - 44 - try { 45 - const response = await this.agent.api.com.atproto.identity.resolveHandle({ 46 - handle: handle, 47 - }); 48 - 49 - const did = response.data.did; 50 - this.cache.set(cacheKey, did); 51 - return did; 52 - } catch (error) { 53 - console.error('Error resolving handle:', error); 54 - return null; 55 - } 56 - } 57 - 58 - async getRecords(identifier: string, collection: string, limit: number = 50): Promise<AtprotoRecord[]> { 59 - // Check if identifier is a handle (contains @) or DID 60 - let did = identifier; 61 - if (identifier.includes('@')) { 62 - console.log('AtprotoClient: Resolving handle to DID:', identifier); 63 - const resolvedDid = await this.resolveHandle(identifier); 64 - if (!resolvedDid) { 65 - console.error('AtprotoClient: Failed to resolve handle:', identifier); 66 - return []; 67 - } 68 - did = resolvedDid; 69 - console.log('AtprotoClient: Resolved handle to DID:', did); 70 - } 71 - 72 - const cacheKey = `records:${did}:${collection}:${limit}`; 73 - const cached = this.cache.get(cacheKey); 74 - if (cached) return cached; 75 - 76 - console.log('AtprotoClient: Fetching records for DID:', did); 77 - console.log('AtprotoClient: Collection:', collection); 78 - console.log('AtprotoClient: Limit:', limit); 79 - 80 - try { 81 - const response = await this.agent.api.com.atproto.repo.listRecords({ 82 - repo: did, 83 - collection, 84 - limit, 85 - }); 86 - 87 - console.log('AtprotoClient: API response received'); 88 - console.log('AtprotoClient: Records count:', response.data.records.length); 89 - 90 - const records = response.data.records.map((record: any) => ({ 91 - uri: record.uri, 92 - cid: record.cid, 93 - value: record.value, 94 - indexedAt: record.indexedAt, 95 - })); 96 - 97 - this.cache.set(cacheKey, records); 98 - return records; 99 - } catch (error) { 100 - console.error('AtprotoClient: Error fetching records:', error); 101 - console.error('AtprotoClient: Error details:', { 102 - did, 103 - collection, 104 - limit, 105 - error: error instanceof Error ? error.message : String(error) 106 - }); 107 - return []; 108 - } 109 - } 110 - 111 - async getFeed(feedUri: string, limit: number = 20): Promise<AtprotoRecord[]> { 112 - const cacheKey = `feed:${feedUri}:${limit}`; 113 - const cached = this.cache.get(cacheKey); 114 - if (cached) return cached; 115 - 116 - try { 117 - const response = await this.agent.api.app.bsky.feed.getFeed({ 118 - feed: feedUri, 119 - limit, 120 - }); 121 - 122 - const records = response.data.feed.map((item: any) => ({ 123 - uri: item.post.uri, 124 - cid: item.post.cid, 125 - value: item.post.record, 126 - indexedAt: item.post.indexedAt, 127 - })); 128 - 129 - this.cache.set(cacheKey, records); 130 - return records; 131 - } catch (error) { 132 - console.error('Error fetching feed:', error); 133 - return []; 134 - } 135 - } 136 - 137 - async getProfile(did: string): Promise<any> { 138 - const cacheKey = `profile:${did}`; 139 - const cached = this.cache.get(cacheKey); 140 - if (cached) return cached; 141 - 142 - try { 143 - const response = await this.agent.api.app.bsky.actor.getProfile({ 144 - actor: did, 145 - }); 146 - 147 - this.cache.set(cacheKey, response.data); 148 - return response.data; 149 - } catch (error) { 150 - console.error('Error fetching profile:', error); 151 - return null; 152 - } 153 - } 154 - 155 - // Filter records by supported content types 156 - filterSupportedRecords(records: AtprotoRecord[]): AtprotoRecord[] { 157 - return records.filter(record => { 158 - const type = record.value?.$type; 159 - return type && ( 160 - type === 'app.bsky.feed.post' || 161 - type === 'app.bsky.actor.profile#whitewindBlogPost' || 162 - type === 'app.bsky.actor.profile#leafletPublication' || 163 - type === 'app.bsky.actor.profile#grainImageGallery' 164 - ); 165 - }); 166 - } 167 - 168 - // Get all records from a repository (using existing API) 169 - async getAllRecords(handle: string, limit: number = 100): Promise<AtprotoRecord[]> { 170 - const cacheKey = `all-records:${handle}:${limit}`; 171 - const cached = this.cache.get(cacheKey); 172 - if (cached) return cached; 173 - 174 - try { 175 - console.log('getAllRecords: Starting with handle:', handle); 176 - 177 - // Resolve handle to DID 178 - const did = await this.resolveHandle(handle); 179 - console.log('getAllRecords: Resolved DID:', did); 180 - 181 - if (!did) { 182 - console.error('getAllRecords: Failed to resolve handle to DID'); 183 - return []; 184 - } 185 - 186 - // Try to get records from common collections 187 - console.log('getAllRecords: Trying common collections...'); 188 - const collections = ['app.bsky.feed.post', 'app.bsky.actor.profile']; 189 - let allRecords: AtprotoRecord[] = []; 190 - 191 - for (const collection of collections) { 192 - try { 193 - console.log(`getAllRecords: Trying collection: ${collection}`); 194 - const response = await this.agent.api.com.atproto.repo.listRecords({ 195 - repo: did, 196 - collection, 197 - limit: Math.floor(limit / collections.length), 198 - }); 199 - 200 - console.log(`getAllRecords: Got ${response.data.records.length} records from ${collection}`); 201 - 202 - const records = response.data.records.map((record: any) => ({ 203 - uri: record.uri, 204 - cid: record.cid, 205 - value: record.value, 206 - indexedAt: record.indexedAt, 207 - })); 208 - 209 - allRecords = allRecords.concat(records); 210 - } catch (error) { 211 - console.log(`getAllRecords: No records in collection ${collection}:`, error); 212 - } 213 - } 214 - 215 - console.log('getAllRecords: Total records found:', allRecords.length); 216 - this.cache.set(cacheKey, allRecords); 217 - return allRecords; 218 - } catch (error) { 219 - console.error('getAllRecords: Error fetching all records:', error); 220 - console.error('getAllRecords: Error details:', { 221 - handle, 222 - limit, 223 - error: error instanceof Error ? error.message : String(error) 224 - }); 225 - return []; 226 - } 227 - } 228 - 229 - // Clear cache (useful for development) 230 - clearCache(): void { 231 - this.cache.clear(); 232 - } 233 - }
+8 -12
src/lib/components/discovered-registry.ts
··· 1 1 import type { DiscoveredTypes } from '../generated/discovered-types'; 2 + import type { AnyRecordByType } from '../atproto/record-types'; 2 3 3 - export interface DiscoveredComponent { 4 - $type: DiscoveredTypes; 4 + export type DiscoveredComponent<T extends string = DiscoveredTypes> = { 5 + $type: T; 5 6 component: string; 6 - props: Record<string, any>; 7 + props: Record<string, unknown>; 7 8 } 8 9 9 - export interface ComponentRegistry { 10 - [key: string]: { 11 - component: string; 12 - props?: Record<string, any>; 13 - }; 14 - } 10 + export type ComponentRegistry = Record<string, { component: string; props?: Record<string, unknown> }> 15 11 16 12 export class DiscoveredComponentRegistry { 17 13 private registry: ComponentRegistry = {}; ··· 46 42 } 47 43 48 44 // Register a component for a specific $type 49 - registerComponent($type: DiscoveredTypes, component: string, props?: Record<string, any>): void { 45 + registerComponent($type: DiscoveredTypes, component: string, props?: Record<string, unknown>): void { 50 46 this.registry[$type] = { 51 47 component, 52 48 props ··· 54 50 } 55 51 56 52 // Get component info for a $type 57 - getComponent($type: DiscoveredTypes): { component: string; props?: Record<string, any> } | null { 53 + getComponent($type: DiscoveredTypes): { component: string; props?: Record<string, unknown> } | null { 58 54 return this.registry[$type] || null; 59 55 } 60 56 ··· 104 100 } 105 101 106 102 // Get component info for rendering 107 - getComponentInfo($type: DiscoveredTypes): DiscoveredComponent | null { 103 + getComponentInfo<T extends DiscoveredTypes>($type: T): DiscoveredComponent<T> | null { 108 104 const componentInfo = this.getComponent($type); 109 105 if (!componentInfo) return null; 110 106
-195
src/pages/api-debug.astro
··· 1 - --- 2 - import Layout from '../layouts/Layout.astro'; 3 - import { AtprotoBrowser } from '../lib/atproto/atproto-browser'; 4 - import { loadConfig } from '../lib/config/site'; 5 - 6 - const config = loadConfig(); 7 - const browser = new AtprotoBrowser(); 8 - 9 - let debugInfo = { 10 - config: null, 11 - repoInfo: null, 12 - collections: [], 13 - testResults: [], 14 - error: null 15 - }; 16 - 17 - try { 18 - debugInfo.config = { 19 - handle: config.atproto.handle, 20 - did: config.atproto.did, 21 - pdsUrl: config.atproto.pdsUrl 22 - }; 23 - 24 - if (config.atproto.handle && config.atproto.handle !== 'your-handle-here') { 25 - // Test 1: Resolve handle 26 - console.log('🔍 Testing handle resolution...'); 27 - const resolvedDid = await browser.resolveHandle(config.atproto.handle); 28 - debugInfo.testResults.push({ 29 - test: 'Handle Resolution', 30 - success: !!resolvedDid, 31 - result: resolvedDid, 32 - error: null 33 - }); 34 - 35 - // Test 2: Get repo info 36 - console.log('📊 Testing repo info...'); 37 - const repoInfo = await browser.getRepoInfo(config.atproto.handle); 38 - debugInfo.repoInfo = repoInfo; 39 - debugInfo.testResults.push({ 40 - test: 'Repository Info', 41 - success: !!repoInfo, 42 - result: repoInfo ? { 43 - did: repoInfo.did, 44 - handle: repoInfo.handle, 45 - collections: repoInfo.collections.length, 46 - recordCount: repoInfo.recordCount 47 - } : null, 48 - error: null 49 - }); 50 - 51 - if (repoInfo) { 52 - debugInfo.collections = repoInfo.collections; 53 - 54 - // Test 3: Get records from first collection 55 - if (repoInfo.collections.length > 0) { 56 - const firstCollection = repoInfo.collections[0]; 57 - console.log(`📦 Testing collection: ${firstCollection}`); 58 - const records = await browser.getCollectionRecords(config.atproto.handle, firstCollection, 5); 59 - debugInfo.testResults.push({ 60 - test: `Collection Records (${firstCollection})`, 61 - success: !!records, 62 - result: records ? { 63 - collection: records.collection, 64 - recordCount: records.recordCount, 65 - sampleRecords: records.records.slice(0, 2).map(r => ({ 66 - uri: r.uri, 67 - $type: r.$type, 68 - collection: r.collection 69 - })) 70 - } : null, 71 - error: null 72 - }); 73 - } 74 - } 75 - } 76 - } catch (error) { 77 - debugInfo.error = error; 78 - console.error('API Debug Error:', error); 79 - } 80 - --- 81 - 82 - <Layout title="API Debug"> 83 - <div class="container mx-auto px-4 py-8"> 84 - <header class="text-center mb-12"> 85 - <h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-4"> 86 - API Debug 87 - </h1> 88 - <p class="text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto"> 89 - Testing ATProto API calls and configuration 90 - </p> 91 - </header> 92 - 93 - <main class="max-w-6xl mx-auto space-y-8"> 94 - {debugInfo.error ? ( 95 - <div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-8"> 96 - <h3 class="text-xl font-semibold text-red-800 dark:text-red-200 mb-4"> 97 - Error 98 - </h3> 99 - <p class="text-red-700 dark:text-red-300 mb-4"> 100 - {debugInfo.error instanceof Error ? debugInfo.error.message : String(debugInfo.error)} 101 - </p> 102 - <pre class="bg-red-100 dark:bg-red-800 p-3 rounded text-xs overflow-x-auto"> 103 - {JSON.stringify(debugInfo.error, null, 2)} 104 - </pre> 105 - </div> 106 - ) : ( 107 - <> 108 - <!-- Configuration --> 109 - <div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-6"> 110 - <h3 class="text-lg font-semibold text-blue-800 dark:text-blue-200 mb-2"> 111 - Configuration 112 - </h3> 113 - <div class="text-sm text-blue-700 dark:text-blue-300"> 114 - <pre class="bg-blue-100 dark:bg-blue-800 p-3 rounded text-xs overflow-x-auto"> 115 - {JSON.stringify(debugInfo.config, null, 2)} 116 - </pre> 117 - </div> 118 - </div> 119 - 120 - <!-- Test Results --> 121 - <div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-6"> 122 - <h3 class="text-lg font-semibold text-green-800 dark:text-green-200 mb-2"> 123 - API Test Results 124 - </h3> 125 - <div class="space-y-4"> 126 - {debugInfo.testResults.map((test, index) => ( 127 - <div class="bg-green-100 dark:bg-green-800 p-4 rounded"> 128 - <div class="flex items-center justify-between mb-2"> 129 - <h4 class="font-semibold text-green-800 dark:text-green-200"> 130 - {test.test} 131 - </h4> 132 - <span class={`px-2 py-1 rounded text-xs ${ 133 - test.success 134 - ? 'bg-green-200 text-green-800 dark:bg-green-700 dark:text-green-200' 135 - : 'bg-red-200 text-red-800 dark:bg-red-700 dark:text-red-200' 136 - }`}> 137 - {test.success ? '✅ Success' : '❌ Failed'} 138 - </span> 139 - </div> 140 - {test.result && ( 141 - <pre class="text-xs bg-green-200 dark:bg-green-700 p-2 rounded overflow-x-auto"> 142 - {JSON.stringify(test.result, null, 2)} 143 - </pre> 144 - )} 145 - {test.error && ( 146 - <p class="text-red-600 dark:text-red-400 text-xs mt-2"> 147 - Error: {test.error} 148 - </p> 149 - )} 150 - </div> 151 - ))} 152 - </div> 153 - </div> 154 - 155 - <!-- Repository Info --> 156 - {debugInfo.repoInfo && ( 157 - <div class="bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg p-6"> 158 - <h3 class="text-lg font-semibold text-purple-800 dark:text-purple-200 mb-2"> 159 - Repository Information 160 - </h3> 161 - <div class="text-sm text-purple-700 dark:text-purple-300"> 162 - <p><strong>DID:</strong> {debugInfo.repoInfo.did}</p> 163 - <p><strong>Handle:</strong> {debugInfo.repoInfo.handle}</p> 164 - <p><strong>Record Count:</strong> {debugInfo.repoInfo.recordCount}</p> 165 - <p><strong>Collections:</strong> {debugInfo.repoInfo.collections.length}</p> 166 - <div class="mt-2"> 167 - <p class="font-semibold">Collections:</p> 168 - <ul class="list-disc list-inside space-y-1"> 169 - {debugInfo.repoInfo.collections.map((collection) => ( 170 - <li class="bg-purple-100 dark:bg-purple-800 px-2 py-1 rounded text-xs"> 171 - {collection} 172 - </li> 173 - ))} 174 - </ul> 175 - </div> 176 - </div> 177 - </div> 178 - )} 179 - 180 - <!-- Raw Debug Info --> 181 - <div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-6"> 182 - <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2"> 183 - Raw Debug Information 184 - </h3> 185 - <div class="text-sm text-gray-600 dark:text-gray-400"> 186 - <pre class="bg-gray-100 dark:bg-gray-700 p-3 rounded text-xs overflow-x-auto"> 187 - {JSON.stringify(debugInfo, null, 2)} 188 - </pre> 189 - </div> 190 - </div> 191 - </> 192 - )} 193 - </main> 194 - </div> 195 - </Layout>
-176
src/pages/atproto-browser-test.astro
··· 1 - --- 2 - import Layout from '../layouts/Layout.astro'; 3 - import { loadConfig } from '../lib/config/site'; 4 - 5 - const config = loadConfig(); 6 - --- 7 - 8 - <Layout title="ATProto Browser Test"> 9 - <div class="container mx-auto px-4 py-8"> 10 - <h1 class="text-4xl font-bold mb-8">ATProto Browser Test</h1> 11 - 12 - <div class="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-8"> 13 - <h2 class="text-2xl font-semibold mb-4">Configuration</h2> 14 - <p><strong>Handle:</strong> {config.atproto.handle}</p> 15 - <p><strong>DID:</strong> {config.atproto.did}</p> 16 - <p class="text-sm text-gray-600 mt-2">Browse ATProto accounts and records like atptools.</p> 17 - </div> 18 - 19 - <div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> 20 - <div class="bg-white border border-gray-200 rounded-lg p-6"> 21 - <h2 class="text-2xl font-semibold mb-4">Browse Account</h2> 22 - <div class="space-y-4"> 23 - <div> 24 - <label class="block text-sm font-medium text-gray-700 mb-2">Account (handle or DID)</label> 25 - <input id="account-input" type="text" value={config.atproto.handle} class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> 26 - </div> 27 - <button id="browse-btn" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"> 28 - Browse Account 29 - </button> 30 - </div> 31 - </div> 32 - 33 - <div class="bg-white border border-gray-200 rounded-lg p-6"> 34 - <h2 class="text-2xl font-semibold mb-4">Account Info</h2> 35 - <div id="account-info" class="space-y-2"> 36 - <p class="text-gray-500">Enter an account to browse...</p> 37 - </div> 38 - </div> 39 - </div> 40 - 41 - <div class="mt-8"> 42 - <h2 class="text-2xl font-semibold mb-4">Collections</h2> 43 - <div id="collections-container" class="space-y-4"> 44 - <p class="text-gray-500 text-center py-8">No collections loaded...</p> 45 - </div> 46 - </div> 47 - 48 - <div class="mt-8"> 49 - <h2 class="text-2xl font-semibold mb-4">Records</h2> 50 - <div id="records-container" class="space-y-4"> 51 - <p class="text-gray-500 text-center py-8">No records loaded...</p> 52 - </div> 53 - </div> 54 - </div> 55 - </Layout> 56 - 57 - <script> 58 - import { AtprotoBrowser } from '../lib/atproto/atproto-browser'; 59 - 60 - const browser = new AtprotoBrowser(); 61 - let currentAccount: string | null = null; 62 - 63 - // DOM elements 64 - const accountInput = document.getElementById('account-input') as HTMLInputElement; 65 - const browseBtn = document.getElementById('browse-btn') as HTMLButtonElement; 66 - const accountInfo = document.getElementById('account-info') as HTMLDivElement; 67 - const collectionsContainer = document.getElementById('collections-container') as HTMLDivElement; 68 - const recordsContainer = document.getElementById('records-container') as HTMLDivElement; 69 - 70 - function displayAccountInfo(repoInfo: any) { 71 - accountInfo.innerHTML = ` 72 - <div class="space-y-2"> 73 - <div><strong>DID:</strong> <span class="font-mono text-sm">${repoInfo.did}</span></div> 74 - <div><strong>Handle:</strong> ${repoInfo.handle}</div> 75 - <div><strong>Collections:</strong> ${repoInfo.collections.length}</div> 76 - <div><strong>Total Records:</strong> ${repoInfo.recordCount}</div> 77 - ${repoInfo.profile ? `<div><strong>Display Name:</strong> ${repoInfo.profile.displayName || 'N/A'}</div>` : ''} 78 - </div> 79 - `; 80 - } 81 - 82 - function displayCollections(collections: string[]) { 83 - if (collections.length === 0) { 84 - collectionsContainer.innerHTML = '<p class="text-gray-500 text-center py-8">No collections found</p>'; 85 - return; 86 - } 87 - 88 - collectionsContainer.innerHTML = ` 89 - <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> 90 - ${collections.map(collection => ` 91 - <div class="bg-gray-50 border border-gray-200 rounded-lg p-4 cursor-pointer hover:bg-gray-100 transition-colors" onclick="loadCollection('${collection}')"> 92 - <h3 class="font-semibold text-gray-900">${collection}</h3> 93 - <p class="text-sm text-gray-600">Click to view records</p> 94 - </div> 95 - `).join('')} 96 - </div> 97 - `; 98 - } 99 - 100 - async function loadCollection(collection: string) { 101 - if (!currentAccount) return; 102 - 103 - try { 104 - const collectionInfo = await browser.getCollectionRecords(currentAccount, collection, 50); 105 - if (collectionInfo) { 106 - displayRecords(collectionInfo.records, collection); 107 - } 108 - } catch (error) { 109 - console.error('Error loading collection:', error); 110 - recordsContainer.innerHTML = '<p class="text-red-500 text-center py-8">Error loading collection</p>'; 111 - } 112 - } 113 - 114 - function displayRecords(records: any[], collection: string) { 115 - if (records.length === 0) { 116 - recordsContainer.innerHTML = '<p class="text-gray-500 text-center py-8">No records found in this collection</p>'; 117 - return; 118 - } 119 - 120 - recordsContainer.innerHTML = ` 121 - <div class="mb-4"> 122 - <h3 class="text-lg font-semibold">${collection} (${records.length} records)</h3> 123 - </div> 124 - <div class="space-y-4"> 125 - ${records.map(record => ` 126 - <div class="bg-white border border-gray-200 rounded-lg p-4"> 127 - <div class="flex items-center space-x-2 mb-2"> 128 - <span class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs font-medium">${record.collection}</span> 129 - <span class="bg-purple-100 text-purple-800 px-2 py-1 rounded text-xs font-medium">${record.$type}</span> 130 - </div> 131 - ${record.value?.text ? `<p class="text-sm text-gray-600 mb-2">${record.value.text}</p>` : ''} 132 - <p class="text-xs text-gray-500">${new Date(record.indexedAt).toLocaleString()}</p> 133 - <p class="text-xs font-mono text-gray-400 mt-1 break-all">${record.uri}</p> 134 - </div> 135 - `).join('')} 136 - </div> 137 - `; 138 - } 139 - 140 - browseBtn.addEventListener('click', async () => { 141 - const account = accountInput.value.trim(); 142 - if (!account) { 143 - alert('Please enter an account handle or DID'); 144 - return; 145 - } 146 - 147 - try { 148 - browseBtn.disabled = true; 149 - browseBtn.textContent = 'Loading...'; 150 - 151 - // Get account info 152 - const repoInfo = await browser.getRepoInfo(account); 153 - if (!repoInfo) { 154 - alert('Could not load account information'); 155 - return; 156 - } 157 - 158 - currentAccount = account; 159 - displayAccountInfo(repoInfo); 160 - displayCollections(repoInfo.collections); 161 - 162 - // Clear previous records 163 - recordsContainer.innerHTML = '<p class="text-gray-500 text-center py-8">Select a collection to view records</p>'; 164 - 165 - } catch (error) { 166 - console.error('Error browsing account:', error); 167 - alert('Error browsing account. Check the console for details.'); 168 - } finally { 169 - browseBtn.disabled = false; 170 - browseBtn.textContent = 'Browse Account'; 171 - } 172 - }); 173 - 174 - // Make loadCollection available globally 175 - (window as any).loadCollection = loadCollection; 176 - </script>
-218
src/pages/content-feed.astro
··· 1 - --- 2 - import Layout from '../layouts/Layout.astro'; 3 - import { ContentSystem } from '../lib/services/content-system'; 4 - import { loadConfig } from '../lib/config/site'; 5 - 6 - const config = loadConfig(); 7 - const contentSystem = new ContentSystem(); 8 - 9 - let contentFeed = null; 10 - let error = null; 11 - 12 - try { 13 - if (config.atproto.handle && config.atproto.handle !== 'your-handle-here') { 14 - // Initialize content system (build-time gathering + runtime streaming) 15 - contentFeed = await contentSystem.initialize(config.atproto.handle, { 16 - enableStreaming: true, 17 - maxItems: 200 18 - }); 19 - } 20 - } catch (err) { 21 - error = err; 22 - console.error('Content feed: Error:', err); 23 - } 24 - 25 - // Helper function to format date 26 - const formatDate = (dateString: string) => { 27 - return new Date(dateString).toLocaleDateString('en-US', { 28 - year: 'numeric', 29 - month: 'short', 30 - day: 'numeric', 31 - hour: '2-digit', 32 - minute: '2-digit' 33 - }); 34 - }; 35 - 36 - // Helper function to get service icon 37 - const getServiceIcon = (service: string) => { 38 - const icons = { 39 - 'grain.social': '🌾', 40 - 'bsky.app': '🔵', 41 - 'sh.tangled': '🪢', 42 - 'unknown': '❓' 43 - }; 44 - return icons[service] || icons.unknown; 45 - }; 46 - 47 - // Helper function to truncate text 48 - const truncateText = (text: string, maxLength: number = 100) => { 49 - if (text.length <= maxLength) return text; 50 - return text.substring(0, maxLength) + '...'; 51 - }; 52 - --- 53 - 54 - <Layout title="Content Feed"> 55 - <div class="container mx-auto px-4 py-8"> 56 - <header class="text-center mb-12"> 57 - <h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-4"> 58 - Content Feed 59 - </h1> 60 - <p class="text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto"> 61 - All your ATProto content with real-time updates 62 - </p> 63 - </header> 64 - 65 - <main class="max-w-6xl mx-auto"> 66 - {error ? ( 67 - <div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-8"> 68 - <h3 class="text-xl font-semibold text-red-800 dark:text-red-200 mb-4"> 69 - Error Loading Content 70 - </h3> 71 - <p class="text-red-700 dark:text-red-300 mb-4"> 72 - {error instanceof Error ? error.message : String(error)} 73 - </p> 74 - </div> 75 - ) : contentFeed ? ( 76 - <div class="space-y-8"> 77 - <!-- Content Feed Stats --> 78 - <div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-6"> 79 - <h3 class="text-lg font-semibold text-blue-800 dark:text-blue-200 mb-2"> 80 - Content Feed Stats 81 - </h3> 82 - <div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm text-blue-700 dark:text-blue-300"> 83 - <div> 84 - <p class="font-semibold">Total Items</p> 85 - <p class="text-2xl">{contentFeed.totalItems}</p> 86 - </div> 87 - <div> 88 - <p class="font-semibold">Collections</p> 89 - <p class="text-2xl">{contentFeed.collections.length}</p> 90 - </div> 91 - <div> 92 - <p class="font-semibold">Last Updated</p> 93 - <p class="text-sm">{formatDate(contentFeed.lastUpdated)}</p> 94 - </div> 95 - <div> 96 - <p class="font-semibold">Status</p> 97 - <p class="text-sm" id="streaming-status">Loading...</p> 98 - </div> 99 - </div> 100 - </div> 101 - 102 - <!-- Content Items --> 103 - <div class="space-y-4"> 104 - <h2 class="text-2xl font-bold text-gray-900 dark:text-white"> 105 - All Content ({contentFeed.items.length} items) 106 - </h2> 107 - 108 - {contentFeed.items.length > 0 ? ( 109 - <div class="space-y-4" id="content-feed"> 110 - {contentFeed.items.map((item, index) => ( 111 - <article class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6"> 112 - <header class="flex items-start justify-between mb-4"> 113 - <div class="flex items-center gap-3"> 114 - <span class="text-2xl">{getServiceIcon(item.service)}</span> 115 - <div> 116 - <h3 class="text-lg font-semibold text-gray-900 dark:text-white"> 117 - {item.$type} 118 - </h3> 119 - <p class="text-sm text-gray-500 dark:text-gray-400"> 120 - Collection: {item.collection} 121 - </p> 122 - </div> 123 - </div> 124 - <div class="text-right text-sm text-gray-500 dark:text-gray-400"> 125 - <p>{formatDate(item.createdAt)}</p> 126 - <p class="text-xs">{item.operation || 'existing'}</p> 127 - </div> 128 - </header> 129 - 130 - <div class="mb-4"> 131 - <div class="flex items-center gap-2 mb-2"> 132 - <span class="bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 px-2 py-1 rounded text-xs"> 133 - {item.service} 134 - </span> 135 - <span class="bg-blue-100 dark:bg-blue-700 text-blue-600 dark:text-blue-300 px-2 py-1 rounded text-xs"> 136 - {item.$type} 137 - </span> 138 - </div> 139 - 140 - {item.value?.text && ( 141 - <p class="text-gray-900 dark:text-white mb-3"> 142 - {truncateText(item.value.text, 200)} 143 - </p> 144 - )} 145 - 146 - {item.value?.title && ( 147 - <p class="font-semibold text-gray-900 dark:text-white mb-2"> 148 - {item.value.title} 149 - </p> 150 - )} 151 - 152 - {item.value?.description && ( 153 - <p class="text-gray-600 dark:text-gray-400 mb-3"> 154 - {truncateText(item.value.description, 150)} 155 - </p> 156 - )} 157 - </div> 158 - 159 - <footer class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400"> 160 - <span>URI: {item.uri.substring(0, 50)}...</span> 161 - <span>CID: {item.cid.substring(0, 10)}...</span> 162 - </footer> 163 - </article> 164 - ))} 165 - </div> 166 - ) : ( 167 - <div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-8"> 168 - <h3 class="text-xl font-semibold text-yellow-800 dark:text-yellow-200 mb-4"> 169 - No Content Found 170 - </h3> 171 - <p class="text-yellow-700 dark:text-yellow-300 mb-4"> 172 - No content was found for your account. This could mean: 173 - </p> 174 - <ul class="text-yellow-600 dark:text-yellow-400 text-sm list-disc list-inside space-y-1"> 175 - <li>You haven't posted any content yet</li> 176 - <li>The content is in a different collection</li> 177 - <li>There's an issue with the API connection</li> 178 - <li>The content format isn't recognized</li> 179 - </ul> 180 - </div> 181 - )} 182 - </div> 183 - </div> 184 - ) : ( 185 - <div class="text-center py-12"> 186 - <div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-8"> 187 - <h3 class="text-xl font-semibold text-yellow-800 dark:text-yellow-200 mb-4"> 188 - Configuration Required 189 - </h3> 190 - <p class="text-yellow-700 dark:text-yellow-300 mb-4"> 191 - To view your content feed, please configure your Bluesky handle in the environment variables. 192 - </p> 193 - <div class="text-sm text-yellow-600 dark:text-yellow-400"> 194 - <p class="mb-2">Create a <code class="bg-yellow-100 dark:bg-yellow-800 px-1 rounded">.env</code> file with:</p> 195 - <pre class="bg-yellow-100 dark:bg-yellow-800 p-3 rounded text-xs overflow-x-auto"> 196 - ATPROTO_HANDLE=your-handle.bsky.social 197 - SITE_TITLE=Your Site Title 198 - SITE_AUTHOR=Your Name</pre> 199 - </div> 200 - </div> 201 - </div> 202 - )} 203 - </main> 204 - </div> 205 - </Layout> 206 - 207 - <script> 208 - // Real-time updates (client-side) 209 - if (typeof window !== 'undefined') { 210 - // This would be implemented with WebSocket or Server-Sent Events 211 - // For now, we'll just update the streaming status 212 - const streamingStatus = document.getElementById('streaming-status'); 213 - if (streamingStatus) { 214 - streamingStatus.textContent = 'Streaming'; 215 - streamingStatus.className = 'text-sm text-green-600 dark:text-green-400'; 216 - } 217 - } 218 - </script>
-163
src/pages/content-test.astro
··· 1 - --- 2 - import Layout from '../layouts/Layout.astro'; 3 - import { ContentRenderer, type RenderedContent } from '../lib/services/content-renderer'; 4 - import { loadConfig } from '../lib/config/site'; 5 - 6 - const config = loadConfig(); 7 - const contentRenderer = new ContentRenderer(); 8 - 9 - let content: RenderedContent[] = []; 10 - let contentTypes: string[] = []; 11 - let error: unknown = null; 12 - 13 - try { 14 - if (config.atproto.handle && config.atproto.handle !== 'your-handle-here') { 15 - // Get available content types 16 - contentTypes = await contentRenderer.getContentTypes(config.atproto.handle); 17 - 18 - // Render content with gallery filter 19 - content = await contentRenderer.renderContent(config.atproto.handle, { 20 - limit: 20, 21 - filter: (record) => { 22 - const $type = record.value?.$type; 23 - return $type?.includes('gallery') || 24 - $type?.includes('grain') || 25 - $type?.includes('image'); 26 - } 27 - }); 28 - } 29 - } catch (err) { 30 - error = err; 31 - console.error('Content test: Error rendering content:', err); 32 - } 33 - --- 34 - 35 - <Layout title="Content Rendering Test"> 36 - <div class="container mx-auto px-4 py-8"> 37 - <header class="text-center mb-12"> 38 - <h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-4"> 39 - Content Rendering Test 40 - </h1> 41 - <p class="text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto"> 42 - Testing the content rendering system with type-safe components 43 - </p> 44 - </header> 45 - 46 - <main class="max-w-6xl mx-auto"> 47 - {error ? ( 48 - <div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-8"> 49 - <h3 class="text-xl font-semibold text-red-800 dark:text-red-200 mb-4"> 50 - Error Loading Content 51 - </h3> 52 - <p class="text-red-700 dark:text-red-300 mb-4"> 53 - {error instanceof Error ? error.message : String(error)} 54 - </p> 55 - <pre class="bg-red-100 dark:bg-red-800 p-3 rounded text-xs overflow-x-auto"> 56 - {JSON.stringify(error, null, 2)} 57 - </pre> 58 - </div> 59 - ) : config.atproto.handle && config.atproto.handle !== 'your-handle-here' ? ( 60 - <div class="space-y-8"> 61 - <div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-6"> 62 - <h3 class="text-lg font-semibold text-blue-800 dark:text-blue-200 mb-2"> 63 - Configuration 64 - </h3> 65 - <div class="text-sm text-blue-700 dark:text-blue-300"> 66 - <p><strong>Handle:</strong> {config.atproto.handle}</p> 67 - <p><strong>DID:</strong> {config.atproto.did}</p> 68 - <p><strong>PDS URL:</strong> {config.atproto.pdsUrl}</p> 69 - </div> 70 - </div> 71 - 72 - <div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-6"> 73 - <h3 class="text-lg font-semibold text-green-800 dark:text-green-200 mb-2"> 74 - Content Types Found 75 - </h3> 76 - <div class="text-sm text-green-700 dark:text-green-300"> 77 - <p><strong>Total Types:</strong> {contentTypes.length}</p> 78 - {contentTypes.length > 0 && ( 79 - <div class="mt-2"> 80 - <p class="font-semibold">Types:</p> 81 - <ul class="list-disc list-inside space-y-1"> 82 - {contentTypes.map((type) => ( 83 - <li class="bg-green-100 dark:bg-green-800 px-2 py-1 rounded text-xs"> 84 - {type} 85 - </li> 86 - ))} 87 - </ul> 88 - </div> 89 - )} 90 - </div> 91 - </div> 92 - 93 - <div class="bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg p-6"> 94 - <h3 class="text-lg font-semibold text-purple-800 dark:text-purple-200 mb-2"> 95 - Rendered Content 96 - </h3> 97 - <div class="text-sm text-purple-700 dark:text-purple-300"> 98 - <p><strong>Content Items:</strong> {content.length}</p> 99 - {content.length > 0 && ( 100 - <div class="mt-2"> 101 - <p class="font-semibold">Items:</p> 102 - <ul class="space-y-2"> 103 - {content.map((item, index) => ( 104 - <li class="bg-purple-100 dark:bg-purple-800 p-3 rounded"> 105 - <div class="flex justify-between items-start"> 106 - <div> 107 - <p class="font-semibold">Item {index + 1}</p> 108 - <p class="text-xs">Type: {item.type}</p> 109 - <p class="text-xs">Component: {item.component}</p> 110 - <p class="text-xs">$type: {item.metadata.$type}</p> 111 - </div> 112 - <div class="text-right text-xs"> 113 - <p>Collection: {item.metadata.collection}</p> 114 - <p>Created: {new Date(item.metadata.createdAt).toLocaleDateString()}</p> 115 - </div> 116 - </div> 117 - </li> 118 - ))} 119 - </ul> 120 - </div> 121 - )} 122 - </div> 123 - </div> 124 - 125 - {content.length === 0 && ( 126 - <div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-8"> 127 - <h3 class="text-xl font-semibold text-yellow-800 dark:text-yellow-200 mb-4"> 128 - No Gallery Content Found 129 - </h3> 130 - <p class="text-yellow-700 dark:text-yellow-300 mb-4"> 131 - No gallery-related content was found for your account. This could mean: 132 - </p> 133 - <ul class="text-yellow-600 dark:text-yellow-400 text-sm list-disc list-inside space-y-1"> 134 - <li>You haven't created any galleries yet</li> 135 - <li>The galleries are in a different collection</li> 136 - <li>There's an issue with the API connection</li> 137 - <li>The gallery format isn't recognized</li> 138 - </ul> 139 - </div> 140 - )} 141 - </div> 142 - ) : ( 143 - <div class="text-center py-12"> 144 - <div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-8"> 145 - <h3 class="text-xl font-semibold text-yellow-800 dark:text-yellow-200 mb-4"> 146 - Configuration Required 147 - </h3> 148 - <p class="text-yellow-700 dark:text-yellow-300 mb-4"> 149 - To test the content rendering system, please configure your Bluesky handle in the environment variables. 150 - </p> 151 - <div class="text-sm text-yellow-600 dark:text-yellow-400"> 152 - <p class="mb-2">Create a <code class="bg-yellow-100 dark:bg-yellow-800 px-1 rounded">.env</code> file with:</p> 153 - <pre class="bg-yellow-100 dark:bg-yellow-800 p-3 rounded text-xs overflow-x-auto"> 154 - ATPROTO_HANDLE=your-handle.bsky.social 155 - SITE_TITLE=Your Site Title 156 - SITE_AUTHOR=Your Name</pre> 157 - </div> 158 - </div> 159 - </div> 160 - )} 161 - </main> 162 - </div> 163 - </Layout>
-202
src/pages/discovery-test.astro
··· 1 - --- 2 - import Layout from '../layouts/Layout.astro'; 3 - import { CollectionDiscovery } from '../lib/build/collection-discovery'; 4 - import { DiscoveredComponentRegistry } from '../lib/components/discovered-registry'; 5 - import { loadConfig } from '../lib/config/site'; 6 - 7 - const config = loadConfig(); 8 - const discovery = new CollectionDiscovery(); 9 - const registry = new DiscoveredComponentRegistry(); 10 - 11 - let discoveryResults = null; 12 - let error = null; 13 - 14 - try { 15 - if (config.atproto.handle && config.atproto.handle !== 'your-handle-here') { 16 - // Run collection discovery 17 - discoveryResults = await discovery.discoverCollections(config.atproto.handle); 18 - 19 - // Update component registry with discovered types 20 - const allTypes = discoveryResults.collections.flatMap(c => c.$types); 21 - registry.updateDiscoveredTypes(allTypes); 22 - } 23 - } catch (err) { 24 - error = err; 25 - console.error('Discovery test error:', err); 26 - } 27 - 28 - // Helper function to format date 29 - const formatDate = (dateString: string) => { 30 - return new Date(dateString).toLocaleDateString('en-US', { 31 - year: 'numeric', 32 - month: 'long', 33 - day: 'numeric', 34 - hour: '2-digit', 35 - minute: '2-digit' 36 - }); 37 - }; 38 - --- 39 - 40 - <Layout title="Discovery Test"> 41 - <div class="container mx-auto px-4 py-8"> 42 - <header class="text-center mb-12"> 43 - <h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-4"> 44 - Build-Time Discovery Test 45 - </h1> 46 - <p class="text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto"> 47 - Testing collection discovery and type generation 48 - </p> 49 - </header> 50 - 51 - <main class="max-w-6xl mx-auto"> 52 - {error ? ( 53 - <div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-8"> 54 - <h3 class="text-xl font-semibold text-red-800 dark:text-red-200 mb-4"> 55 - Discovery Error 56 - </h3> 57 - <p class="text-red-700 dark:text-red-300 mb-4"> 58 - {error instanceof Error ? error.message : String(error)} 59 - </p> 60 - <pre class="bg-red-100 dark:bg-red-800 p-3 rounded text-xs overflow-x-auto"> 61 - {JSON.stringify(error, null, 2)} 62 - </pre> 63 - </div> 64 - ) : discoveryResults ? ( 65 - <div class="space-y-8"> 66 - <!-- Discovery Summary --> 67 - <div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-6"> 68 - <h3 class="text-lg font-semibold text-blue-800 dark:text-blue-200 mb-2"> 69 - Discovery Summary 70 - </h3> 71 - <div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm text-blue-700 dark:text-blue-300"> 72 - <div> 73 - <p class="font-semibold">Collections</p> 74 - <p class="text-2xl">{discoveryResults.totalCollections}</p> 75 - </div> 76 - <div> 77 - <p class="font-semibold">Records</p> 78 - <p class="text-2xl">{discoveryResults.totalRecords}</p> 79 - </div> 80 - <div> 81 - <p class="font-semibold">Repository</p> 82 - <p class="text-sm">{discoveryResults.repository.handle}</p> 83 - </div> 84 - <div> 85 - <p class="font-semibold">Generated</p> 86 - <p class="text-sm">{formatDate(discoveryResults.generatedAt)}</p> 87 - </div> 88 - </div> 89 - </div> 90 - 91 - <!-- Discovered Collections --> 92 - <div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-6"> 93 - <h3 class="text-lg font-semibold text-green-800 dark:text-green-200 mb-2"> 94 - Discovered Collections 95 - </h3> 96 - <div class="space-y-4"> 97 - {discoveryResults.collections.map((collection, index) => ( 98 - <div class="bg-green-100 dark:bg-green-800 p-4 rounded"> 99 - <div class="flex items-start justify-between mb-2"> 100 - <div> 101 - <h4 class="font-semibold text-green-800 dark:text-green-200"> 102 - {collection.name} 103 - </h4> 104 - <p class="text-sm text-green-600 dark:text-green-400"> 105 - {collection.description} 106 - </p> 107 - </div> 108 - <span class="bg-green-200 dark:bg-green-700 text-green-800 dark:text-green-200 px-2 py-1 rounded text-xs"> 109 - {collection.service} 110 - </span> 111 - </div> 112 - 113 - <div class="mb-3"> 114 - <p class="text-sm text-green-600 dark:text-green-400 mb-1"> 115 - Types: {collection.$types.length} 116 - </p> 117 - <div class="flex flex-wrap gap-1"> 118 - {collection.$types.map($type => ( 119 - <span class="bg-green-200 dark:bg-green-700 text-green-800 dark:text-green-200 px-2 py-1 rounded text-xs"> 120 - {$type} 121 - </span> 122 - ))} 123 - </div> 124 - </div> 125 - 126 - <details class="text-sm"> 127 - <summary class="cursor-pointer text-green-600 dark:text-green-400"> 128 - View Generated Types 129 - </summary> 130 - <pre class="bg-green-200 dark:bg-green-700 p-3 rounded text-xs overflow-x-auto mt-2"> 131 - {collection.generatedTypes} 132 - </pre> 133 - </details> 134 - </div> 135 - ))} 136 - </div> 137 - </div> 138 - 139 - <!-- Component Registry --> 140 - <div class="bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg p-6"> 141 - <h3 class="text-lg font-semibold text-purple-800 dark:text-purple-200 mb-2"> 142 - Component Registry 143 - </h3> 144 - <div class="text-sm text-purple-700 dark:text-purple-300"> 145 - <p><strong>Registered Types:</strong> {registry.getRegisteredTypes().length}</p> 146 - <div class="mt-2"> 147 - <p class="font-semibold">Component Mappings:</p> 148 - <div class="grid grid-cols-1 md:grid-cols-2 gap-2 mt-2"> 149 - {registry.getRegisteredTypes().map($type => { 150 - const component = registry.getComponent($type); 151 - return ( 152 - <div class="bg-purple-100 dark:bg-purple-800 p-2 rounded text-xs"> 153 - <span class="font-semibold">{$type}</span> 154 - <br /> 155 - <span class="text-purple-600 dark:text-purple-400"> 156 - → {component?.component || 'No component'} 157 - </span> 158 - </div> 159 - ); 160 - })} 161 - </div> 162 - </div> 163 - </div> 164 - </div> 165 - 166 - <!-- Raw Discovery Data --> 167 - <div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-6"> 168 - <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2"> 169 - Raw Discovery Data 170 - </h3> 171 - <div class="text-sm text-gray-600 dark:text-gray-400"> 172 - <details> 173 - <summary class="cursor-pointer">View Raw Data</summary> 174 - <pre class="bg-gray-100 dark:bg-gray-700 p-3 rounded text-xs overflow-x-auto mt-2"> 175 - {JSON.stringify(discoveryResults, null, 2)} 176 - </pre> 177 - </details> 178 - </div> 179 - </div> 180 - </div> 181 - ) : ( 182 - <div class="text-center py-12"> 183 - <div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-8"> 184 - <h3 class="text-xl font-semibold text-yellow-800 dark:text-yellow-200 mb-4"> 185 - Configuration Required 186 - </h3> 187 - <p class="text-yellow-700 dark:text-yellow-300 mb-4"> 188 - To test collection discovery, please configure your Bluesky handle in the environment variables. 189 - </p> 190 - <div class="text-sm text-yellow-600 dark:text-yellow-400"> 191 - <p class="mb-2">Create a <code class="bg-yellow-100 dark:bg-yellow-800 px-1 rounded">.env</code> file with:</p> 192 - <pre class="bg-yellow-100 dark:bg-yellow-800 p-3 rounded text-xs overflow-x-auto"> 193 - ATPROTO_HANDLE=your-handle.bsky.social 194 - SITE_TITLE=Your Site Title 195 - SITE_AUTHOR=Your Name</pre> 196 - </div> 197 - </div> 198 - </div> 199 - )} 200 - </main> 201 - </div> 202 - </Layout>
-165
src/pages/galleries-unified.astro
··· 1 - --- 2 - import Layout from '../layouts/Layout.astro'; 3 - import GrainGalleryDisplay from '../components/content/GrainGalleryDisplay.astro'; 4 - import { ContentSystem } from '../lib/services/content-system'; 5 - import { loadConfig } from '../lib/config/site'; 6 - 7 - const config = loadConfig(); 8 - const contentSystem = new ContentSystem(); 9 - 10 - let galleries = []; 11 - let contentFeed = null; 12 - let error = null; 13 - 14 - try { 15 - if (config.atproto.handle && config.atproto.handle !== 'your-handle-here') { 16 - // Initialize content system 17 - contentFeed = await contentSystem.initialize(config.atproto.handle, { 18 - enableStreaming: true, 19 - maxItems: 500 20 - }); 21 - 22 - // Get galleries using the specialized service 23 - galleries = await contentSystem.getGalleries(config.atproto.handle); 24 - } 25 - } catch (err) { 26 - error = err; 27 - console.error('Unified galleries: Error:', err); 28 - } 29 - 30 - // Helper function to format date 31 - const formatDate = (dateString: string) => { 32 - return new Date(dateString).toLocaleDateString('en-US', { 33 - year: 'numeric', 34 - month: 'long', 35 - day: 'numeric', 36 - }); 37 - }; 38 - --- 39 - 40 - <Layout title="Unified Galleries"> 41 - <div class="container mx-auto px-4 py-8"> 42 - <header class="text-center mb-12"> 43 - <h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-4"> 44 - Unified Galleries 45 - </h1> 46 - <p class="text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto"> 47 - Your Grain.social galleries with real-time updates 48 - </p> 49 - </header> 50 - 51 - <main class="max-w-6xl mx-auto"> 52 - {error ? ( 53 - <div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-8"> 54 - <h3 class="text-xl font-semibold text-red-800 dark:text-red-200 mb-4"> 55 - Error Loading Galleries 56 - </h3> 57 - <p class="text-red-700 dark:text-red-300 mb-4"> 58 - {error instanceof Error ? error.message : String(error)} 59 - </p> 60 - </div> 61 - ) : contentFeed ? ( 62 - <div class="space-y-8"> 63 - <!-- Content System Stats --> 64 - <div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-6"> 65 - <h3 class="text-lg font-semibold text-blue-800 dark:text-blue-200 mb-2"> 66 - Content System Stats 67 - </h3> 68 - <div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm text-blue-700 dark:text-blue-300"> 69 - <div> 70 - <p class="font-semibold">Total Content Items</p> 71 - <p class="text-2xl">{contentFeed.totalItems}</p> 72 - </div> 73 - <div> 74 - <p class="font-semibold">Collections</p> 75 - <p class="text-2xl">{contentFeed.collections.length}</p> 76 - </div> 77 - <div> 78 - <p class="font-semibold">Galleries Found</p> 79 - <p class="text-2xl">{galleries.length}</p> 80 - </div> 81 - <div> 82 - <p class="font-semibold">Last Updated</p> 83 - <p class="text-sm">{formatDate(contentFeed.lastUpdated)}</p> 84 - </div> 85 - </div> 86 - </div> 87 - 88 - <!-- Gallery Content --> 89 - <div class="space-y-4"> 90 - <h2 class="text-2xl font-bold text-gray-900 dark:text-white"> 91 - Your Galleries ({galleries.length} galleries) 92 - </h2> 93 - 94 - {galleries.length > 0 ? ( 95 - <div class="space-y-8"> 96 - {galleries.map((gallery) => ( 97 - <GrainGalleryDisplay 98 - gallery={gallery} 99 - showDescription={true} 100 - showTimestamp={true} 101 - showCollections={true} 102 - columns={3} 103 - /> 104 - ))} 105 - </div> 106 - ) : ( 107 - <div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-8"> 108 - <h3 class="text-xl font-semibold text-yellow-800 dark:text-yellow-200 mb-4"> 109 - No Galleries Found 110 - </h3> 111 - <p class="text-yellow-700 dark:text-yellow-300 mb-4"> 112 - No Grain.social galleries were found for your account. This could mean: 113 - </p> 114 - <ul class="text-yellow-600 dark:text-yellow-400 text-sm list-disc list-inside space-y-1"> 115 - <li>You haven't created any galleries yet</li> 116 - <li>The galleries are in a different collection</li> 117 - <li>There's an issue with the API connection</li> 118 - <li>The gallery format isn't recognized</li> 119 - <li>The gallery grouping logic needs adjustment</li> 120 - </ul> 121 - </div> 122 - )} 123 - </div> 124 - 125 - <!-- Raw Content Debug --> 126 - <div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-6"> 127 - <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4"> 128 - Raw Content Debug 129 - </h3> 130 - <div class="text-sm text-gray-600 dark:text-gray-400"> 131 - <p><strong>Total Content Items:</strong> {contentFeed.items.length}</p> 132 - <p><strong>Collections:</strong> {contentFeed.collections.join(', ')}</p> 133 - <p><strong>Content Types Found:</strong></p> 134 - <ul class="list-disc list-inside mt-2"> 135 - {Array.from(new Set(contentFeed.items.map(item => item.$type))).map($type => ( 136 - <li class="bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded text-xs"> 137 - {$type} 138 - </li> 139 - ))} 140 - </ul> 141 - </div> 142 - </div> 143 - </div> 144 - ) : ( 145 - <div class="text-center py-12"> 146 - <div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-8"> 147 - <h3 class="text-xl font-semibold text-yellow-800 dark:text-yellow-200 mb-4"> 148 - Configuration Required 149 - </h3> 150 - <p class="text-yellow-700 dark:text-yellow-300 mb-4"> 151 - To view your galleries, please configure your Bluesky handle in the environment variables. 152 - </p> 153 - <div class="text-sm text-yellow-600 dark:text-yellow-400"> 154 - <p class="mb-2">Create a <code class="bg-yellow-100 dark:bg-yellow-800 px-1 rounded">.env</code> file with:</p> 155 - <pre class="bg-yellow-100 dark:bg-yellow-800 p-3 rounded text-xs overflow-x-auto"> 156 - ATPROTO_HANDLE=your-handle.bsky.social 157 - SITE_TITLE=Your Site Title 158 - SITE_AUTHOR=Your Name</pre> 159 - </div> 160 - </div> 161 - </div> 162 - )} 163 - </main> 164 - </div> 165 - </Layout>
-148
src/pages/gallery-debug.astro
··· 1 - --- 2 - import Layout from '../layouts/Layout.astro'; 3 - import { AtprotoBrowser } from '../lib/atproto/atproto-browser'; 4 - import { loadConfig } from '../lib/config/site'; 5 - 6 - const config = loadConfig(); 7 - const browser = new AtprotoBrowser(); 8 - 9 - let repoInfo = null; 10 - let collections = []; 11 - let grainRecords = []; 12 - let error = null; 13 - 14 - try { 15 - if (config.atproto.handle && config.atproto.handle !== 'your-handle-here') { 16 - // Get repository info 17 - repoInfo = await browser.getRepoInfo(config.atproto.handle); 18 - 19 - if (repoInfo) { 20 - // Get all collections 21 - collections = repoInfo.collections; 22 - 23 - // Get records from grain-related collections 24 - const grainCollections = collections.filter(col => 25 - col.includes('grain') || col.includes('social.grain') 26 - ); 27 - 28 - for (const collection of grainCollections) { 29 - const records = await browser.getCollectionRecords(config.atproto.handle, collection, 50); 30 - if (records && records.records) { 31 - grainRecords.push({ 32 - collection, 33 - records: records.records 34 - }); 35 - } 36 - } 37 - } 38 - } 39 - } catch (err) { 40 - error = err; 41 - console.error('Gallery debug: Error:', err); 42 - } 43 - --- 44 - 45 - <Layout title="Gallery Debug"> 46 - <div class="container mx-auto px-4 py-8"> 47 - <header class="text-center mb-12"> 48 - <h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-4"> 49 - Gallery Structure Debug 50 - </h1> 51 - <p class="text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto"> 52 - Examining Grain.social collection structure and relationships 53 - </p> 54 - </header> 55 - 56 - <main class="max-w-6xl mx-auto space-y-8"> 57 - {error ? ( 58 - <div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-8"> 59 - <h3 class="text-xl font-semibold text-red-800 dark:text-red-200 mb-4"> 60 - Error 61 - </h3> 62 - <p class="text-red-700 dark:text-red-300 mb-4"> 63 - {error instanceof Error ? error.message : String(error)} 64 - </p> 65 - </div> 66 - ) : repoInfo ? ( 67 - <> 68 - <div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-6"> 69 - <h3 class="text-lg font-semibold text-blue-800 dark:text-blue-200 mb-2"> 70 - Repository Info 71 - </h3> 72 - <div class="text-sm text-blue-700 dark:text-blue-300"> 73 - <p><strong>Handle:</strong> {repoInfo.handle}</p> 74 - <p><strong>DID:</strong> {repoInfo.did}</p> 75 - <p><strong>Total Collections:</strong> {collections.length}</p> 76 - <p><strong>Total Records:</strong> {repoInfo.recordCount}</p> 77 - </div> 78 - </div> 79 - 80 - <div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-6"> 81 - <h3 class="text-lg font-semibold text-green-800 dark:text-green-200 mb-2"> 82 - All Collections 83 - </h3> 84 - <div class="text-sm text-green-700 dark:text-green-300"> 85 - <ul class="list-disc list-inside space-y-1"> 86 - {collections.map((collection) => ( 87 - <li class="bg-green-100 dark:bg-green-800 px-2 py-1 rounded"> 88 - {collection} 89 - </li> 90 - ))} 91 - </ul> 92 - </div> 93 - </div> 94 - 95 - <div class="bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg p-6"> 96 - <h3 class="text-lg font-semibold text-purple-800 dark:text-purple-200 mb-2"> 97 - Grain.social Records 98 - </h3> 99 - <div class="text-sm text-purple-700 dark:text-purple-300"> 100 - {grainRecords.map(({ collection, records }) => ( 101 - <div class="mb-6"> 102 - <h4 class="font-semibold mb-2">{collection} ({records.length} records)</h4> 103 - <div class="space-y-2"> 104 - {records.slice(0, 5).map((record, index) => ( 105 - <div class="bg-purple-100 dark:bg-purple-800 p-3 rounded"> 106 - <div class="flex justify-between items-start"> 107 - <div> 108 - <p class="font-semibold">Record {index + 1}</p> 109 - <p class="text-xs">URI: {record.uri}</p> 110 - <p class="text-xs">$type: {record.value?.$type || 'unknown'}</p> 111 - <p class="text-xs">Created: {record.value?.createdAt || record.indexedAt}</p> 112 - </div> 113 - <div class="text-right text-xs"> 114 - <p>CID: {record.cid.substring(0, 10)}...</p> 115 - </div> 116 - </div> 117 - <details class="mt-2"> 118 - <summary class="cursor-pointer text-xs">View Record Data</summary> 119 - <pre class="text-xs bg-purple-200 dark:bg-purple-700 p-2 rounded mt-1 overflow-x-auto"> 120 - {JSON.stringify(record.value, null, 2)} 121 - </pre> 122 - </details> 123 - </div> 124 - ))} 125 - {records.length > 5 && ( 126 - <p class="text-xs italic">... and {records.length - 5} more records</p> 127 - )} 128 - </div> 129 - </div> 130 - ))} 131 - </div> 132 - </div> 133 - </> 134 - ) : ( 135 - <div class="text-center py-12"> 136 - <div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-8"> 137 - <h3 class="text-xl font-semibold text-yellow-800 dark:text-yellow-200 mb-4"> 138 - Configuration Required 139 - </h3> 140 - <p class="text-yellow-700 dark:text-yellow-300 mb-4"> 141 - To debug the gallery structure, please configure your Bluesky handle in the environment variables. 142 - </p> 143 - </div> 144 - </div> 145 - )} 146 - </main> 147 - </div> 148 - </Layout>
+8
src/pages/index.astro
··· 120 120 Build-time collection discovery and type generation. 121 121 </p> 122 122 </a> 123 + <a href="/simplified-test" class="block p-6 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 hover:shadow-md transition-shadow"> 124 + <h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2"> 125 + Simplified Content Test 126 + </h3> 127 + <p class="text-gray-600 dark:text-gray-400"> 128 + Test the simplified content service approach based on reference repository patterns. 129 + </p> 130 + </a> 123 131 <a href="/jetstream-test" class="block p-6 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 hover:shadow-md transition-shadow"> 124 132 <h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2"> 125 133 Jetstream Test
-164
src/pages/jetstream-test.astro
··· 1 - --- 2 - import Layout from '../layouts/Layout.astro'; 3 - import { loadConfig } from '../lib/config/site'; 4 - 5 - const config = loadConfig(); 6 - --- 7 - 8 - <Layout title="Jetstream Test"> 9 - <div class="container mx-auto px-4 py-8"> 10 - <h1 class="text-4xl font-bold mb-8">Jetstream Repository Test</h1> 11 - 12 - <div class="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-8"> 13 - <h2 class="text-2xl font-semibold mb-4">Configuration</h2> 14 - <p><strong>Handle:</strong> {config.atproto.handle}</p> 15 - <p><strong>DID:</strong> {config.atproto.did}</p> 16 - <p class="text-sm text-gray-600 mt-2">This uses ATProto sync API to stream all repository activity with DID filtering, similar to atptools.</p> 17 - </div> 18 - 19 - <div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> 20 - <div class="bg-white border border-gray-200 rounded-lg p-6"> 21 - <h2 class="text-2xl font-semibold mb-4">Connection</h2> 22 - <button id="start-btn" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"> 23 - Start Jetstream 24 - </button> 25 - <button id="stop-btn" class="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 ml-2" disabled> 26 - Stop Stream 27 - </button> 28 - <div id="status" class="mt-4 p-2 rounded bg-gray-100"> 29 - Status: <span id="status-text">Stopped</span> 30 - </div> 31 - </div> 32 - 33 - <div class="bg-white border border-gray-200 rounded-lg p-6"> 34 - <h2 class="text-2xl font-semibold mb-4">Statistics</h2> 35 - <div class="space-y-2"> 36 - <div>Records received: <span id="records-count" class="font-bold">0</span></div> 37 - <div>Streaming status: <span id="streaming-status" class="font-bold">Stopped</span></div> 38 - <div>Last sync: <span id="last-sync" class="font-bold">Never</span></div> 39 - </div> 40 - </div> 41 - </div> 42 - 43 - <div class="mt-8"> 44 - <h2 class="text-2xl font-semibold mb-4">Live Records</h2> 45 - <div id="records-container" class="space-y-4 max-h-96 overflow-y-auto"> 46 - <p class="text-gray-500 text-center py-8">No records received yet...</p> 47 - </div> 48 - </div> 49 - </div> 50 - </Layout> 51 - 52 - <script> 53 - import { JetstreamClient } from '../lib/atproto/jetstream-client'; 54 - 55 - let client: JetstreamClient | null = null; 56 - let recordsCount = 0; 57 - 58 - // DOM elements 59 - const startBtn = document.getElementById('start-btn') as HTMLButtonElement; 60 - const stopBtn = document.getElementById('stop-btn') as HTMLButtonElement; 61 - const statusText = document.getElementById('status-text') as HTMLSpanElement; 62 - const recordsCountEl = document.getElementById('records-count') as HTMLSpanElement; 63 - const streamingStatusEl = document.getElementById('streaming-status') as HTMLSpanElement; 64 - const lastSyncEl = document.getElementById('last-sync') as HTMLSpanElement; 65 - const recordsContainer = document.getElementById('records-container') as HTMLDivElement; 66 - 67 - function updateStatus(status: string) { 68 - statusText.textContent = status; 69 - streamingStatusEl.textContent = status; 70 - 71 - if (status === 'Streaming') { 72 - startBtn.disabled = true; 73 - stopBtn.disabled = false; 74 - } else { 75 - startBtn.disabled = false; 76 - stopBtn.disabled = true; 77 - } 78 - } 79 - 80 - function updateLastSync() { 81 - lastSyncEl.textContent = new Date().toLocaleTimeString(); 82 - } 83 - 84 - function addRecord(record: any) { 85 - recordsCount++; 86 - recordsCountEl.textContent = recordsCount.toString(); 87 - 88 - const recordEl = document.createElement('div'); 89 - recordEl.className = 'bg-gray-50 border border-gray-200 rounded-lg p-4'; 90 - 91 - const time = new Date(record.time_us / 1000).toLocaleString(); 92 - const collection = record.collection; 93 - const $type = record.$type; 94 - const service = record.service; 95 - const text = record.value?.text || 'No text content'; 96 - 97 - recordEl.innerHTML = ` 98 - <div class="flex items-start space-x-3"> 99 - <div class="flex-1 min-w-0"> 100 - <div class="flex items-center space-x-2 mb-2"> 101 - <span class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs font-medium">${service}</span> 102 - <span class="bg-green-100 text-green-800 px-2 py-1 rounded text-xs font-medium">${collection}</span> 103 - <span class="bg-purple-100 text-purple-800 px-2 py-1 rounded text-xs font-medium">${$type}</span> 104 - <span class="bg-${record.operation === 'create' ? 'green' : record.operation === 'update' ? 'yellow' : 'red'}-100 text-${record.operation === 'create' ? 'green' : record.operation === 'update' ? 'yellow' : 'red'}-800 px-2 py-1 rounded text-xs font-medium">${record.operation}</span> 105 - </div> 106 - ${text ? `<p class="text-sm text-gray-600 mb-2">${text}</p>` : ''} 107 - <p class="text-xs text-gray-500">${time}</p> 108 - <p class="text-xs font-mono text-gray-400 mt-1 break-all">${record.uri}</p> 109 - </div> 110 - </div> 111 - `; 112 - 113 - recordsContainer.insertBefore(recordEl, recordsContainer.firstChild); 114 - 115 - // Keep only the last 20 records 116 - const records = recordsContainer.querySelectorAll('div'); 117 - if (records.length > 20) { 118 - records[records.length - 1].remove(); 119 - } 120 - } 121 - 122 - startBtn.addEventListener('click', async () => { 123 - try { 124 - updateStatus('Starting...'); 125 - 126 - client = new JetstreamClient(); 127 - 128 - client.onConnect(() => { 129 - updateStatus('Streaming'); 130 - console.log('Jetstream connected'); 131 - }); 132 - 133 - client.onDisconnect(() => { 134 - updateStatus('Stopped'); 135 - console.log('Jetstream disconnected'); 136 - }); 137 - 138 - client.onError((error) => { 139 - console.error('Jetstream error:', error); 140 - updateStatus('Error'); 141 - }); 142 - 143 - client.onRecord((record) => { 144 - console.log('Record received:', record); 145 - addRecord(record); 146 - updateLastSync(); 147 - }); 148 - 149 - await client.startStreaming(); 150 - 151 - } catch (error) { 152 - console.error('Failed to start jetstream:', error); 153 - updateStatus('Error'); 154 - alert('Failed to start jetstream. Check the console for details.'); 155 - } 156 - }); 157 - 158 - stopBtn.addEventListener('click', () => { 159 - if (client) { 160 - client.stopStreaming(); 161 - client = null; 162 - } 163 - }); 164 - </script>
-371
src/pages/lexicon-generator-test.astro
··· 1 - --- 2 - import Layout from '../layouts/Layout.astro'; 3 - import { loadConfig } from '../lib/config/site'; 4 - 5 - const config = loadConfig(); 6 - --- 7 - 8 - <Layout title="Lexicon Generator Test"> 9 - <div class="container mx-auto px-4 py-8"> 10 - <h1 class="text-4xl font-bold mb-8">Lexicon Generator Test</h1> 11 - 12 - <div class="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-8"> 13 - <h2 class="text-2xl font-semibold mb-4">Configuration</h2> 14 - <p><strong>Handle:</strong> {config.atproto.handle}</p> 15 - <p><strong>DID:</strong> {config.atproto.did}</p> 16 - <p class="text-sm text-gray-600 mt-2">Generate TypeScript types for all lexicons discovered in your configured repository.</p> 17 - </div> 18 - 19 - <div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> 20 - <div class="bg-white border border-gray-200 rounded-lg p-6"> 21 - <h2 class="text-2xl font-semibold mb-4">Generate Types</h2> 22 - <div class="space-y-4"> 23 - <p class="text-sm text-gray-600">Generating types for: <strong>{config.atproto.handle}</strong></p> 24 - <button id="generate-btn" class="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700"> 25 - Generate Types for My Repository 26 - </button> 27 - </div> 28 - </div> 29 - 30 - <div class="bg-white border border-gray-200 rounded-lg p-6"> 31 - <h2 class="text-2xl font-semibold mb-4">Generation Status</h2> 32 - <div id="status" class="space-y-2"> 33 - <p class="text-gray-500">Click generate to analyze your repository...</p> 34 - </div> 35 - </div> 36 - </div> 37 - 38 - <div class="mt-8"> 39 - <h2 class="text-2xl font-semibold mb-4">Discovered Lexicons</h2> 40 - <div id="lexicons-container" class="space-y-4"> 41 - <p class="text-gray-500 text-center py-8">No lexicons discovered yet...</p> 42 - </div> 43 - </div> 44 - 45 - <div class="mt-8"> 46 - <h2 class="text-2xl font-semibold mb-4">Generated TypeScript Types</h2> 47 - <div class="bg-gray-900 text-green-400 p-4 rounded-lg"> 48 - <pre id="types-output" class="text-sm overflow-x-auto">// Generated types will appear here...</pre> 49 - </div> 50 - <div class="mt-4"> 51 - <button id="copy-btn" class="bg-gray-600 text-white px-4 py-2 rounded hover:bg-gray-700" disabled> 52 - Copy to Clipboard 53 - </button> 54 - <button id="download-btn" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 ml-2" disabled> 55 - Download Types File 56 - </button> 57 - </div> 58 - </div> 59 - </div> 60 - </Layout> 61 - 62 - <script> 63 - // Simple lexicon generator for your configured account 64 - class SimpleLexiconGenerator { 65 - constructor() { 66 - this.config = { 67 - handle: 'tynanpurdy.com', 68 - did: 'did:plc:6ayddqghxhciedbaofoxkcbs', 69 - pdsUrl: 'https://bsky.social' 70 - }; 71 - } 72 - 73 - async generateTypesForRepository() { 74 - console.log('🔍 Generating types for repository:', this.config.handle); 75 - 76 - const lexicons = []; 77 - const collectionTypes = {}; 78 - 79 - try { 80 - // Get all collections in the repository 81 - const collections = await this.getAllCollections(); 82 - console.log(`📊 Found ${collections.length} collections:`, collections); 83 - 84 - // Analyze each collection 85 - for (const collection of collections) { 86 - console.log(`🔍 Analyzing collection: ${collection}`); 87 - 88 - try { 89 - const collectionInfo = await this.getCollectionRecords(collection, 100); 90 - if (collectionInfo && collectionInfo.records.length > 0) { 91 - // Group records by type 92 - const typeGroups = new Map(); 93 - 94 - collectionInfo.records.forEach(record => { 95 - const $type = record.$type; 96 - if (!typeGroups.has($type)) { 97 - typeGroups.set($type, []); 98 - } 99 - typeGroups.get($type).push(record); 100 - }); 101 - 102 - // Create lexicon definitions for each type 103 - typeGroups.forEach((records, $type) => { 104 - const sampleRecord = records[0]; 105 - const properties = this.extractProperties(sampleRecord.value); 106 - 107 - const lexicon = { 108 - $type, 109 - collection, 110 - properties, 111 - sampleRecord, 112 - description: `Discovered in collection ${collection}` 113 - }; 114 - 115 - lexicons.push(lexicon); 116 - 117 - // Track collection types 118 - if (!collectionTypes[collection]) { 119 - collectionTypes[collection] = []; 120 - } 121 - collectionTypes[collection].push($type); 122 - 123 - console.log(`✅ Generated lexicon for ${$type} in ${collection}`); 124 - }); 125 - } 126 - } catch (error) { 127 - console.error(`❌ Error analyzing collection ${collection}:`, error); 128 - } 129 - } 130 - 131 - // Generate TypeScript type definitions 132 - const typeDefinitions = this.generateTypeScriptTypes(lexicons, collectionTypes); 133 - 134 - console.log(`🎉 Generated ${lexicons.length} lexicon definitions`); 135 - 136 - return { 137 - lexicons, 138 - typeDefinitions, 139 - collectionTypes 140 - }; 141 - 142 - } catch (error) { 143 - console.error('Error generating types:', error); 144 - throw error; 145 - } 146 - } 147 - 148 - async getAllCollections() { 149 - try { 150 - const response = await fetch(`https://bsky.social/xrpc/com.atproto.repo.describeRepo?repo=${this.config.did}`); 151 - const data = await response.json(); 152 - return data.collections || []; 153 - } catch (error) { 154 - console.error('Error getting collections:', error); 155 - return []; 156 - } 157 - } 158 - 159 - async getCollectionRecords(collection, limit = 100) { 160 - try { 161 - const response = await fetch(`https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${this.config.did}&collection=${collection}&limit=${limit}`); 162 - const data = await response.json(); 163 - 164 - const records = data.records.map(record => ({ 165 - uri: record.uri, 166 - cid: record.cid, 167 - value: record.value, 168 - indexedAt: record.indexedAt, 169 - collection: collection, 170 - $type: record.value?.$type || 'unknown', 171 - })); 172 - 173 - return { 174 - collection: collection, 175 - recordCount: records.length, 176 - records: records, 177 - cursor: data.cursor, 178 - }; 179 - } catch (error) { 180 - console.error('Error getting collection records:', error); 181 - return null; 182 - } 183 - } 184 - 185 - extractProperties(value) { 186 - const properties = {}; 187 - 188 - if (value && typeof value === 'object') { 189 - Object.keys(value).forEach(key => { 190 - if (key !== '$type') { 191 - properties[key] = { 192 - type: typeof value[key], 193 - value: value[key] 194 - }; 195 - } 196 - }); 197 - } 198 - 199 - return properties; 200 - } 201 - 202 - generateTypeScriptTypes(lexicons, collectionTypes) { 203 - let types = '// Auto-generated TypeScript types for discovered lexicons\n'; 204 - types += '// Generated from ATProto repository analysis\n\n'; 205 - 206 - // Generate interfaces for each lexicon 207 - lexicons.forEach(lexicon => { 208 - const interfaceName = this.generateInterfaceName(lexicon.$type); 209 - types += `export interface ${interfaceName} {\n`; 210 - types += ` $type: '${lexicon.$type}';\n`; 211 - 212 - Object.entries(lexicon.properties).forEach(([key, prop]) => { 213 - const type = this.getTypeScriptType(prop.type, prop.value); 214 - types += ` ${key}: ${type};\n`; 215 - }); 216 - 217 - types += '}\n\n'; 218 - }); 219 - 220 - // Generate collection type mappings 221 - types += '// Collection type mappings\n'; 222 - types += 'export interface CollectionTypes {\n'; 223 - Object.entries(collectionTypes).forEach(([collection, types]) => { 224 - types += ` '${collection}': ${types.map(t => `'${t}'`).join(' | ')};\n`; 225 - }); 226 - types += '}\n\n'; 227 - 228 - // Generate union types for all lexicons 229 - const allTypes = lexicons.map(l => this.generateInterfaceName(l.$type)); 230 - types += `export type AllLexicons = ${allTypes.join(' | ')};\n\n`; 231 - 232 - // Generate helper functions 233 - types += '// Helper functions\n'; 234 - types += 'export function isLexiconType(record: any, type: string): boolean {\n'; 235 - types += ' return record?.$type === type;\n'; 236 - types += '}\n\n'; 237 - 238 - types += 'export function getCollectionTypes(collection: string): string[] {\n'; 239 - types += ' const collectionTypes: Record<string, string[]> = {\n'; 240 - Object.entries(collectionTypes).forEach(([collection, types]) => { 241 - types += ` '${collection}': [${types.map(t => `'${t}'`).join(', ')}],\n`; 242 - }); 243 - types += ' };\n'; 244 - types += ' return collectionTypes[collection] || [];\n'; 245 - types += '}\n'; 246 - 247 - return types; 248 - } 249 - 250 - generateInterfaceName($type) { 251 - return $type 252 - .split('.') 253 - .map(part => part.charAt(0).toUpperCase() + part.slice(1)) 254 - .join(''); 255 - } 256 - 257 - getTypeScriptType(jsType, value) { 258 - switch (jsType) { 259 - case 'string': 260 - return 'string'; 261 - case 'number': 262 - return typeof value === 'number' && Number.isInteger(value) ? 'number' : 'number'; 263 - case 'boolean': 264 - return 'boolean'; 265 - case 'object': 266 - if (Array.isArray(value)) { 267 - return 'any[]'; 268 - } 269 - return 'Record<string, any>'; 270 - default: 271 - return 'any'; 272 - } 273 - } 274 - } 275 - 276 - const generator = new SimpleLexiconGenerator(); 277 - let generatedTypes = ''; 278 - 279 - // DOM elements 280 - const generateBtn = document.getElementById('generate-btn') as HTMLButtonElement; 281 - const status = document.getElementById('status') as HTMLDivElement; 282 - const lexiconsContainer = document.getElementById('lexicons-container') as HTMLDivElement; 283 - const typesOutput = document.getElementById('types-output') as HTMLPreElement; 284 - const copyBtn = document.getElementById('copy-btn') as HTMLButtonElement; 285 - const downloadBtn = document.getElementById('download-btn') as HTMLButtonElement; 286 - 287 - function updateStatus(message: string, isError = false) { 288 - status.innerHTML = ` 289 - <p class="${isError ? 'text-red-600' : 'text-green-600'}">${message}</p> 290 - `; 291 - } 292 - 293 - function displayLexicons(lexicons: any[]) { 294 - if (lexicons.length === 0) { 295 - lexiconsContainer.innerHTML = '<p class="text-gray-500 text-center py-8">No lexicons discovered</p>'; 296 - return; 297 - } 298 - 299 - lexiconsContainer.innerHTML = ` 300 - <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> 301 - ${lexicons.map(lexicon => ` 302 - <div class="bg-gray-50 border border-gray-200 rounded-lg p-4"> 303 - <h3 class="font-semibold text-gray-900 mb-2">${lexicon.$type}</h3> 304 - <p class="text-sm text-gray-600 mb-2">Collection: ${lexicon.collection}</p> 305 - <p class="text-sm text-gray-600 mb-2">Properties: ${Object.keys(lexicon.properties).length}</p> 306 - <p class="text-xs text-gray-500">${lexicon.description}</p> 307 - </div> 308 - `).join('')} 309 - </div> 310 - `; 311 - } 312 - 313 - function displayTypes(types: string) { 314 - generatedTypes = types; 315 - typesOutput.textContent = types; 316 - copyBtn.disabled = false; 317 - downloadBtn.disabled = false; 318 - } 319 - 320 - generateBtn.addEventListener('click', async () => { 321 - try { 322 - generateBtn.disabled = true; 323 - generateBtn.textContent = 'Generating...'; 324 - updateStatus('Starting type generation...'); 325 - 326 - // Generate types 327 - const result = await generator.generateTypesForRepository(); 328 - 329 - updateStatus(`Generated ${result.lexicons.length} lexicon types successfully!`); 330 - displayLexicons(result.lexicons); 331 - displayTypes(result.typeDefinitions); 332 - 333 - } catch (error) { 334 - console.error('Error generating types:', error); 335 - updateStatus('Error generating types. Check the console for details.', true); 336 - } finally { 337 - generateBtn.disabled = false; 338 - generateBtn.textContent = 'Generate Types for My Repository'; 339 - } 340 - }); 341 - 342 - copyBtn.addEventListener('click', async () => { 343 - try { 344 - await navigator.clipboard.writeText(generatedTypes); 345 - copyBtn.textContent = 'Copied!'; 346 - setTimeout(() => { 347 - copyBtn.textContent = 'Copy to Clipboard'; 348 - }, 2000); 349 - } catch (error) { 350 - console.error('Error copying to clipboard:', error); 351 - alert('Error copying to clipboard'); 352 - } 353 - }); 354 - 355 - downloadBtn.addEventListener('click', () => { 356 - try { 357 - const blob = new Blob([generatedTypes], { type: 'text/typescript' }); 358 - const url = URL.createObjectURL(blob); 359 - const a = document.createElement('a'); 360 - a.href = url; 361 - a.download = 'generated-lexicons.ts'; 362 - document.body.appendChild(a); 363 - a.click(); 364 - document.body.removeChild(a); 365 - URL.revokeObjectURL(url); 366 - } catch (error) { 367 - console.error('Error downloading file:', error); 368 - alert('Error downloading file'); 369 - } 370 - }); 371 - </script>
-99
src/pages/simple-test.astro
··· 1 - --- 2 - import Layout from '../layouts/Layout.astro'; 3 - import { loadConfig } from '../lib/config/site'; 4 - 5 - const config = loadConfig(); 6 - 7 - // Simple test - just check if we can make a basic HTTP request 8 - let testResults = { 9 - config: config, 10 - basicTest: null, 11 - error: null 12 - }; 13 - 14 - try { 15 - // Test 1: Basic HTTP request to Bluesky API 16 - const response = await fetch('https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=tynanpurdy.com'); 17 - const data = await response.json(); 18 - 19 - testResults.basicTest = { 20 - success: response.ok, 21 - status: response.status, 22 - data: data 23 - }; 24 - } catch (error) { 25 - testResults.error = error; 26 - console.error('Simple test error:', error); 27 - } 28 - --- 29 - 30 - <Layout title="Simple API Test"> 31 - <div class="container mx-auto px-4 py-8"> 32 - <header class="text-center mb-12"> 33 - <h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-4"> 34 - Simple API Test 35 - </h1> 36 - <p class="text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto"> 37 - Testing basic HTTP requests to ATProto APIs 38 - </p> 39 - </header> 40 - 41 - <main class="max-w-6xl mx-auto space-y-8"> 42 - <!-- Configuration --> 43 - <div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-6"> 44 - <h3 class="text-lg font-semibold text-blue-800 dark:text-blue-200 mb-2"> 45 - Configuration 46 - </h3> 47 - <div class="text-sm text-blue-700 dark:text-blue-300"> 48 - <pre class="bg-blue-100 dark:bg-blue-800 p-3 rounded text-xs overflow-x-auto"> 49 - {JSON.stringify(testResults.config, null, 2)} 50 - </pre> 51 - </div> 52 - </div> 53 - 54 - {testResults.error ? ( 55 - <div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-8"> 56 - <h3 class="text-xl font-semibold text-red-800 dark:text-red-200 mb-4"> 57 - Error 58 - </h3> 59 - <p class="text-red-700 dark:text-red-300 mb-4"> 60 - {testResults.error instanceof Error ? testResults.error.message : String(testResults.error)} 61 - </p> 62 - <pre class="bg-red-100 dark:bg-red-800 p-3 rounded text-xs overflow-x-auto"> 63 - {JSON.stringify(testResults.error, null, 2)} 64 - </pre> 65 - </div> 66 - ) : testResults.basicTest ? ( 67 - <div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-6"> 68 - <h3 class="text-lg font-semibold text-green-800 dark:text-green-200 mb-2"> 69 - Basic API Test 70 - </h3> 71 - <div class="text-sm text-green-700 dark:text-green-300"> 72 - <div class="flex items-center gap-2 mb-2"> 73 - <span class={`px-2 py-1 rounded text-xs ${ 74 - testResults.basicTest.success 75 - ? 'bg-green-200 text-green-800 dark:bg-green-700 dark:text-green-200' 76 - : 'bg-red-200 text-red-800 dark:bg-red-700 dark:text-red-200' 77 - }`}> 78 - {testResults.basicTest.success ? '✅ Success' : '❌ Failed'} 79 - </span> 80 - <span class="text-xs">Status: {testResults.basicTest.status}</span> 81 - </div> 82 - <pre class="bg-green-100 dark:bg-green-700 p-3 rounded text-xs overflow-x-auto"> 83 - {JSON.stringify(testResults.basicTest.data, null, 2)} 84 - </pre> 85 - </div> 86 - </div> 87 - ) : ( 88 - <div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-8"> 89 - <h3 class="text-xl font-semibold text-yellow-800 dark:text-yellow-200 mb-4"> 90 - No Test Results 91 - </h3> 92 - <p class="text-yellow-700 dark:text-yellow-300"> 93 - No test results available. Check the console for errors. 94 - </p> 95 - </div> 96 - )} 97 - </main> 98 - </div> 99 - </Layout>