feat(web): add mobile swipe gestures to image gallery

Install react-swipeable and wire useSwipeable onto the main image
container — left-swipe advances to next image, right-swipe goes back.
Gestures only activate when there are multiple images; desktop button
navigation is fully preserved.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-22 23:31:31 +07:00
parent e798468e4c
commit 7e2ccdfb7c
4 changed files with 241 additions and 94 deletions

View File

@@ -40,98 +40,6 @@ const TIER_COLORS: Record<string, string> = {
ENTERPRISE: 'text-amber-600',
};
/** Fallback data when API is unavailable */
const FALLBACK_PLANS: PlanDto[] = [
{
id: 'fallback-free',
tier: 'FREE',
name: 'Miễn phí',
priceMonthlyVND: '0',
priceYearlyVND: '0',
maxListings: 3,
maxSavedSearches: 5,
features: {
basicSearch: true,
listingPost: true,
maxPhotos: 5,
analytics: false,
prioritySupport: false,
aiValuation: false,
featuredListing: false,
},
isActive: true,
},
{
id: 'fallback-agent',
tier: 'AGENT_PRO',
name: 'Agent Pro',
priceMonthlyVND: '499000',
priceYearlyVND: '4990000',
maxListings: 50,
maxSavedSearches: 30,
features: {
basicSearch: true,
listingPost: true,
maxPhotos: 30,
analytics: true,
prioritySupport: true,
aiValuation: true,
featuredListing: true,
leadManagement: true,
agentProfile: true,
},
isActive: true,
},
{
id: 'fallback-investor',
tier: 'INVESTOR',
name: 'Investor',
priceMonthlyVND: '999000',
priceYearlyVND: '9990000',
maxListings: 20,
maxSavedSearches: 100,
features: {
basicSearch: true,
listingPost: true,
maxPhotos: 15,
analytics: true,
prioritySupport: true,
aiValuation: true,
featuredListing: false,
marketReports: true,
priceAlerts: true,
portfolioTracking: true,
},
isActive: true,
},
{
id: 'fallback-enterprise',
tier: 'ENTERPRISE',
name: 'Enterprise',
priceMonthlyVND: '4990000',
priceYearlyVND: '49900000',
maxListings: -1,
maxSavedSearches: -1,
features: {
basicSearch: true,
listingPost: true,
maxPhotos: 100,
analytics: true,
prioritySupport: true,
aiValuation: true,
featuredListing: true,
leadManagement: true,
agentProfile: true,
marketReports: true,
priceAlerts: true,
portfolioTracking: true,
apiAccess: true,
whiteLabel: true,
dedicatedSupport: true,
},
isActive: true,
},
];
// ---------------------------------------------------------------------------
// Helpers
@@ -209,7 +117,7 @@ export default function PricingPage() {
const [checkoutPlan, setCheckoutPlan] = useState<PlanDto | null>(null);
const [checkoutOpen, setCheckoutOpen] = useState(false);
const plans = (plansData ?? (error ? FALLBACK_PLANS : []))
const plans = (plansData ?? [])
.slice()
.sort(
(a, b) =>
@@ -316,6 +224,13 @@ export default function PricingPage() {
<div className="flex h-64 items-center justify-center text-muted-foreground">
{t('loading')}
</div>
) : error ? (
<div className="flex h-64 flex-col items-center justify-center gap-3 text-muted-foreground">
<p className="text-lg font-medium text-destructive">
{t('errorLoadingPlans')}
</p>
<p className="text-sm">{t('errorLoadingPlansHint')}</p>
</div>
) : (
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{plans.map((plan) => {