Răsfoiți Sursa

feat: added gallery collections

YusufSyam 2 săptămâni în urmă
părinte
comite
0a5c6226ca
4 a modificat fișierele cu 242 adăugiri și 1 ștergeri
  1. 184 0
      GALLERY_API_DOCUMENTATION.md
  2. 29 0
      src/collections/Gallery.ts
  3. 27 0
      src/payload-types.ts
  4. 2 1
      src/payload.config.ts

+ 184 - 0
GALLERY_API_DOCUMENTATION.md

@@ -0,0 +1,184 @@
+# Gallery API Documentation
+
+Dokumentasi API untuk collection `gallery` yang berisi foto-foto dengan caption.
+
+## Base Endpoint
+
+```
+/api/gallery
+```
+
+## Authentication
+
+Endpoint ini bersifat **public read-only**, tidak memerlukan authentication untuk membaca data.
+
+---
+
+## Endpoints
+
+### GET `/api/gallery`
+Mengambil daftar gallery items dengan pagination dan filtering.
+
+**Query Parameters:**
+- `page` - Nomor halaman (default: 1)
+- `limit` - Jumlah item per halaman (default: 10)
+- `sort` - Sorting field (contoh: `-createdAt` untuk newest first, `createdAt` untuk oldest first)
+- `depth` - Depth untuk populate image relationship (default: 0, gunakan 1 untuk populate image object dengan URL)
+
+**Response Structure:**
+```json
+{
+  "docs": [
+    {
+      "id": 1,
+      "image": 123, // ID media (jika depth=0) atau object Media (jika depth=1)
+      "caption": "Event foto description",
+      "createdAt": "2025-01-15T10:00:00.000Z",
+      "updatedAt": "2025-01-15T10:00:00.000Z"
+    }
+  ],
+  "totalDocs": 50,
+  "limit": 10,
+  "totalPages": 5,
+  "page": 1,
+  "hasPrevPage": false,
+  "hasNextPage": true
+}
+```
+
+**Gallery Object Fields:**
+- `id` - Unique identifier (number)
+- `image` - Media ID (number) atau Media object (jika depth=1)
+- `caption` - Caption/deskripsi foto (string, required)
+- `createdAt` - Creation timestamp (ISO date string)
+- `updatedAt` - Last update timestamp (ISO date string)
+
+### GET `/api/gallery/:id`
+Mengambil single gallery item berdasarkan ID.
+
+**Path Parameters:**
+- `id` - Gallery item ID
+
+**Query Parameters:**
+- `depth` - Depth untuk populate image relationship (recommended: 1 untuk mendapatkan URL gambar)
+
+**Response:** Single gallery object
+
+---
+
+## Cara Menggunakan di Frontend
+
+### 1. Fetch List Gallery Items
+
+```javascript
+// Mengambil 12 gambar terbaru
+const response = await fetch('http://localhost:3001/api/gallery?limit=12&sort=-createdAt&depth=1')
+const data = await response.json()
+
+// data.docs berisi array gallery items
+data.docs.forEach(item => {
+  console.log(item.caption)
+  console.log(item.image.url) // URL gambar jika depth=1
+})
+```
+
+### 2. Fetch Single Gallery Item
+
+```javascript
+const response = await fetch('http://localhost:3001/api/gallery/123?depth=1')
+const item = await response.json()
+
+console.log(item.caption)
+console.log(item.image.url) // URL gambar
+```
+
+### 3. Pagination
+
+```javascript
+// Halaman 2, 20 item per halaman
+const response = await fetch('http://localhost:3001/api/gallery?page=2&limit=20&depth=1')
+const data = await response.json()
+
+// Check apakah ada halaman selanjutnya
+if (data.hasNextPage) {
+  // Load more...
+}
+```
+
+### 4. Display Gallery Grid
+
+**React Component Example:**
+```jsx
+function GalleryGrid() {
+  const [items, setItems] = useState([])
+  const [loading, setLoading] = useState(true)
+
+  useEffect(() => {
+    fetch('http://localhost:3001/api/gallery?limit=20&sort=-createdAt&depth=1')
+      .then(res => res.json())
+      .then(data => {
+        setItems(data.docs)
+        setLoading(false)
+      })
+  }, [])
+
+  if (loading) return <div>Loading...</div>
+
+  return (
+    <div className="gallery-grid">
+      {items.map(item => (
+        <div key={item.id} className="gallery-item">
+          <img 
+            src={item.image.url} 
+            alt={item.image.alt || item.caption}
+            loading="lazy"
+          />
+          <p className="caption">{item.caption}</p>
+        </div>
+      ))}
+    </div>
+  )
+}
+```
+
+---
+
+## Important Notes
+
+1. **Gunakan `depth=1`** untuk mendapatkan object `image` lengkap (dengan URL, width, height, dll) tanpa perlu fetch terpisah
+2. **Default sorting**: Gunakan `sort=-createdAt` untuk menampilkan gambar terbaru terlebih dahulu
+3. **Image URL**: Ketika `depth=1`, `item.image.url` berisi URL lengkap untuk menampilkan gambar
+4. **Alt text**: Gunakan `item.image.alt` untuk accessibility atau fallback ke `item.caption`
+5. **Pagination**: Gunakan `hasNextPage` dan `hasPrevPage` untuk implementasi infinite scroll atau pagination UI
+
+---
+
+## Media Object (ketika depth=1)
+
+Struktur lengkap object `image` ketika menggunakan `depth=1`:
+
+```json
+{
+  "id": 123,
+  "alt": "Image description",
+  "filename": "photo.jpg",
+  "mimeType": "image/jpeg",
+  "filesize": 123456,
+  "width": 1920,
+  "height": 1080,
+  "url": "http://localhost:3001/media/photo.jpg",
+  "createdAt": "2025-01-15T10:00:00.000Z",
+  "updatedAt": "2025-01-15T10:00:00.000Z"
+}
+```
+
+---
+
+## Best Practices
+
+1. **Lazy Loading**: Gunakan `loading="lazy"` pada tag `<img>` untuk performa yang lebih baik
+2. **Responsive Images**: Gunakan `width` dan `height` dari media object untuk implementasi responsive images
+3. **Error Handling**: Selalu handle error saat fetch dan pastikan `image` object tidak null
+4. **Pagination**: Implement pagination atau infinite scroll untuk gallery yang besar
+5. **Image Optimization**: Pertimbangkan menggunakan image CDN atau Next.js Image component untuk optimasi
+

