A personal website powered by Astro and ATProto

not sure

+1
package.json
··· 18 18 "astro": "^5.12.8", 19 19 "dotenv": "^17.2.1", 20 20 "tailwindcss": "^4.1.11", 21 + "tsx": "^4.19.2", 21 22 "typescript": "^5.9.2" 22 23 } 23 24 }
+51
scripts/discover-collections.ts
··· 1 + #!/usr/bin/env node 2 + 3 + import { CollectionDiscovery } from '../src/lib/build/collection-discovery'; 4 + import { loadConfig } from '../src/lib/config/site'; 5 + import path from 'path'; 6 + 7 + async function main() { 8 + console.log('🚀 Starting collection discovery...'); 9 + 10 + try { 11 + const config = loadConfig(); 12 + const discovery = new CollectionDiscovery(); 13 + 14 + if (!config.atproto.handle || config.atproto.handle === 'your-handle-here') { 15 + console.error('❌ No ATProto handle configured. Please set ATPROTO_HANDLE in your environment.'); 16 + process.exit(1); 17 + } 18 + 19 + console.log(`🔍 Discovering collections for: ${config.atproto.handle}`); 20 + 21 + // Discover collections 22 + const results = await discovery.discoverCollections(config.atproto.handle); 23 + 24 + // Save results 25 + const outputPath = path.join(process.cwd(), 'src/lib/generated/discovered-types.ts'); 26 + await discovery.saveDiscoveryResults(results, outputPath); 27 + 28 + console.log('✅ Collection discovery complete!'); 29 + console.log(`📊 Summary:`); 30 + console.log(` - Collections: ${results.totalCollections}`); 31 + console.log(` - Records: ${results.totalRecords}`); 32 + console.log(` - Repository: ${results.repository.handle}`); 33 + console.log(` - Output: ${outputPath}`); 34 + 35 + // Log discovered collections 36 + console.log('\n📦 Discovered Collections:'); 37 + for (const collection of results.collections) { 38 + console.log(` - ${collection.name} (${collection.service})`); 39 + console.log(` Types: ${collection.$types.join(', ')}`); 40 + } 41 + 42 + } catch (error) { 43 + console.error('❌ Collection discovery failed:', error); 44 + process.exit(1); 45 + } 46 + } 47 + 48 + // Run if called directly 49 + if (require.main === module) { 50 + main(); 51 + }
+113
src/components/content/GalleryDisplay.astro
··· 1 + --- 2 + import type { ProcessedGallery } from '../../lib/services/gallery-service'; 3 + 4 + interface Props { 5 + gallery: ProcessedGallery; 6 + showDescription?: boolean; 7 + showTimestamp?: boolean; 8 + columns?: number; 9 + showType?: boolean; 10 + } 11 + 12 + const { 13 + gallery, 14 + showDescription = true, 15 + showTimestamp = true, 16 + columns = 3, 17 + showType = false 18 + } = Astro.props; 19 + 20 + const formatDate = (dateString: string) => { 21 + return new Date(dateString).toLocaleDateString('en-US', { 22 + year: 'numeric', 23 + month: 'long', 24 + day: 'numeric', 25 + }); 26 + }; 27 + 28 + const gridCols = { 29 + 1: 'grid-cols-1', 30 + 2: 'grid-cols-2', 31 + 3: 'grid-cols-3', 32 + 4: 'grid-cols-4', 33 + 5: 'grid-cols-5', 34 + 6: 'grid-cols-6', 35 + }[columns] || 'grid-cols-3'; 36 + 37 + // Determine image layout based on number of images 38 + const getImageLayout = (imageCount: number) => { 39 + if (imageCount === 1) return 'grid-cols-1'; 40 + if (imageCount === 2) return 'grid-cols-2'; 41 + if (imageCount === 3) return 'grid-cols-3'; 42 + if (imageCount === 4) return 'grid-cols-2 md:grid-cols-4'; 43 + return gridCols; 44 + }; 45 + 46 + const imageLayout = getImageLayout(gallery.images.length); 47 + --- 48 + 49 + <article class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6 mb-6"> 50 + <header class="mb-4"> 51 + <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2"> 52 + {gallery.title} 53 + </h2> 54 + 55 + {showDescription && gallery.description && ( 56 + <div class="text-gray-600 dark:text-gray-400 mb-3"> 57 + {gallery.description} 58 + </div> 59 + )} 60 + 61 + {gallery.text && ( 62 + <div class="text-gray-900 dark:text-white mb-4"> 63 + {gallery.text} 64 + </div> 65 + )} 66 + 67 + <div class="flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400 mb-4"> 68 + {showTimestamp && ( 69 + <span> 70 + Created on {formatDate(gallery.createdAt)} 71 + </span> 72 + )} 73 + 74 + {showType && ( 75 + <span class="bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded text-xs"> 76 + {gallery.$type} 77 + </span> 78 + )} 79 + 80 + <span class="bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 px-2 py-1 rounded text-xs"> 81 + {gallery.images.length} image{gallery.images.length !== 1 ? 's' : ''} 82 + </span> 83 + </div> 84 + </header> 85 + 86 + {gallery.images && gallery.images.length > 0 && ( 87 + <div class={`grid ${imageLayout} gap-4`}> 88 + {gallery.images.map((image, index) => ( 89 + <div class="relative group"> 90 + <img 91 + src={image.url} 92 + alt={image.alt || `Gallery image ${index + 1}`} 93 + class="w-full h-48 object-cover rounded-lg transition-transform duration-200 group-hover:scale-105" 94 + style={image.aspectRatio ? `aspect-ratio: ${image.aspectRatio.width} / ${image.aspectRatio.height}` : ''} 95 + loading="lazy" 96 + /> 97 + {image.alt && ( 98 + <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"> 99 + {image.alt} 100 + </div> 101 + )} 102 + </div> 103 + ))} 104 + </div> 105 + )} 106 + 107 + <footer class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700"> 108 + <div class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400"> 109 + <span>Collection: {gallery.collection}</span> 110 + <span>Type: {gallery.$type}</span> 111 + </div> 112 + </footer> 113 + </article>
+124
src/components/content/GrainGalleryDisplay.astro
··· 1 + --- 2 + import type { ProcessedGrainGallery } from '../../lib/services/grain-gallery-service'; 3 + 4 + interface Props { 5 + gallery: ProcessedGrainGallery; 6 + showDescription?: boolean; 7 + showTimestamp?: boolean; 8 + showCollections?: boolean; 9 + columns?: number; 10 + } 11 + 12 + const { 13 + gallery, 14 + showDescription = true, 15 + showTimestamp = true, 16 + showCollections = false, 17 + columns = 3 18 + } = Astro.props; 19 + 20 + const formatDate = (dateString: string) => { 21 + return new Date(dateString).toLocaleDateString('en-US', { 22 + year: 'numeric', 23 + month: 'long', 24 + day: 'numeric', 25 + }); 26 + }; 27 + 28 + const gridCols = { 29 + 1: 'grid-cols-1', 30 + 2: 'grid-cols-2', 31 + 3: 'grid-cols-3', 32 + 4: 'grid-cols-4', 33 + 5: 'grid-cols-5', 34 + 6: 'grid-cols-6', 35 + }[columns] || 'grid-cols-3'; 36 + 37 + // Determine image layout based on number of images 38 + const getImageLayout = (imageCount: number) => { 39 + if (imageCount === 1) return 'grid-cols-1'; 40 + if (imageCount === 2) return 'grid-cols-2'; 41 + if (imageCount === 3) return 'grid-cols-3'; 42 + if (imageCount === 4) return 'grid-cols-2 md:grid-cols-4'; 43 + return gridCols; 44 + }; 45 + 46 + const imageLayout = getImageLayout(gallery.images.length); 47 + --- 48 + 49 + <article class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6 mb-6"> 50 + <header class="mb-4"> 51 + <div class="flex items-start justify-between mb-2"> 52 + <h2 class="text-2xl font-bold text-gray-900 dark:text-white"> 53 + {gallery.title} 54 + </h2> 55 + <div class="flex items-center gap-2"> 56 + <span class="bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded text-xs"> 57 + {gallery.id} 58 + </span> 59 + </div> 60 + </div> 61 + 62 + {showDescription && gallery.description && ( 63 + <div class="text-gray-600 dark:text-gray-400 mb-3"> 64 + {gallery.description} 65 + </div> 66 + )} 67 + 68 + <div class="flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400 mb-4"> 69 + {showTimestamp && ( 70 + <span> 71 + Created on {formatDate(gallery.createdAt)} 72 + </span> 73 + )} 74 + 75 + <span class="bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 px-2 py-1 rounded text-xs"> 76 + {gallery.images.length} image{gallery.images.length !== 1 ? 's' : ''} 77 + </span> 78 + 79 + <span class="bg-green-100 dark:bg-green-700 text-green-600 dark:text-green-300 px-2 py-1 rounded text-xs"> 80 + {gallery.itemCount} item{gallery.itemCount !== 1 ? 's' : ''} 81 + </span> 82 + </div> 83 + 84 + {showCollections && gallery.collections.length > 0 && ( 85 + <div class="mb-4"> 86 + <p class="text-sm text-gray-500 dark:text-gray-400 mb-1">Collections:</p> 87 + <div class="flex flex-wrap gap-1"> 88 + {gallery.collections.map((collection) => ( 89 + <span class="bg-purple-100 dark:bg-purple-800 text-purple-600 dark:text-purple-300 px-2 py-1 rounded text-xs"> 90 + {collection} 91 + </span> 92 + ))} 93 + </div> 94 + </div> 95 + )} 96 + </header> 97 + 98 + {gallery.images && gallery.images.length > 0 && ( 99 + <div class={`grid ${imageLayout} gap-4`}> 100 + {gallery.images.map((image, index) => ( 101 + <div class="relative group"> 102 + <img 103 + src={image.url} 104 + alt={image.alt || `Gallery image ${index + 1}`} 105 + class="w-full h-48 object-cover rounded-lg transition-transform duration-200 group-hover:scale-105" 106 + loading="lazy" 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} 111 + </div> 112 + )} 113 + </div> 114 + ))} 115 + </div> 116 + )} 117 + 118 + <footer class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700"> 119 + <div class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400"> 120 + <span>Gallery ID: {gallery.id}</span> 121 + <span>Collections: {gallery.collections.join(', ')}</span> 122 + </div> 123 + </footer> 124 + </article>
+303
src/lib/build/collection-discovery.ts
··· 1 + import { AtprotoBrowser } from '../atproto/atproto-browser'; 2 + import { loadConfig } from '../config/site'; 3 + import fs from 'fs/promises'; 4 + import path from 'path'; 5 + 6 + export interface CollectionType { 7 + name: string; 8 + description: string; 9 + service: string; 10 + sampleRecords: any[]; 11 + generatedTypes: string; 12 + $types: string[]; 13 + } 14 + 15 + export interface BuildTimeDiscovery { 16 + collections: CollectionType[]; 17 + totalCollections: number; 18 + totalRecords: number; 19 + generatedAt: string; 20 + repository: { 21 + handle: string; 22 + did: string; 23 + recordCount: number; 24 + }; 25 + } 26 + 27 + export class CollectionDiscovery { 28 + private browser: AtprotoBrowser; 29 + private config: any; 30 + 31 + constructor() { 32 + this.config = loadConfig(); 33 + this.browser = new AtprotoBrowser(); 34 + } 35 + 36 + // Discover all collections and generate types 37 + async discoverCollections(identifier: string): Promise<BuildTimeDiscovery> { 38 + console.log('🔍 Starting collection discovery for:', identifier); 39 + 40 + try { 41 + // Get repository info 42 + const repoInfo = await this.browser.getRepoInfo(identifier); 43 + if (!repoInfo) { 44 + throw new Error(`Could not get repository info for: ${identifier}`); 45 + } 46 + 47 + console.log('📊 Repository info:', { 48 + handle: repoInfo.handle, 49 + did: repoInfo.did, 50 + collections: repoInfo.collections.length, 51 + recordCount: repoInfo.recordCount 52 + }); 53 + 54 + const collections: CollectionType[] = []; 55 + let totalRecords = 0; 56 + 57 + // Process each collection 58 + for (const collectionName of repoInfo.collections) { 59 + console.log(`📦 Processing collection: ${collectionName}`); 60 + 61 + const collectionType = await this.processCollection(identifier, collectionName); 62 + if (collectionType) { 63 + collections.push(collectionType); 64 + totalRecords += collectionType.sampleRecords.length; 65 + } 66 + } 67 + 68 + const discovery: BuildTimeDiscovery = { 69 + collections, 70 + totalCollections: collections.length, 71 + totalRecords, 72 + generatedAt: new Date().toISOString(), 73 + repository: { 74 + handle: repoInfo.handle, 75 + did: repoInfo.did, 76 + recordCount: repoInfo.recordCount 77 + } 78 + }; 79 + 80 + console.log(`✅ Discovery complete: ${collections.length} collections, ${totalRecords} records`); 81 + return discovery; 82 + 83 + } catch (error) { 84 + console.error('Error discovering collections:', error); 85 + throw error; 86 + } 87 + } 88 + 89 + // Process a single collection 90 + private async processCollection(identifier: string, collectionName: string): Promise<CollectionType | null> { 91 + try { 92 + // Get records from collection 93 + const records = await this.browser.getCollectionRecords(identifier, collectionName, 10); 94 + if (!records || records.records.length === 0) { 95 + console.log(`⚠️ No records found in collection: ${collectionName}`); 96 + return null; 97 + } 98 + 99 + // Group records by $type 100 + const recordsByType = new Map<string, any[]>(); 101 + for (const record of records.records) { 102 + const $type = record.$type || 'unknown'; 103 + if (!recordsByType.has($type)) { 104 + recordsByType.set($type, []); 105 + } 106 + recordsByType.get($type)!.push(record.value); 107 + } 108 + 109 + // Generate types for each $type 110 + const generatedTypes: string[] = []; 111 + const $types: string[] = []; 112 + 113 + for (const [$type, typeRecords] of recordsByType) { 114 + if ($type === 'unknown') continue; 115 + 116 + $types.push($type); 117 + const typeDefinition = this.generateTypeDefinition($type, typeRecords); 118 + generatedTypes.push(typeDefinition); 119 + } 120 + 121 + // Create collection type 122 + const collectionType: CollectionType = { 123 + name: collectionName, 124 + description: this.getCollectionDescription(collectionName), 125 + service: this.inferService(collectionName), 126 + sampleRecords: records.records.slice(0, 3).map(r => r.value), 127 + generatedTypes: generatedTypes.join('\n\n'), 128 + $types 129 + }; 130 + 131 + console.log(`✅ Processed collection ${collectionName}: ${$types.length} types`); 132 + return collectionType; 133 + 134 + } catch (error) { 135 + console.error(`Error processing collection ${collectionName}:`, error); 136 + return null; 137 + } 138 + } 139 + 140 + // Generate TypeScript type definition 141 + private generateTypeDefinition($type: string, records: any[]): string { 142 + if (records.length === 0) return ''; 143 + 144 + // Analyze the first record to understand the structure 145 + const sampleRecord = records[0]; 146 + const properties = this.extractProperties(sampleRecord); 147 + 148 + // Generate interface 149 + const interfaceName = this.$typeToInterfaceName($type); 150 + let typeDefinition = `export interface ${interfaceName} {\n`; 151 + 152 + // Add $type property 153 + typeDefinition += ` $type: '${$type}';\n`; 154 + 155 + // Add other properties 156 + for (const [key, value] of Object.entries(properties)) { 157 + const type = this.inferType(value); 158 + typeDefinition += ` ${key}?: ${type};\n`; 159 + } 160 + 161 + typeDefinition += '}\n'; 162 + 163 + return typeDefinition; 164 + } 165 + 166 + // Extract properties from a record 167 + private extractProperties(obj: any, maxDepth: number = 3, currentDepth: number = 0): Record<string, any> { 168 + if (currentDepth >= maxDepth || !obj || typeof obj !== 'object') { 169 + return {}; 170 + } 171 + 172 + const properties: Record<string, any> = {}; 173 + 174 + for (const [key, value] of Object.entries(obj)) { 175 + if (key === '$type') continue; // Skip $type as it's handled separately 176 + 177 + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { 178 + // Nested object 179 + const nestedProps = this.extractProperties(value, maxDepth, currentDepth + 1); 180 + if (Object.keys(nestedProps).length > 0) { 181 + properties[key] = nestedProps; 182 + } 183 + } else { 184 + // Simple property 185 + properties[key] = value; 186 + } 187 + } 188 + 189 + return properties; 190 + } 191 + 192 + // Infer TypeScript type from value 193 + private inferType(value: any): string { 194 + if (value === null) return 'null'; 195 + if (value === undefined) return 'undefined'; 196 + 197 + const type = typeof value; 198 + 199 + switch (type) { 200 + case 'string': 201 + return 'string'; 202 + case 'number': 203 + return 'number'; 204 + case 'boolean': 205 + return 'boolean'; 206 + case 'object': 207 + if (Array.isArray(value)) { 208 + if (value.length === 0) return 'any[]'; 209 + const elementType = this.inferType(value[0]); 210 + return `${elementType}[]`; 211 + } 212 + return 'Record<string, any>'; 213 + default: 214 + return 'any'; 215 + } 216 + } 217 + 218 + // Convert $type to interface name 219 + private $typeToInterfaceName($type: string): string { 220 + // Convert app.bsky.feed.post to AppBskyFeedPost 221 + return $type 222 + .split('.') 223 + .map(part => part.charAt(0).toUpperCase() + part.slice(1)) 224 + .join(''); 225 + } 226 + 227 + // Get collection description 228 + private getCollectionDescription(collectionName: string): string { 229 + const descriptions: Record<string, string> = { 230 + 'app.bsky.feed.post': 'Bluesky posts', 231 + 'app.bsky.actor.profile': 'Bluesky profile information', 232 + 'app.bsky.feed.generator': 'Bluesky custom feeds', 233 + 'app.bsky.graph.follow': 'Bluesky follow relationships', 234 + 'app.bsky.graph.block': 'Bluesky block relationships', 235 + 'app.bsky.feed.like': 'Bluesky like records', 236 + 'app.bsky.feed.repost': 'Bluesky repost records', 237 + 'social.grain.gallery': 'Grain.social image galleries', 238 + 'grain.social.feed.gallery': 'Grain.social galleries', 239 + 'grain.social.feed.post': 'Grain.social posts', 240 + 'grain.social.actor.profile': 'Grain.social profile information', 241 + }; 242 + 243 + return descriptions[collectionName] || `${collectionName} records`; 244 + } 245 + 246 + // Infer service from collection name 247 + private inferService(collectionName: string): string { 248 + if (collectionName.startsWith('grain.social') || collectionName.startsWith('social.grain')) { 249 + return 'grain.social'; 250 + } 251 + if (collectionName.startsWith('app.bsky')) { 252 + return 'bsky.app'; 253 + } 254 + if (collectionName.startsWith('sh.tangled')) { 255 + return 'sh.tangled'; 256 + } 257 + return 'unknown'; 258 + } 259 + 260 + // Save discovery results to file 261 + async saveDiscoveryResults(discovery: BuildTimeDiscovery, outputPath: string): Promise<void> { 262 + try { 263 + // Create output directory if it doesn't exist 264 + const outputDir = path.dirname(outputPath); 265 + await fs.mkdir(outputDir, { recursive: true }); 266 + 267 + // Save discovery metadata 268 + const metadataPath = outputPath.replace('.ts', '.json'); 269 + await fs.writeFile(metadataPath, JSON.stringify(discovery, null, 2)); 270 + 271 + // Generate TypeScript file with all types 272 + let typesContent = '// Auto-generated types from collection discovery\n'; 273 + typesContent += `// Generated at: ${discovery.generatedAt}\n`; 274 + typesContent += `// Repository: ${discovery.repository.handle} (${discovery.repository.did})\n`; 275 + typesContent += `// Collections: ${discovery.totalCollections}, Records: ${discovery.totalRecords}\n\n`; 276 + 277 + // Add all generated types 278 + for (const collection of discovery.collections) { 279 + typesContent += `// Collection: ${collection.name}\n`; 280 + typesContent += `// Service: ${collection.service}\n`; 281 + typesContent += `// Types: ${collection.$types.join(', ')}\n`; 282 + typesContent += collection.generatedTypes; 283 + typesContent += '\n\n'; 284 + } 285 + 286 + // Add union type for all discovered types 287 + const allTypes = discovery.collections.flatMap(c => c.$types); 288 + if (allTypes.length > 0) { 289 + typesContent += '// Union type for all discovered types\n'; 290 + typesContent += `export type DiscoveredTypes = ${allTypes.map(t => `'${t}'`).join(' | ')};\n\n`; 291 + } 292 + 293 + await fs.writeFile(outputPath, typesContent); 294 + 295 + console.log(`💾 Saved discovery results to: ${outputPath}`); 296 + console.log(`📊 Generated ${discovery.totalCollections} collection types`); 297 + 298 + } catch (error) { 299 + console.error('Error saving discovery results:', error); 300 + throw error; 301 + } 302 + } 303 + }
+117
src/lib/components/discovered-registry.ts
··· 1 + import type { DiscoveredTypes } from '../generated/discovered-types'; 2 + 3 + export interface DiscoveredComponent { 4 + $type: DiscoveredTypes; 5 + component: string; 6 + props: Record<string, any>; 7 + } 8 + 9 + export interface ComponentRegistry { 10 + [key: string]: { 11 + component: string; 12 + props?: Record<string, any>; 13 + }; 14 + } 15 + 16 + export class DiscoveredComponentRegistry { 17 + private registry: ComponentRegistry = {}; 18 + private discoveredTypes: DiscoveredTypes[] = []; 19 + 20 + constructor() { 21 + this.initializeRegistry(); 22 + } 23 + 24 + // Initialize the registry with discovered types 25 + private initializeRegistry(): void { 26 + // This will be populated with discovered types 27 + // For now, we'll use a basic mapping 28 + this.registry = { 29 + 'app.bsky.feed.post': { 30 + component: 'BlueskyPost', 31 + props: { showAuthor: false, showTimestamp: true } 32 + }, 33 + 'app.bsky.actor.profile': { 34 + component: 'ProfileDisplay', 35 + props: { showHandle: true } 36 + }, 37 + 'social.grain.gallery': { 38 + component: 'GrainGalleryDisplay', 39 + props: { showCollections: true, columns: 3 } 40 + }, 41 + 'grain.social.feed.gallery': { 42 + component: 'GrainGalleryDisplay', 43 + props: { showCollections: true, columns: 3 } 44 + } 45 + }; 46 + } 47 + 48 + // Register a component for a specific $type 49 + registerComponent($type: DiscoveredTypes, component: string, props?: Record<string, any>): void { 50 + this.registry[$type] = { 51 + component, 52 + props 53 + }; 54 + } 55 + 56 + // Get component info for a $type 57 + getComponent($type: DiscoveredTypes): { component: string; props?: Record<string, any> } | null { 58 + return this.registry[$type] || null; 59 + } 60 + 61 + // Get all registered $types 62 + getRegisteredTypes(): DiscoveredTypes[] { 63 + return Object.keys(this.registry) as DiscoveredTypes[]; 64 + } 65 + 66 + // Check if a $type has a registered component 67 + hasComponent($type: DiscoveredTypes): boolean { 68 + return $type in this.registry; 69 + } 70 + 71 + // Get component mapping for rendering 72 + getComponentMapping(): ComponentRegistry { 73 + return this.registry; 74 + } 75 + 76 + // Update discovered types (called after build-time discovery) 77 + updateDiscoveredTypes(types: DiscoveredTypes[]): void { 78 + this.discoveredTypes = types; 79 + 80 + // Auto-register components for discovered types that don't have explicit mappings 81 + for (const $type of types) { 82 + if (!this.hasComponent($type)) { 83 + // Auto-assign based on service/collection 84 + const component = this.autoAssignComponent($type); 85 + if (component) { 86 + this.registerComponent($type, component); 87 + } 88 + } 89 + } 90 + } 91 + 92 + // Auto-assign component based on $type 93 + private autoAssignComponent($type: DiscoveredTypes): string | null { 94 + if ($type.includes('grain') || $type.includes('gallery')) { 95 + return 'GrainGalleryDisplay'; 96 + } 97 + if ($type.includes('post') || $type.includes('feed')) { 98 + return 'BlueskyPost'; 99 + } 100 + if ($type.includes('profile') || $type.includes('actor')) { 101 + return 'ProfileDisplay'; 102 + } 103 + return 'GenericContentDisplay'; 104 + } 105 + 106 + // Get component info for rendering 107 + getComponentInfo($type: DiscoveredTypes): DiscoveredComponent | null { 108 + const componentInfo = this.getComponent($type); 109 + if (!componentInfo) return null; 110 + 111 + return { 112 + $type, 113 + component: componentInfo.component, 114 + props: componentInfo.props || {} 115 + }; 116 + } 117 + }
+8 -1
src/lib/config/collections.ts
··· 62 62 63 63 // Grain.social collections (high priority for your use case) 64 64 { 65 - name: 'grain.social.feed.gallery', 65 + name: 'social.grain.gallery', 66 66 description: 'Grain.social image galleries', 67 67 service: 'grain.social', 68 68 priority: 95, 69 + enabled: true 70 + }, 71 + { 72 + name: 'grain.social.feed.gallery', 73 + description: 'Grain.social image galleries (legacy)', 74 + service: 'grain.social', 75 + priority: 85, 69 76 enabled: true 70 77 }, 71 78 {
+232
src/lib/services/content-renderer.ts
··· 1 + import { AtprotoBrowser } from '../atproto/atproto-browser'; 2 + import { loadConfig } from '../config/site'; 3 + import type { AtprotoRecord } from '../types/atproto'; 4 + 5 + export interface ContentRendererOptions { 6 + showAuthor?: boolean; 7 + showTimestamp?: boolean; 8 + showType?: boolean; 9 + limit?: number; 10 + filter?: (record: AtprotoRecord) => boolean; 11 + } 12 + 13 + export interface RenderedContent { 14 + type: string; 15 + component: string; 16 + props: Record<string, any>; 17 + metadata: { 18 + uri: string; 19 + cid: string; 20 + collection: string; 21 + $type: string; 22 + createdAt: string; 23 + }; 24 + } 25 + 26 + export class ContentRenderer { 27 + private browser: AtprotoBrowser; 28 + private config: any; 29 + 30 + constructor() { 31 + this.config = loadConfig(); 32 + this.browser = new AtprotoBrowser(); 33 + } 34 + 35 + // Determine the appropriate component for a record type 36 + private getComponentForType($type: string): string { 37 + // Map ATProto types to component names 38 + const componentMap: Record<string, string> = { 39 + 'app.bsky.feed.post': 'BlueskyPost', 40 + 'app.bsky.actor.profile#whitewindBlogPost': 'WhitewindBlogPost', 41 + 'app.bsky.actor.profile#leafletPublication': 'LeafletPublication', 42 + 'app.bsky.actor.profile#grainImageGallery': 'GrainImageGallery', 43 + 'gallery.display': 'GalleryDisplay', 44 + }; 45 + 46 + // Check for gallery-related types 47 + if ($type.includes('gallery') || $type.includes('grain')) { 48 + return 'GalleryDisplay'; 49 + } 50 + 51 + return componentMap[$type] || 'BlueskyPost'; 52 + } 53 + 54 + // Process a record into a renderable format 55 + private processRecord(record: AtprotoRecord): RenderedContent | null { 56 + const value = record.value; 57 + if (!value || !value.$type) return null; 58 + 59 + const component = this.getComponentForType(value.$type); 60 + 61 + // Extract common metadata 62 + const metadata = { 63 + uri: record.uri, 64 + cid: record.cid, 65 + collection: record.collection, 66 + $type: value.$type, 67 + createdAt: value.createdAt || record.indexedAt, 68 + }; 69 + 70 + // For gallery display, use the gallery service format 71 + if (component === 'GalleryDisplay') { 72 + // This would need to be processed by the gallery service 73 + // For now, return a basic format 74 + return { 75 + type: 'gallery', 76 + component: 'GalleryDisplay', 77 + props: { 78 + gallery: { 79 + uri: record.uri, 80 + cid: record.cid, 81 + title: value.title || 'Untitled Gallery', 82 + description: value.description, 83 + text: value.text, 84 + createdAt: value.createdAt || record.indexedAt, 85 + images: this.extractImages(value), 86 + $type: value.$type, 87 + collection: record.collection, 88 + }, 89 + showDescription: true, 90 + showTimestamp: true, 91 + showType: false, 92 + columns: 3, 93 + }, 94 + metadata, 95 + }; 96 + } 97 + 98 + // For other content types, return the record directly 99 + return { 100 + type: 'content', 101 + component, 102 + props: { 103 + post: value, 104 + showAuthor: false, 105 + showTimestamp: true, 106 + }, 107 + metadata, 108 + }; 109 + } 110 + 111 + // Extract images from various embed formats 112 + private extractImages(value: any): Array<{ alt?: string; url: string }> { 113 + const images: Array<{ alt?: string; url: string }> = []; 114 + 115 + // Extract from embed.images 116 + if (value.embed?.$type === 'app.bsky.embed.images' && value.embed.images) { 117 + for (const image of value.embed.images) { 118 + if (image.image?.ref) { 119 + const did = this.config.atproto.did; 120 + const url = `https://bsky.social/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${image.image.ref}`; 121 + images.push({ 122 + alt: image.alt, 123 + url, 124 + }); 125 + } 126 + } 127 + } 128 + 129 + // Extract from direct images array 130 + if (value.images && Array.isArray(value.images)) { 131 + for (const image of value.images) { 132 + if (image.url) { 133 + images.push({ 134 + alt: image.alt, 135 + url: image.url, 136 + }); 137 + } 138 + } 139 + } 140 + 141 + return images; 142 + } 143 + 144 + // Fetch and render content for a given identifier 145 + async renderContent( 146 + identifier: string, 147 + options: ContentRendererOptions = {} 148 + ): Promise<RenderedContent[]> { 149 + try { 150 + const { limit = 50, filter } = options; 151 + 152 + // Get repository info 153 + const repoInfo = await this.browser.getRepoInfo(identifier); 154 + if (!repoInfo) { 155 + throw new Error(`Could not get repository info for: ${identifier}`); 156 + } 157 + 158 + const renderedContent: RenderedContent[] = []; 159 + 160 + // Get records from main collections 161 + const collections = ['app.bsky.feed.post', 'app.bsky.actor.profile', 'social.grain.gallery']; 162 + 163 + for (const collection of collections) { 164 + if (repoInfo.collections.includes(collection)) { 165 + const records = await this.browser.getCollectionRecords(identifier, collection, limit); 166 + if (records && records.records) { 167 + for (const record of records.records) { 168 + // Apply filter if provided 169 + if (filter && !filter(record)) continue; 170 + 171 + const rendered = this.processRecord(record); 172 + if (rendered) { 173 + renderedContent.push(rendered); 174 + } 175 + } 176 + } 177 + } 178 + } 179 + 180 + // Sort by creation date (newest first) 181 + renderedContent.sort((a, b) => { 182 + const dateA = new Date(a.metadata.createdAt); 183 + const dateB = new Date(b.metadata.createdAt); 184 + return dateB.getTime() - dateA.getTime(); 185 + }); 186 + 187 + return renderedContent; 188 + } catch (error) { 189 + console.error('Error rendering content:', error); 190 + return []; 191 + } 192 + } 193 + 194 + // Render a specific record by URI 195 + async renderRecord(uri: string): Promise<RenderedContent | null> { 196 + try { 197 + const record = await this.browser.getRecord(uri); 198 + if (!record) return null; 199 + 200 + return this.processRecord(record); 201 + } catch (error) { 202 + console.error('Error rendering record:', error); 203 + return null; 204 + } 205 + } 206 + 207 + // Get available content types for an identifier 208 + async getContentTypes(identifier: string): Promise<string[]> { 209 + try { 210 + const repoInfo = await this.browser.getRepoInfo(identifier); 211 + if (!repoInfo) return []; 212 + 213 + const types = new Set<string>(); 214 + 215 + for (const collection of repoInfo.collections) { 216 + const records = await this.browser.getCollectionRecords(identifier, collection, 10); 217 + if (records && records.records) { 218 + for (const record of records.records) { 219 + if (record.value?.$type) { 220 + types.add(record.value.$type); 221 + } 222 + } 223 + } 224 + } 225 + 226 + return Array.from(types); 227 + } catch (error) { 228 + console.error('Error getting content types:', error); 229 + return []; 230 + } 231 + } 232 + }
+271
src/lib/services/content-system.ts
··· 1 + import { AtprotoBrowser } from '../atproto/atproto-browser'; 2 + import { JetstreamClient } from '../atproto/jetstream-client'; 3 + import { GrainGalleryService } from './grain-gallery-service'; 4 + import { loadConfig } from '../config/site'; 5 + import type { AtprotoRecord } from '../types/atproto'; 6 + 7 + export interface ContentItem { 8 + uri: string; 9 + cid: string; 10 + $type: string; 11 + collection: string; 12 + createdAt: string; 13 + indexedAt: string; 14 + value: any; 15 + service: string; 16 + operation?: 'create' | 'update' | 'delete'; 17 + } 18 + 19 + export interface ContentFeed { 20 + items: ContentItem[]; 21 + lastUpdated: string; 22 + totalItems: number; 23 + collections: string[]; 24 + } 25 + 26 + export interface ContentSystemConfig { 27 + enableStreaming?: boolean; 28 + buildTimeOnly?: boolean; 29 + collections?: string[]; 30 + maxItems?: number; 31 + } 32 + 33 + export class ContentSystem { 34 + private browser: AtprotoBrowser; 35 + private jetstream: JetstreamClient; 36 + private grainGalleryService: GrainGalleryService; 37 + private config: any; 38 + private contentFeed: ContentFeed; 39 + private isStreaming = false; 40 + 41 + constructor() { 42 + this.config = loadConfig(); 43 + this.browser = new AtprotoBrowser(); 44 + this.jetstream = new JetstreamClient(); 45 + this.grainGalleryService = new GrainGalleryService(); 46 + 47 + this.contentFeed = { 48 + items: [], 49 + lastUpdated: new Date().toISOString(), 50 + totalItems: 0, 51 + collections: [] 52 + }; 53 + } 54 + 55 + // Initialize content system (build-time) 56 + async initialize(identifier: string, options: ContentSystemConfig = {}): Promise<ContentFeed> { 57 + console.log('🚀 Initializing content system for:', identifier); 58 + 59 + try { 60 + // Get repository info 61 + const repoInfo = await this.browser.getRepoInfo(identifier); 62 + if (!repoInfo) { 63 + throw new Error(`Could not get repository info for: ${identifier}`); 64 + } 65 + 66 + console.log('📊 Repository info:', { 67 + handle: repoInfo.handle, 68 + did: repoInfo.did, 69 + collections: repoInfo.collections.length, 70 + recordCount: repoInfo.recordCount 71 + }); 72 + 73 + // Gather all content from collections 74 + const allItems: ContentItem[] = []; 75 + const collections = options.collections || repoInfo.collections; 76 + 77 + for (const collection of collections) { 78 + console.log(`📦 Fetching from collection: ${collection}`); 79 + const records = await this.browser.getCollectionRecords(identifier, collection, options.maxItems || 100); 80 + 81 + if (records && records.records) { 82 + for (const record of records.records) { 83 + const contentItem: ContentItem = { 84 + uri: record.uri, 85 + cid: record.cid, 86 + $type: record.$type, 87 + collection: record.collection, 88 + createdAt: record.value?.createdAt || record.indexedAt, 89 + indexedAt: record.indexedAt, 90 + value: record.value, 91 + service: this.inferService(record.$type, record.collection), 92 + operation: 'create' // Build-time items are existing 93 + }; 94 + 95 + allItems.push(contentItem); 96 + } 97 + } 98 + } 99 + 100 + // Sort by creation date (newest first) 101 + allItems.sort((a, b) => { 102 + const dateA = new Date(a.createdAt); 103 + const dateB = new Date(b.createdAt); 104 + return dateB.getTime() - dateA.getTime(); 105 + }); 106 + 107 + this.contentFeed = { 108 + items: allItems, 109 + lastUpdated: new Date().toISOString(), 110 + totalItems: allItems.length, 111 + collections: collections 112 + }; 113 + 114 + console.log(`✅ Content system initialized with ${allItems.length} items`); 115 + 116 + // Start streaming if enabled 117 + if (!options.buildTimeOnly && options.enableStreaming !== false) { 118 + await this.startStreaming(identifier); 119 + } 120 + 121 + return this.contentFeed; 122 + } catch (error) { 123 + console.error('Error initializing content system:', error); 124 + throw error; 125 + } 126 + } 127 + 128 + // Start real-time streaming 129 + async startStreaming(identifier: string): Promise<void> { 130 + if (this.isStreaming) { 131 + console.log('⚠️ Already streaming'); 132 + return; 133 + } 134 + 135 + console.log('🌊 Starting real-time content streaming...'); 136 + this.isStreaming = true; 137 + 138 + // Set up jetstream event handlers 139 + this.jetstream.onRecord((record) => { 140 + this.handleNewContent(record); 141 + }); 142 + 143 + this.jetstream.onError((error) => { 144 + console.error('❌ Jetstream error:', error); 145 + }); 146 + 147 + this.jetstream.onConnect(() => { 148 + console.log('✅ Connected to real-time stream'); 149 + }); 150 + 151 + this.jetstream.onDisconnect(() => { 152 + console.log('🔌 Disconnected from real-time stream'); 153 + this.isStreaming = false; 154 + }); 155 + 156 + // Start streaming 157 + await this.jetstream.startStreaming(); 158 + } 159 + 160 + // Handle new content from streaming 161 + private handleNewContent(jetstreamRecord: any): void { 162 + const contentItem: ContentItem = { 163 + uri: jetstreamRecord.uri, 164 + cid: jetstreamRecord.cid, 165 + $type: jetstreamRecord.$type, 166 + collection: jetstreamRecord.collection, 167 + createdAt: jetstreamRecord.value?.createdAt || jetstreamRecord.indexedAt, 168 + indexedAt: jetstreamRecord.indexedAt, 169 + value: jetstreamRecord.value, 170 + service: jetstreamRecord.service, 171 + operation: jetstreamRecord.operation 172 + }; 173 + 174 + // Add to beginning of feed (newest first) 175 + this.contentFeed.items.unshift(contentItem); 176 + this.contentFeed.totalItems++; 177 + this.contentFeed.lastUpdated = new Date().toISOString(); 178 + 179 + console.log('📝 New content added:', { 180 + $type: contentItem.$type, 181 + collection: contentItem.collection, 182 + operation: contentItem.operation 183 + }); 184 + 185 + // Emit event for UI updates 186 + this.emitContentUpdate(contentItem); 187 + } 188 + 189 + // Get current content feed 190 + getContentFeed(): ContentFeed { 191 + return this.contentFeed; 192 + } 193 + 194 + // Get content by type 195 + getContentByType($type: string): ContentItem[] { 196 + return this.contentFeed.items.filter(item => item.$type === $type); 197 + } 198 + 199 + // Get content by collection 200 + getContentByCollection(collection: string): ContentItem[] { 201 + return this.contentFeed.items.filter(item => item.collection === collection); 202 + } 203 + 204 + // Get galleries (using specialized service) 205 + async getGalleries(identifier: string): Promise<any[]> { 206 + return await this.grainGalleryService.getGalleries(identifier); 207 + } 208 + 209 + // Filter content by function 210 + filterContent(filterFn: (item: ContentItem) => boolean): ContentItem[] { 211 + return this.contentFeed.items.filter(filterFn); 212 + } 213 + 214 + // Search content 215 + searchContent(query: string): ContentItem[] { 216 + const lowerQuery = query.toLowerCase(); 217 + return this.contentFeed.items.filter(item => { 218 + const text = JSON.stringify(item.value).toLowerCase(); 219 + return text.includes(lowerQuery); 220 + }); 221 + } 222 + 223 + // Stop streaming 224 + stopStreaming(): void { 225 + if (this.isStreaming) { 226 + this.jetstream.stopStreaming(); 227 + this.isStreaming = false; 228 + } 229 + } 230 + 231 + // Infer service from record type and collection 232 + private inferService($type: string, collection: string): string { 233 + if (collection.startsWith('grain.social') || $type.includes('grain')) return 'grain.social'; 234 + if (collection.startsWith('app.bsky')) return 'bsky.app'; 235 + if (collection.startsWith('sh.tangled')) return 'sh.tangled'; 236 + return 'unknown'; 237 + } 238 + 239 + // Event system for UI updates 240 + private listeners: { 241 + onContentUpdate?: (item: ContentItem) => void; 242 + onContentAdd?: (item: ContentItem) => void; 243 + onContentRemove?: (item: ContentItem) => void; 244 + } = {}; 245 + 246 + onContentUpdate(callback: (item: ContentItem) => void): void { 247 + this.listeners.onContentUpdate = callback; 248 + } 249 + 250 + onContentAdd(callback: (item: ContentItem) => void): void { 251 + this.listeners.onContentAdd = callback; 252 + } 253 + 254 + onContentRemove(callback: (item: ContentItem) => void): void { 255 + this.listeners.onContentRemove = callback; 256 + } 257 + 258 + private emitContentUpdate(item: ContentItem): void { 259 + this.listeners.onContentUpdate?.(item); 260 + if (item.operation === 'create') { 261 + this.listeners.onContentAdd?.(item); 262 + } else if (item.operation === 'delete') { 263 + this.listeners.onContentRemove?.(item); 264 + } 265 + } 266 + 267 + // Get streaming status 268 + getStreamingStatus(): 'streaming' | 'stopped' { 269 + return this.isStreaming ? 'streaming' : 'stopped'; 270 + } 271 + }
+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>
+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>
+15 -74
src/pages/galleries.astro
··· 1 1 --- 2 2 import Layout from '../layouts/Layout.astro'; 3 - import GrainImageGallery from '../components/content/GrainImageGallery.astro'; 4 - import { ATprotoDiscovery } from '../lib/atproto/discovery'; 3 + import GrainGalleryDisplay from '../components/content/GrainGalleryDisplay.astro'; 4 + import { GrainGalleryService, type ProcessedGrainGallery } from '../lib/services/grain-gallery-service'; 5 5 import { loadConfig } from '../lib/config/site'; 6 - import type { AtprotoRecord } from '../lib/types/atproto'; 7 6 8 7 const config = loadConfig(); 9 - const discovery = new ATprotoDiscovery(config.atproto.pdsUrl); 8 + const grainGalleryService = new GrainGalleryService(); 10 9 11 - // Fetch all records and filter for galleries 12 - let galleries: AtprotoRecord[] = []; 10 + // Fetch galleries using the Grain.social service 11 + let galleries: ProcessedGrainGallery[] = []; 13 12 try { 14 13 if (config.atproto.handle && config.atproto.handle !== 'your-handle-here') { 15 - // Perform comprehensive repository analysis 16 - const analysis = await discovery.analyzeRepository(config.atproto.handle); 17 - 18 - // Filter for grain-related content from all discovered lexicons 19 - galleries = analysis.lexicons 20 - .filter(lexicon => 21 - lexicon.$type.includes('grain') || 22 - lexicon.$type.includes('gallery') || 23 - lexicon.service === 'grain.social' || 24 - lexicon.description.includes('grain') 25 - ) 26 - .map(lexicon => lexicon.sampleRecord); 27 - 28 - // Sort by creation date (newest first) 29 - galleries.sort((a, b) => { 30 - const dateA = new Date(a.value?.createdAt || a.indexedAt || 0); 31 - const dateB = new Date(b.value?.createdAt || b.indexedAt || 0); 32 - return dateB.getTime() - dateA.getTime(); 33 - }); 14 + galleries = await grainGalleryService.getGalleries(config.atproto.handle); 34 15 } 35 16 } catch (error) { 36 17 console.error('Galleries page: Error fetching galleries:', error); ··· 53 34 {config.atproto.handle && config.atproto.handle !== 'your-handle-here' ? ( 54 35 galleries.length > 0 ? ( 55 36 <div class="space-y-8"> 56 - {galleries.map((record) => { 57 - const $type = record.value?.$type; 58 - 59 - // Handle different types of grain records 60 - if ($type?.includes('grain') || $type?.includes('gallery')) { 61 - // For now, display as a generic gallery component 62 - return ( 63 - <article class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6 mb-6"> 64 - <header class="mb-4"> 65 - <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2"> 66 - {record.value?.title || 'Gallery'} 67 - </h2> 68 - 69 - {record.value?.description && ( 70 - <div class="text-gray-600 dark:text-gray-400 mb-3"> 71 - {record.value.description} 72 - </div> 73 - )} 74 - 75 - <div class="text-sm text-gray-500 dark:text-gray-400 mb-4"> 76 - Type: {record.value?.$type || 'Unknown'} 77 - </div> 78 - </header> 79 - 80 - {record.value?.text && ( 81 - <div class="text-gray-900 dark:text-white mb-4"> 82 - {record.value.text} 83 - </div> 84 - )} 85 - 86 - {record.value?.embed && record.value.embed.$type === 'app.bsky.embed.images' && ( 87 - <div class="grid grid-cols-3 gap-4"> 88 - {record.value.embed.images?.map((image: any) => ( 89 - <div class="relative group"> 90 - <img 91 - src={image.image?.ref ? `https://bsky.social/xrpc/com.atproto.sync.getBlob?did=did:plc:6ayddqghxhciedbaofoxkcbs&cid=${image.image.ref}` : ''} 92 - alt={image.alt || 'Gallery image'} 93 - class="w-full h-48 object-cover rounded-lg transition-transform duration-200 group-hover:scale-105" 94 - /> 95 - </div> 96 - ))} 97 - </div> 98 - )} 99 - </article> 100 - ); 101 - } 102 - 103 - return null; 104 - })} 37 + {galleries.map((gallery) => ( 38 + <GrainGalleryDisplay 39 + gallery={gallery} 40 + showDescription={true} 41 + showTimestamp={true} 42 + showCollections={true} 43 + columns={3} 44 + /> 45 + ))} 105 46 </div> 106 47 ) : ( 107 48 <div class="text-center py-12">
+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>
+72
src/pages/index.astro
··· 40 40 Explore More 41 41 </h2> 42 42 <div class="grid md:grid-cols-2 gap-6"> 43 + <a href="/content-feed" 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"> 44 + <h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2"> 45 + Content Feed 46 + </h3> 47 + <p class="text-gray-600 dark:text-gray-400"> 48 + All your ATProto content with real-time updates and streaming. 49 + </p> 50 + </a> 43 51 <a href="/galleries" 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"> 44 52 <h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2"> 45 53 Image Galleries 46 54 </h3> 47 55 <p class="text-gray-600 dark:text-gray-400"> 48 56 View my grain.social image galleries and photo collections. 57 + </p> 58 + </a> 59 + <a href="/galleries-unified" 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"> 60 + <h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2"> 61 + Unified Galleries 62 + </h3> 63 + <p class="text-gray-600 dark:text-gray-400"> 64 + Galleries with real-time updates using the content system. 65 + </p> 66 + </a> 67 + <a href="/gallery-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"> 68 + <h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2"> 69 + Gallery System Test 70 + </h3> 71 + <p class="text-gray-600 dark:text-gray-400"> 72 + Test the gallery service and display components with detailed debugging. 73 + </p> 74 + </a> 75 + <a href="/content-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"> 76 + <h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2"> 77 + Content Rendering Test 78 + </h3> 79 + <p class="text-gray-600 dark:text-gray-400"> 80 + Test the content rendering system with type-safe components and filters. 81 + </p> 82 + </a> 83 + <a href="/gallery-debug" 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"> 84 + <h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2"> 85 + Gallery Structure Debug 86 + </h3> 87 + <p class="text-gray-600 dark:text-gray-400"> 88 + Examine Grain.social collection structure and relationships. 89 + </p> 90 + </a> 91 + <a href="/grain-gallery-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"> 92 + <h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2"> 93 + Grain Gallery Test 94 + </h3> 95 + <p class="text-gray-600 dark:text-gray-400"> 96 + Test the Grain.social gallery grouping and display system. 97 + </p> 98 + </a> 99 + <a href="/api-debug" 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"> 100 + <h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2"> 101 + API Debug 102 + </h3> 103 + <p class="text-gray-600 dark:text-gray-400"> 104 + Debug ATProto API calls and configuration issues. 105 + </p> 106 + </a> 107 + <a href="/simple-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"> 108 + <h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2"> 109 + Simple API Test 110 + </h3> 111 + <p class="text-gray-600 dark:text-gray-400"> 112 + Basic HTTP request test to ATProto APIs. 113 + </p> 114 + </a> 115 + <a href="/discovery-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"> 116 + <h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-2"> 117 + Discovery Test 118 + </h3> 119 + <p class="text-gray-600 dark:text-gray-400"> 120 + Build-time collection discovery and type generation. 49 121 </p> 50 122 </a> 51 123 <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">
+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>