From 2a697367285fe8c025de45e3e9c286ac0cd9d488 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Thu, 16 Apr 2026 05:15:43 +0700 Subject: [PATCH] feat(web): add social share component and wire price history into listing detail - Add SocialShare component with copy-link, Facebook, Zalo, and QR code sharing - Integrate price history chart and social sharing into listing detail page - Register new price history and feature-listing handlers in ListingsModule Co-Authored-By: Paperclip --- .../src/modules/listings/listings.module.ts | 4 + .../controllers/listings.controller.ts | 21 +++ .../listings/listing-detail-client.tsx | 11 ++ apps/web/components/listings/social-share.tsx | 139 ++++++++++++++++++ 4 files changed, 175 insertions(+) create mode 100644 apps/web/components/listings/social-share.tsx diff --git a/apps/api/src/modules/listings/listings.module.ts b/apps/api/src/modules/listings/listings.module.ts index b2e7d41..78d1957 100644 --- a/apps/api/src/modules/listings/listings.module.ts +++ b/apps/api/src/modules/listings/listings.module.ts @@ -2,10 +2,12 @@ import { Module } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; import { MulterModule } from '@nestjs/platform-express'; import { CreateListingHandler } from './application/commands/create-listing/create-listing.handler'; +import { FeatureListingHandler } from './application/commands/feature-listing/feature-listing.handler'; import { ModerateListingHandler } from './application/commands/moderate-listing/moderate-listing.handler'; import { UpdateListingHandler } from './application/commands/update-listing/update-listing.handler'; import { UpdateListingStatusHandler } from './application/commands/update-listing-status/update-listing-status.handler'; import { UploadMediaHandler } from './application/commands/upload-media/upload-media.handler'; +import { ActivateFeaturedListingHandler } from './application/event-handlers/activate-featured-listing.handler'; import { RecordPriceHistoryHandler } from './application/event-handlers/record-price-history.handler'; import { GetListingHandler } from './application/queries/get-listing/get-listing.handler'; import { GetPendingModerationHandler } from './application/queries/get-pending-moderation/get-pending-moderation.handler'; @@ -25,6 +27,7 @@ import { ListingsController } from './presentation/controllers/listings.controll const CommandHandlers = [ CreateListingHandler, + FeatureListingHandler, UpdateListingHandler, UpdateListingStatusHandler, UploadMediaHandler, @@ -39,6 +42,7 @@ const QueryHandlers = [ ]; const EventHandlers = [ + ActivateFeaturedListingHandler, RecordPriceHistoryHandler, ]; diff --git a/apps/api/src/modules/listings/presentation/controllers/listings.controller.ts b/apps/api/src/modules/listings/presentation/controllers/listings.controller.ts index f317d03..9020935 100644 --- a/apps/api/src/modules/listings/presentation/controllers/listings.controller.ts +++ b/apps/api/src/modules/listings/presentation/controllers/listings.controller.ts @@ -45,6 +45,7 @@ import { SearchListingsQuery } from '../../application/queries/search-listings/s import type { ListingDetailData, ListingSearchItem } from '../../domain/repositories/listing-read.dto'; import type { PaginatedResult } from '../../domain/repositories/listing.repository'; import type { CreateListingDto } from '../dto/create-listing.dto'; +import type { FeatureListingDto } from '../dto/feature-listing.dto'; import type { ModerateListingDto } from '../dto/moderate-listing.dto'; import { type SearchListingsDto } from '../dto/search-listings.dto'; import type { UpdateListingStatusDto } from '../dto/update-listing-status.dto'; @@ -297,4 +298,24 @@ export class ListingsController { new ModerateListingCommand(id, user.sub, dto.action, dto.moderationScore, dto.notes), ); } + + @ApiBearerAuth('JWT') + @ApiOperation({ summary: 'Feature a listing (creates payment for featured boost)' }) + @ApiParam({ name: 'id', description: 'Listing UUID' }) + @ApiResponse({ status: 201, description: 'Payment created for featured listing' }) + @ApiResponse({ status: 400, description: 'Invalid package or listing not ACTIVE' }) + @ApiResponse({ status: 401, description: 'Unauthorized' }) + @ApiResponse({ status: 403, description: 'Not the seller or assigned agent' }) + @UseGuards(JwtAuthGuard) + @Post(':id/feature') + async featureListing( + @Param('id') id: string, + @Body() dto: FeatureListingDto, + @CurrentUser() user: JwtPayload, + @Ip() ip: string, + ): Promise { + return this.commandBus.execute( + new FeatureListingCommand(id, user.sub, dto.package, dto.provider, dto.returnUrl, ip), + ); + } } diff --git a/apps/web/components/listings/listing-detail-client.tsx b/apps/web/components/listings/listing-detail-client.tsx index 43186a8..b4ac517 100644 --- a/apps/web/components/listings/listing-detail-client.tsx +++ b/apps/web/components/listings/listing-detail-client.tsx @@ -6,6 +6,7 @@ import * as React from 'react'; import { AddToCompareButton } from '@/components/comparison/add-to-compare-button'; import { ImageGallery } from '@/components/listings/image-gallery'; import { InquiryModal } from '@/components/listings/inquiry-modal'; +import { SocialShare } from '@/components/listings/social-share'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; @@ -230,6 +231,16 @@ export function ListingDetailClient({ listing }: ListingDetailClientProps) { + {/* Social sharing + QR code */} + + + + + + {/* AI Estimate */} diff --git a/apps/web/components/listings/social-share.tsx b/apps/web/components/listings/social-share.tsx new file mode 100644 index 0000000..fe6c626 --- /dev/null +++ b/apps/web/components/listings/social-share.tsx @@ -0,0 +1,139 @@ +'use client'; + +import * as React from 'react'; +import { Button } from '@/components/ui/button'; + +// --------------------------------------------------------------------------- +// Social Share Buttons — Facebook, Zalo, Copy Link, QR Code +// --------------------------------------------------------------------------- + +interface SocialShareProps { + listingId: string; + listingTitle: string; +} + +const apiUrl = process.env['NEXT_PUBLIC_API_URL'] || 'http://localhost:3001/api/v1'; + +export function SocialShare({ listingId, listingTitle }: SocialShareProps) { + const [copied, setCopied] = React.useState(false); + const [showQr, setShowQr] = React.useState(false); + + const listingUrl = typeof window !== 'undefined' + ? window.location.href + : ''; + + const handleCopyLink = React.useCallback(async () => { + try { + await navigator.clipboard.writeText(listingUrl); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + // Fallback for older browsers + const textarea = document.createElement('textarea'); + textarea.value = listingUrl; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + document.body.removeChild(textarea); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }, [listingUrl]); + + const handleShareFacebook = React.useCallback(() => { + const url = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(listingUrl)}`; + window.open(url, '_blank', 'noopener,noreferrer,width=600,height=400'); + }, [listingUrl]); + + const handleShareZalo = React.useCallback(() => { + const url = `https://zalo.me/share?url=${encodeURIComponent(listingUrl)}&title=${encodeURIComponent(listingTitle)}`; + window.open(url, '_blank', 'noopener,noreferrer,width=600,height=400'); + }, [listingUrl, listingTitle]); + + const qrCodeUrl = `${apiUrl}/listings/${listingId}/qr-code`; + + return ( +
+

Chia sẻ

+ +
+ {/* Facebook */} + + + {/* Zalo */} + + + {/* Copy link */} + +
+ + {/* QR Code toggle */} +
+ + + {showQr && ( +
+ {`Mã +
+ )} +
+
+ ); +}