+ 29 - 0
src/collections/Gallery.ts

@@ -0,0 +1,29 @@
+import type { CollectionConfig } from 'payload'
+
+export const Gallery: CollectionConfig = {
+  slug: 'gallery',
+  admin: {
+    useAsTitle: 'caption',
+    defaultColumns: ['caption', 'image', 'createdAt'],
+  },
+  access: {
+    read: () => true,
+  },
+  fields: [
+    {
+      name: 'image',
+      label: 'Photo',
+      type: 'upload',
+      relationTo: 'media',
+      required: true,
+    },
+    {
+      name: 'caption',
+      label: 'Caption',
+      type: 'text',
+      required: true,
+    },
+  ],
+  timestamps: true,
+}
+

+ 27 - 0
src/payload-types.ts

@@ -73,6 +73,7 @@ export interface Config {
     posts: Post;
     posts: Post;
     clients: Client;
     clients: Client;
     careers: Career;
     careers: Career;
+    gallery: Gallery;
     'payload-kv': PayloadKv;
     'payload-kv': PayloadKv;
     'payload-locked-documents': PayloadLockedDocument;
     'payload-locked-documents': PayloadLockedDocument;
     'payload-preferences': PayloadPreference;
     'payload-preferences': PayloadPreference;
@@ -86,6 +87,7 @@ export interface Config {
     posts: PostsSelect<false> | PostsSelect<true>;
     posts: PostsSelect<false> | PostsSelect<true>;
     clients: ClientsSelect<false> | ClientsSelect<true>;
     clients: ClientsSelect<false> | ClientsSelect<true>;
     careers: CareersSelect<false> | CareersSelect<true>;
     careers: CareersSelect<false> | CareersSelect<true>;
+    gallery: GallerySelect<false> | GallerySelect<true>;
     'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
     'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
     'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
     'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
     'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
     'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
@@ -266,6 +268,17 @@ export interface Career {
   updatedAt: string;
   updatedAt: string;
   createdAt: string;
   createdAt: string;
 }
 }
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "gallery".
+ */
+export interface Gallery {
+  id: number;
+  image: number | Media;
+  caption: string;
+  updatedAt: string;
+  createdAt: string;
+}
 /**
 /**
  * This interface was referenced by `Config`'s JSON-Schema
  * This interface was referenced by `Config`'s JSON-Schema
  * via the `definition` "payload-kv".
  * via the `definition` "payload-kv".
@@ -313,6 +326,10 @@ export interface PayloadLockedDocument {
     | ({
     | ({
         relationTo: 'careers';
         relationTo: 'careers';
         value: number | Career;
         value: number | Career;
+      } | null)
+    | ({
+        relationTo: 'gallery';
+        value: number | Gallery;
       } | null);
       } | null);
   globalSlug?: string | null;
   globalSlug?: string | null;
   user: {
   user: {
@@ -465,6 +482,16 @@ export interface CareersSelect<T extends boolean = true> {
   updatedAt?: T;
   updatedAt?: T;
   createdAt?: T;
   createdAt?: T;
 }
 }
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "gallery_select".
+ */
+export interface GallerySelect<T extends boolean = true> {
+  image?: T;
+  caption?: T;
+  updatedAt?: T;
+  createdAt?: T;
+}
 /**
 /**
  * This interface was referenced by `Config`'s JSON-Schema
  * This interface was referenced by `Config`'s JSON-Schema
  * via the `definition` "payload-kv_select".
  * via the `definition` "payload-kv_select".

+ 2 - 1
src/payload.config.ts

@@ -12,6 +12,7 @@ import { Authors } from './collections/Authors'
 import { Posts } from './collections/Posts'
 import { Posts } from './collections/Posts'
 import { Clients } from './collections/Clients'
 import { Clients } from './collections/Clients'
 import { Careers } from './collections/Careers'
 import { Careers } from './collections/Careers'
+import { Gallery } from './collections/Gallery'
 
 
 const filename = fileURLToPath(import.meta.url)
 const filename = fileURLToPath(import.meta.url)
 const dirname = path.dirname(filename)
 const dirname = path.dirname(filename)
@@ -23,7 +24,7 @@ export default buildConfig({
       baseDir: path.resolve(dirname),
       baseDir: path.resolve(dirname),
     },
     },
   },
   },
-  collections: [Users, Media, Authors, Posts, Clients, Careers],
+  collections: [Users, Media, Authors, Posts, Clients, Careers, Gallery],
   editor: lexicalEditor(),
   editor: lexicalEditor(),
   secret: process.env.PAYLOAD_SECRET || '',
   secret: process.env.PAYLOAD_SECRET || '',
   typescript: {
   typescript: {