|
| 1 | +# AppImage Component |
| 2 | + |
| 3 | +A wrapper component around Next.js Image that provides Cloudinary integration with automatic optimization and blur placeholders. |
| 4 | + |
| 5 | +## Location |
| 6 | +`src/components/AppImage.tsx` |
| 7 | + |
| 8 | +## Overview |
| 9 | +AppImage is an optimized image component that handles Cloudinary transformations, automatic format selection, quality optimization, and blur placeholders for better user experience. |
| 10 | + |
| 11 | +## Props |
| 12 | + |
| 13 | +```typescript |
| 14 | +interface AppImageProps { |
| 15 | + alt?: string; |
| 16 | + sizes?: string; |
| 17 | + width?: number | `${number}` | undefined; |
| 18 | + height?: number | `${number}` | undefined; |
| 19 | + imageSource?: IServerFile; |
| 20 | +} |
| 21 | +``` |
| 22 | + |
| 23 | +### Props Details |
| 24 | +- `alt`: Alternative text for accessibility |
| 25 | +- `sizes`: Responsive image sizes (Next.js Image prop) |
| 26 | +- `width`: Image width (Next.js Image prop) |
| 27 | +- `height`: Image height (Next.js Image prop) |
| 28 | +- `imageSource`: Server file object containing provider and key information |
| 29 | + |
| 30 | +## IServerFile Interface |
| 31 | +```typescript |
| 32 | +interface IServerFile { |
| 33 | + provider: "cloudinary" | "r2" | string; |
| 34 | + key: string; |
| 35 | +} |
| 36 | +``` |
| 37 | + |
| 38 | +## Usage Examples |
| 39 | + |
| 40 | +### Basic Usage |
| 41 | +```typescript |
| 42 | +import AppImage from '@/components/AppImage'; |
| 43 | + |
| 44 | +function ArticleCover() { |
| 45 | + const imageSource = { |
| 46 | + provider: "cloudinary", |
| 47 | + key: "articles/my-article-cover" |
| 48 | + }; |
| 49 | + |
| 50 | + return ( |
| 51 | + <AppImage |
| 52 | + imageSource={imageSource} |
| 53 | + alt="Article cover image" |
| 54 | + width={800} |
| 55 | + height={400} |
| 56 | + sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" |
| 57 | + /> |
| 58 | + ); |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +### With Different Providers |
| 63 | +```typescript |
| 64 | +// Cloudinary image |
| 65 | +const cloudinaryImage = { |
| 66 | + provider: "cloudinary", |
| 67 | + key: "profile-photos/user-avatar" |
| 68 | +}; |
| 69 | + |
| 70 | +// R2/Other provider image |
| 71 | +const r2Image = { |
| 72 | + provider: "r2", |
| 73 | + key: "https://example.com/image.jpg" |
| 74 | +}; |
| 75 | + |
| 76 | +return ( |
| 77 | + <div> |
| 78 | + <AppImage imageSource={cloudinaryImage} alt="User avatar" /> |
| 79 | + <AppImage imageSource={r2Image} alt="External image" /> |
| 80 | + </div> |
| 81 | +); |
| 82 | +``` |
| 83 | + |
| 84 | +### Responsive Image Grid |
| 85 | +```typescript |
| 86 | +function ImageGallery({ images }: { images: IServerFile[] }) { |
| 87 | + return ( |
| 88 | + <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"> |
| 89 | + {images.map((image, index) => ( |
| 90 | + <AppImage |
| 91 | + key={index} |
| 92 | + imageSource={image} |
| 93 | + alt={`Gallery image ${index + 1}`} |
| 94 | + width={300} |
| 95 | + height={300} |
| 96 | + sizes="(max-width: 768px) 50vw, (max-width: 1200px) 33vw, 25vw" |
| 97 | + /> |
| 98 | + ))} |
| 99 | + </div> |
| 100 | + ); |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +## Features |
| 105 | + |
| 106 | +### Cloudinary Integration |
| 107 | +- **Automatic format**: Selects optimal format (WebP, AVIF, etc.) |
| 108 | +- **Quality optimization**: Auto quality based on content |
| 109 | +- **URL generation**: Constructs optimized Cloudinary URLs |
| 110 | +- **Blur placeholder**: Generates blurred version for loading states |
| 111 | + |
| 112 | +### Performance Optimizations |
| 113 | +- **Lazy loading**: Images load only when needed |
| 114 | +- **Responsive sizing**: Proper sizes attribute for responsive images |
| 115 | +- **Format selection**: Automatic modern format selection |
| 116 | +- **Quality adjustment**: Optimal quality for file size balance |
| 117 | + |
| 118 | +### Fallback Handling |
| 119 | +- **Provider fallback**: Handles non-Cloudinary providers |
| 120 | +- **Default placeholder**: Falls back to local placeholder image |
| 121 | +- **Error handling**: Graceful degradation for missing images |
| 122 | + |
| 123 | +## Cloudinary Transformations |
| 124 | + |
| 125 | +### Applied Automatically |
| 126 | +```typescript |
| 127 | +// Quality optimization |
| 128 | +.quality("auto") |
| 129 | + |
| 130 | +// Format optimization |
| 131 | +.format("auto") |
| 132 | + |
| 133 | +// Blur placeholder |
| 134 | +.effect(blur(100000)) |
| 135 | +``` |
| 136 | + |
| 137 | +### Generated URLs |
| 138 | +```typescript |
| 139 | +// Original image |
| 140 | +"https://res.cloudinary.com/techdiary-dev/image/upload/q_auto,f_auto/v1/path/to/image" |
| 141 | + |
| 142 | +// Blur placeholder |
| 143 | +"https://res.cloudinary.com/techdiary-dev/image/upload/q_auto,f_auto,e_blur:100000/v1/path/to/image" |
| 144 | +``` |
| 145 | + |
| 146 | +## Provider Support |
| 147 | + |
| 148 | +### Cloudinary Provider |
| 149 | +- Full transformation support |
| 150 | +- Automatic optimization |
| 151 | +- Blur placeholder generation |
| 152 | +- Format and quality selection |
| 153 | + |
| 154 | +### Other Providers (R2, Direct URLs) |
| 155 | +- Direct URL passthrough |
| 156 | +- Default placeholder image |
| 157 | +- No transformations applied |
| 158 | +- Basic Next.js Image functionality |
| 159 | + |
| 160 | +## Implementation Details |
| 161 | + |
| 162 | +### Cloudinary Setup |
| 163 | +```typescript |
| 164 | +const cld = new Cloudinary({ |
| 165 | + cloud: { cloudName: "techdiary-dev" }, |
| 166 | +}); |
| 167 | +``` |
| 168 | + |
| 169 | +### URL Construction |
| 170 | +```typescript |
| 171 | +// Main image URL |
| 172 | +const imageUrl = cld |
| 173 | + .image(imageSource.key) |
| 174 | + .quality("auto") |
| 175 | + .format("auto") |
| 176 | + .toURL(); |
| 177 | + |
| 178 | +// Blur placeholder URL |
| 179 | +const blurUrl = cld |
| 180 | + .image(imageSource.key) |
| 181 | + .quality("auto") |
| 182 | + .format("auto") |
| 183 | + .effect(blur(100000)) |
| 184 | + .toURL(); |
| 185 | +``` |
| 186 | + |
| 187 | +## Best Practices |
| 188 | + |
| 189 | +### Sizing and Responsive |
| 190 | +```typescript |
| 191 | +// Provide proper dimensions |
| 192 | +<AppImage |
| 193 | + width={800} |
| 194 | + height={600} |
| 195 | + sizes="(max-width: 768px) 100vw, 50vw" |
| 196 | +/> |
| 197 | + |
| 198 | +// Use aspect ratio classes for responsive design |
| 199 | +<div className="aspect-video"> |
| 200 | + <AppImage imageSource={image} alt="Video thumbnail" /> |
| 201 | +</div> |
| 202 | +``` |
| 203 | + |
| 204 | +### Accessibility |
| 205 | +```typescript |
| 206 | +// Always provide meaningful alt text |
| 207 | +<AppImage |
| 208 | + imageSource={profilePhoto} |
| 209 | + alt={`Profile photo of ${user.name}`} |
| 210 | +/> |
| 211 | + |
| 212 | +// Use empty alt for decorative images |
| 213 | +<AppImage |
| 214 | + imageSource={decorativeImage} |
| 215 | + alt="" |
| 216 | + role="presentation" |
| 217 | +/> |
| 218 | +``` |
| 219 | + |
| 220 | +### Performance |
| 221 | +```typescript |
| 222 | +// Use appropriate sizes for responsive images |
| 223 | +sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw" |
| 224 | + |
| 225 | +// Preload important images |
| 226 | +<link |
| 227 | + rel="preload" |
| 228 | + as="image" |
| 229 | + href={generateImageUrl(imageSource)} |
| 230 | +/> |
| 231 | +``` |
| 232 | + |
| 233 | +## Common Use Cases |
| 234 | + |
| 235 | +### Article Cover Images |
| 236 | +```typescript |
| 237 | +function ArticleCover({ coverImage }: { coverImage: IServerFile }) { |
| 238 | + return ( |
| 239 | + <div className="aspect-video overflow-hidden rounded-lg"> |
| 240 | + <AppImage |
| 241 | + imageSource={coverImage} |
| 242 | + alt="Article cover" |
| 243 | + width={1200} |
| 244 | + height={630} |
| 245 | + sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 70vw" |
| 246 | + /> |
| 247 | + </div> |
| 248 | + ); |
| 249 | +} |
| 250 | +``` |
| 251 | + |
| 252 | +### User Avatars |
| 253 | +```typescript |
| 254 | +function Avatar({ profilePhoto, userName }: { |
| 255 | + profilePhoto: IServerFile; |
| 256 | + userName: string; |
| 257 | +}) { |
| 258 | + return ( |
| 259 | + <div className="w-10 h-10 rounded-full overflow-hidden"> |
| 260 | + <AppImage |
| 261 | + imageSource={profilePhoto} |
| 262 | + alt={`${userName}'s avatar`} |
| 263 | + width={40} |
| 264 | + height={40} |
| 265 | + /> |
| 266 | + </div> |
| 267 | + ); |
| 268 | +} |
| 269 | +``` |
| 270 | + |
| 271 | +### Image Galleries |
| 272 | +```typescript |
| 273 | +function Gallery({ images }: { images: IServerFile[] }) { |
| 274 | + return ( |
| 275 | + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> |
| 276 | + {images.map((image, index) => ( |
| 277 | + <AppImage |
| 278 | + key={index} |
| 279 | + imageSource={image} |
| 280 | + alt={`Gallery image ${index + 1}`} |
| 281 | + width={400} |
| 282 | + height={300} |
| 283 | + sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" |
| 284 | + /> |
| 285 | + ))} |
| 286 | + </div> |
| 287 | + ); |
| 288 | +} |
| 289 | +``` |
| 290 | + |
| 291 | +## Error Handling |
| 292 | + |
| 293 | +The component gracefully handles: |
| 294 | +- Missing imageSource prop |
| 295 | +- Invalid Cloudinary keys |
| 296 | +- Network errors |
| 297 | +- Unsupported image formats |
| 298 | + |
| 299 | +## Environment Configuration |
| 300 | + |
| 301 | +Ensure Cloudinary is properly configured: |
| 302 | +```typescript |
| 303 | +// Environment variables |
| 304 | +NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=techdiary-dev |
| 305 | +CLOUDINARY_API_KEY=your_api_key |
| 306 | +CLOUDINARY_API_SECRET=your_api_secret |
| 307 | +``` |
| 308 | + |
| 309 | +## Related Components |
| 310 | + |
| 311 | +- **ImageDropzoneWithCropper**: For image uploads with editing |
| 312 | +- **Next.js Image**: Base component being wrapped |
| 313 | +- **Cloudinary SDK**: For URL generation and transformations |
0 commit comments