1'use client'
2
3import React, { useEffect, useState } from 'react'
4import { motion } from 'framer-motion'
5import Image from 'next/image'
6import { formatDistanceToNow } from 'date-fns'
7import { ImageLightbox } from './image-lightbox'
8
9interface Post {
10 uri: string
11 cid: string
12 author: {
13 handle: string
14 displayName: string
15 avatar: string
16 }
17 record: {
18 text: string
19 createdAt: string
20 embed?: {
21 images?: {
22 alt: string
23 image: {
24 ref: { $link: string }
25 mimeType: string
26 }
27 }[]
28 }
29 }
30 embed?: {
31 images?: {
32 fullsize: string
33 thumb: string
34 alt: string
35 }[]
36 }
37}
38
39export function BlueskyPosts() {
40 const [posts, setPosts] = useState<Post[]>([])
41 const [loading, setLoading] = useState(true)
42 const [error, setError] = useState<string | null>(null)
43 const [selectedImage, setSelectedImage] = useState<{
44 src: string
45 alt: string
46 } | null>(null)
47
48 useEffect(() => {
49 const fetchPosts = async () => {
50 try {
51 const response = await fetch('/api/posts', {
52 next: {
53 revalidate: 300, // 5 minutes
54 },
55 })
56
57 if (!response.ok) {
58 const error = await response.json()
59 throw new Error(error.error || 'Failed to fetch posts')
60 }
61
62 const data = await response.json()
63 setPosts(data)
64 } catch (error) {
65 console.error('Error fetching posts:', error)
66 setError(
67 error instanceof Error ? error.message : 'Failed to fetch posts',
68 )
69 } finally {
70 setLoading(false)
71 }
72 }
73
74 fetchPosts()
75 }, [])
76
77 const fadeInUp = {
78 initial: { opacity: 0, y: 20 },
79 animate: { opacity: 1, y: 0 },
80 transition: { duration: 0.6 },
81 }
82
83 if (loading) {
84 return (
85 <div className="flex min-h-screen items-center justify-center">
86 <div className="h-8 w-8 animate-spin rounded-full border-4 border-blue-500 border-t-transparent"></div>
87 </div>
88 )
89 }
90
91 if (error) {
92 return (
93 <div className="flex min-h-screen items-center justify-center">
94 <div className="text-center">
95 <h2 className="mb-4 text-2xl font-bold text-red-500">Error</h2>
96 <p className="text-gray-400">{error}</p>
97 </div>
98 </div>
99 )
100 }
101
102 return (
103 <section className="min-h-screen py-20">
104 {selectedImage && (
105 <ImageLightbox
106 src={selectedImage.src}
107 alt={selectedImage.alt}
108 onClose={() => setSelectedImage(null)}
109 />
110 )}
111 <motion.div
112 className="container mx-auto px-4"
113 initial="initial"
114 animate="animate"
115 variants={{
116 initial: { opacity: 0 },
117 animate: { opacity: 1, transition: { staggerChildren: 0.1 } },
118 }}
119 >
120 <motion.h2
121 className="mb-12 text-center text-4xl font-bold"
122 variants={fadeInUp}
123 >
124 Latest Posts
125 </motion.h2>
126 <div className="mx-auto max-w-2xl space-y-6">
127 {posts.map((post) => (
128 <motion.div
129 key={post.cid}
130 className="rounded-xl border border-gray-700 bg-gray-800/50 p-6 backdrop-blur-sm"
131 variants={fadeInUp}
132 >
133 <div className="mb-4 flex items-center gap-3">
134 <Image
135 src={post.author.avatar}
136 alt={post.author.displayName}
137 width={40}
138 height={40}
139 className="rounded-full"
140 />
141 <div>
142 <h3 className="font-semibold">{post.author.displayName}</h3>
143 <p className="text-sm text-gray-400">@{post.author.handle}</p>
144 </div>
145 <span className="ml-auto text-sm text-gray-400">
146 {formatDistanceToNow(new Date(post.record.createdAt), {
147 addSuffix: true,
148 })}
149 </span>
150 </div>
151 <p className="mb-4 whitespace-pre-wrap text-gray-200">
152 {post.record.text}
153 </p>
154 {post.embed?.images && (
155 <div className="grid gap-2">
156 {post.embed.images.map((image, index) => (
157 <div
158 key={index}
159 className="relative aspect-video cursor-pointer overflow-hidden rounded-lg"
160 onClick={() =>
161 setSelectedImage({
162 src: image.fullsize,
163 alt: image.alt || 'Post image',
164 })
165 }
166 >
167 <Image
168 src={image.fullsize}
169 alt={image.alt || 'Post image'}
170 fill
171 className="object-cover transition-transform duration-200 hover:scale-105"
172 />
173 </div>
174 ))}
175 </div>
176 )}
177 <div className="mt-4 flex items-center gap-4 text-gray-400">
178 <a
179 href={`https://bsky.app/profile/${post.author.handle}/post/${post.uri.split('/').pop()}`}
180 target="_blank"
181 rel="noopener noreferrer"
182 className="text-sm hover:text-blue-400"
183 >
184 View on Bluesky ā
185 </a>
186 </div>
187 </motion.div>
188 ))}
189 </div>
190 </motion.div>
191 </section>
192 )
193}
194