소스 검색

feat: add images in post action dialog

Mohammad Mahdi Salimi 1 개월 전
부모
커밋
484c14dd40
2개의 변경된 파일과 123개의 추가작업 그리고 22개의 파일을 삭제
  1. 107 11
      src/features/posts/components/posts-action-dialog.tsx
  2. 16 11
      src/types/posts.types.ts

+ 107 - 11
src/features/posts/components/posts-action-dialog.tsx

@@ -1,21 +1,20 @@
+import { useState } from 'react'
+import { type MediaFile, type Post } from '@/types/posts.types'
+import { ChevronLeft, ChevronRight } from 'lucide-react'
+import { cn } from '@/lib/utils'
+import { useChangeStatePost } from '@/hooks/posts/useChangeStatePost'
+import { usePost } from '@/hooks/posts/usePost'
+import { Button } from '@/components/ui/button'
 import {
   Dialog,
   DialogContent,
   DialogHeader,
   DialogTitle,
 } from '@/components/ui/dialog'
-
-import { Button } from '@/components/ui/button'
-import { Switch } from '@/components/ui/switch'
 import { Label } from '@/components/ui/label'
-
-import { usePosts } from './posts-provider'
-import { usePost } from '@/hooks/posts/usePost'
-import { useChangeStatePost } from '@/hooks/posts/useChangeStatePost'
-
+import { Switch } from '@/components/ui/switch'
 import VideoPlayer from '@/components/video-player'
-import { useState } from 'react'
-import { type Post } from '@/types/posts.types'
+import { usePosts } from './posts-provider'
 
 export function PostsActionDialog() {
   const { open, setOpen, currentRow } = usePosts()
@@ -87,7 +86,7 @@ function PostContent({
   return (
     <div>
       <div className='pb-2'>
-        <VideoPlayer src={post?.video?.url as string} />
+        <PostMedia post={post} />
       </div>
 
       <div className='py-2 text-sm'>متن پست : {post?.caption}</div>
@@ -120,3 +119,100 @@ function PostContent({
     </div>
   )
 }
+
+function PostMedia({ post }: { post: Post }) {
+  const images = (post.images ?? []).filter((image) => Boolean(image?.url))
+
+  if (post.video?.url && post.type !== 'gallery') {
+    return <VideoPlayer src={post.video.url} />
+  }
+
+  if (images.length > 0) {
+    return <PostGallery images={images} />
+  }
+
+  return (
+    <div className='rounded-2xl bg-muted py-10 text-center text-sm text-muted-foreground'>
+      رسانه‌ای برای نمایش وجود ندارد
+    </div>
+  )
+}
+
+function PostGallery({ images }: { images: MediaFile[] }) {
+  const [index, setIndex] = useState(0)
+  const current = images[index] ?? images[0]
+  const hasMany = images.length > 1
+
+  const go = (delta: number) => {
+    setIndex(
+      (currentIndex) => (currentIndex + delta + images.length) % images.length
+    )
+  }
+
+  if (!current) return null
+
+  return (
+    <div className='space-y-3'>
+      <div className='relative overflow-hidden rounded-2xl bg-muted'>
+        <img
+          src={current.url}
+          alt={current.name}
+          className='mx-auto max-h-[60vh] w-full object-contain'
+        />
+
+        {hasMany && (
+          <>
+            <Button
+              type='button'
+              variant='secondary'
+              size='icon'
+              className='absolute inset-s-3 top-1/2 -translate-y-1/2'
+              onClick={() => go(-1)}
+              aria-label='تصویر قبلی'
+            >
+              <ChevronRight />
+            </Button>
+            <Button
+              type='button'
+              variant='secondary'
+              size='icon'
+              className='absolute inset-e-3 top-1/2 -translate-y-1/2'
+              onClick={() => go(1)}
+              aria-label='تصویر بعدی'
+            >
+              <ChevronLeft />
+            </Button>
+            <span className='absolute bottom-3 left-1/2 -translate-x-1/2 rounded-full bg-black/60 px-2.5 py-0.5 text-xs text-white'>
+              {index + 1} / {images.length}
+            </span>
+          </>
+        )}
+      </div>
+
+      {hasMany && (
+        <div className='flex gap-2 overflow-x-auto pb-1'>
+          {images.map((image, imageIndex) => (
+            <button
+              key={image.hash || image.url}
+              type='button'
+              onClick={() => setIndex(imageIndex)}
+              className={cn(
+                'size-16 shrink-0 overflow-hidden rounded-md border-2',
+                imageIndex === index
+                  ? 'border-primary'
+                  : 'border-transparent opacity-70'
+              )}
+              aria-label={image.name}
+            >
+              <img
+                src={image.url}
+                alt={image.name}
+                className='size-full object-cover'
+              />
+            </button>
+          ))}
+        </div>
+      )}
+    </div>
+  )
+}

+ 16 - 11
src/types/posts.types.ts

@@ -1,13 +1,26 @@
 import type { Pagination } from './common.types'
 
+export interface MediaFile {
+  name: string
+  url: string
+  hash: string
+  type: string
+  entity_slug: string
+}
+
 export interface Post {
   id: number
   user_id: number
+  activity_fields_id: number
   user: User
+  type: string
   state: string
   caption: string
-  video: Avatar
-  thumbnail: null
+  height: number | null
+  width: number | null
+  video: MediaFile | null
+  thumbnail: MediaFile | null
+  images: MediaFile[] | null
   likes_count: number
   dislikes_count: number
   views_count: number
@@ -22,15 +35,7 @@ interface User {
   last_name: string
   full_name: string
   username: string
-  avatar: Avatar | null
-}
-
-interface Avatar {
-  name: string
-  url: string
-  hash: string
-  type: string
-  entity_slug: string
+  avatar: MediaFile | null
 }
 
 export interface GetPostsResponse {