freshyo/apps/admin-ui/app/(drawer)/dashboard-banners/index.tsx
2026-01-24 00:13:15 +05:30

467 lines
No EOL
17 KiB
TypeScript

import React, { useState } from 'react';
import { View, ScrollView, TouchableOpacity, Alert, Image, RefreshControl } from 'react-native';
import { AppContainer, MyText, tw, MyTouchableOpacity } from 'common-ui';
import { trpc } from '../../../src/trpc-client';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { useRouter } from 'expo-router';
interface Banner {
id: number;
name: string;
imageUrl: string;
description: string | null;
productIds: number[] | null;
redirectUrl: string | null;
serialNum: number | null;
isActive: boolean;
createdAt: string;
lastUpdated: string;
}
export default function DashboardBanners() {
const router = useRouter();
const [refreshing, setRefreshing] = useState(false);
// Edit mode state
const [editMode, setEditMode] = useState(false);
const [selectedSlot, setSelectedSlot] = useState<number | null>(null);
const [slotAssignments, setSlotAssignments] = useState<{[slot: number]: number | null}>({});
const [originalAssignments, setOriginalAssignments] = useState<{[slot: number]: number | null}>({});
const [saving, setSaving] = useState(false);
// Real API calls
const { data: bannersData, isLoading, error, refetch } = trpc.admin.banner.getBanners.useQuery();
const deleteBannerMutation = trpc.admin.banner.deleteBanner.useMutation();
const updateBannerMutation = trpc.admin.banner.updateBanner.useMutation();
const emptySlotMutation = trpc.admin.banner.updateBanner.useMutation();
const banners = bannersData?.banners || [];
// Initialize slot assignments when banners load
React.useEffect(() => {
const assignments: {[slot: number]: number | null} = {1: null, 2: null, 3: null, 4: null};
banners.forEach(banner => {
if (banner.serialNum && banner.serialNum >= 1 && banner.serialNum <= 4) {
assignments[banner.serialNum] = banner.id;
}
});
setSlotAssignments(assignments);
setOriginalAssignments({...assignments});
}, [bannersData]);
const onRefresh = async () => {
setRefreshing(true);
await refetch();
setRefreshing(false);
};
// Slot and edit mode handlers
const handleSlotClick = (slotNumber: number) => {
setSelectedSlot(slotNumber);
setEditMode(true);
};
const handleBannerSelect = (bannerId: number) => {
if (!editMode || selectedSlot === null) return;
// Remove banner from any existing slot
const newAssignments = {...slotAssignments};
Object.keys(newAssignments).forEach(slot => {
if (newAssignments[parseInt(slot)] === bannerId) {
newAssignments[parseInt(slot)] = null;
}
});
// Assign banner to selected slot
newAssignments[selectedSlot] = bannerId;
setSlotAssignments(newAssignments);
};
const handleSave = async () => {
setSaving(true);
try {
// Get banners that need to be updated
const bannersToUpdate: {id: number, serialNum: number | null}[] = [];
// Clear serial numbers for banners no longer in slots
banners.forEach(banner => {
const currentSlot = banner.serialNum;
const assignedSlot = Object.keys(slotAssignments).find(slot =>
slotAssignments[parseInt(slot)] === banner.id
);
if (currentSlot !== (assignedSlot ? parseInt(assignedSlot) : null)) {
bannersToUpdate.push({
id: banner.id,
serialNum: assignedSlot ? parseInt(assignedSlot) : null
});
}
});
// Update banners that gained slots
Object.keys(slotAssignments).forEach(slot => {
const slotNum = parseInt(slot);
const bannerId = slotAssignments[slotNum];
if (bannerId) {
const banner = banners.find(b => b.id === bannerId);
if (banner && banner.serialNum !== slotNum) {
bannersToUpdate.push({
id: bannerId,
serialNum: slotNum
});
}
}
});
// Execute updates
await Promise.all(
bannersToUpdate.map(({id, serialNum}) =>
updateBannerMutation.mutateAsync({ id, serialNum })
)
);
setOriginalAssignments({...slotAssignments});
setEditMode(false);
setSelectedSlot(null);
await refetch();
Alert.alert('Success', 'Slot assignments saved successfully');
} catch (error) {
Alert.alert('Error', 'Failed to save slot assignments');
} finally {
setSaving(false);
}
};
const handleCancel = () => {
setSlotAssignments({...originalAssignments});
setEditMode(false);
setSelectedSlot(null);
};
const handleEmptySlot = async () => {
if (!selectedSlot || !slotAssignments[selectedSlot]) return;
const bannerId = slotAssignments[selectedSlot];
const banner = banners.find(b => b.id === bannerId);
if (!banner) return;
try {
// Update banner's serialNum to null to empty the slot
await emptySlotMutation.mutateAsync({
id: banner.id,
serialNum: null
});
// Update local state
setSlotAssignments(prev => ({
...prev,
[selectedSlot]: null
}));
// Update original assignments for cancel functionality
setOriginalAssignments(prev => ({
...prev,
[selectedSlot]: null
}));
Alert.alert('Success', `Slot ${selectedSlot} has been emptied`);
} catch (error) {
Alert.alert('Error', 'Failed to empty slot');
}
};
// Helper function to get banner name by ID
const getBannerNameById = (id: number | null) => {
if (!id) return null;
const banner = banners.find(b => b.id === id);
return banner ? banner.name : null;
};
const handleEdit = (banner: Banner) => {
router.push(`/dashboard-banners/edit-banner/${banner.id}` as any);
};
const handleDelete = (id: number) => {
Alert.alert(
'Delete Banner',
'Are you sure you want to delete this banner?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Delete',
style: 'destructive',
onPress: async () => {
try {
await deleteBannerMutation.mutateAsync({ id });
refetch();
Alert.alert('Success', 'Banner deleted');
} catch (error) {
Alert.alert('Error', 'Failed to delete banner');
}
}
}
]
);
};
const handleCreate = () => {
router.push('/(drawer)/dashboard-banners/create-banner' as any);
};
if (isLoading) {
return (
<AppContainer>
<View style={tw`flex-1 justify-center items-center bg-white`}>
<MyText style={tw`text-gray-600`}>Loading banners...</MyText>
</View>
</AppContainer>
);
}
if (error) {
return (
<AppContainer>
<View style={tw`flex-1 justify-center items-center bg-white`}>
<MyText style={tw`text-red-600`}>Error loading banners</MyText>
</View>
</AppContainer>
);
}
return (
<AppContainer>
<View style={tw`flex-1 bg-gray-50`}>
<ScrollView
style={tw`flex-1`}
showsVerticalScrollIndicator={false}
contentContainerStyle={tw`pb-24`}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={['#3B82F6']} // Blue color to match the theme
tintColor="#3B82F6"
/>
}
>
{/* Header */}
{/* <View style={tw`px-4 py-6`}>
<MyText style={tw`text-lg font-bold text-gray-900 mb-4`}>All Banners</MyText>
<MyTouchableOpacity
onPress={handleCreate}
style={tw`bg-blue-600 rounded-lg py-3 px-4 items-center mb-6`}
>
<View style={tw`flex-row items-center`}>
<MaterialIcons name="add" size={20} color="white" />
<MyText style={tw`text-white font-semibold ml-2`}>Add New Banner</MyText>
</View>
</MyTouchableOpacity>
</View> */}
{/* Slots Row */}
<View style={tw`px-4 mb-6`}>
<MyText style={tw`text-sm font-medium text-gray-700 mb-3`}>
{editMode ? `Select banner for Slot ${selectedSlot}` : 'Banner Slots'}
</MyText>
<View style={tw`flex-row justify-between`}>
{[1, 2, 3, 4].map(slotNum => {
const assignedBannerId = slotAssignments[slotNum];
const bannerName = getBannerNameById(assignedBannerId);
const isSelected = editMode && selectedSlot === slotNum;
return (
<MyTouchableOpacity
key={slotNum}
onPress={() => handleSlotClick(slotNum)}
style={tw`flex-1 mx-1 p-3 rounded-lg border-2 ${
isSelected
? 'border-blue-500 bg-blue-50'
: assignedBannerId
? 'border-green-300 bg-green-50'
: 'border-gray-300 bg-white'
}`}
>
<View style={tw`items-center`}>
<MyText style={tw`text-xs text-gray-500 mb-1`}>Slot {slotNum}</MyText>
<MyText
style={tw`text-xs font-medium text-center ${
assignedBannerId ? 'text-gray-900' : 'text-gray-400'
}`}
numberOfLines={2}
>
{bannerName || 'Empty'}
</MyText>
</View>
</MyTouchableOpacity>
);
})}
</View>
{/* Action buttons in edit mode */}
{editMode && (
<View style={tw`mt-4 gap-3`}>
{/* Save/Cancel buttons */}
<View style={tw`flex-row gap-3`}>
<MyTouchableOpacity
onPress={handleCancel}
disabled={saving}
style={tw`flex-1 bg-gray-500 rounded-lg py-3 px-4 items-center ${
saving ? 'opacity-50' : ''
}`}
>
<MyText style={tw`text-white font-semibold`}>Cancel</MyText>
</MyTouchableOpacity>
<MyTouchableOpacity
onPress={handleSave}
disabled={saving}
style={tw`flex-1 bg-blue-600 rounded-lg py-3 px-4 items-center ${
saving ? 'opacity-50' : ''
}`}
>
<MyText style={tw`text-white font-semibold`}>
{saving ? 'Saving...' : 'Save Changes'}
</MyText>
</MyTouchableOpacity>
</View>
{/* Empty Slot button */}
<MyTouchableOpacity
onPress={handleEmptySlot}
disabled={!slotAssignments[selectedSlot || 0] || emptySlotMutation.isPending}
style={tw`bg-red-500 rounded-lg py-3 px-4 items-center ${
(!slotAssignments[selectedSlot || 0] || emptySlotMutation.isPending) ? 'opacity-50' : ''
}`}
>
<View style={tw`flex-row items-center`}>
<MaterialIcons
name={emptySlotMutation.isPending ? "hourglass-empty" : "clear"}
size={16}
color="white"
/>
<MyText style={tw`text-white font-semibold ml-2`}>
{emptySlotMutation.isPending ? 'Emptying...' : 'Empty Slot'}
</MyText>
</View>
</MyTouchableOpacity>
</View>
)}
</View>
{/* Banners List */}
<View style={tw`px-4 pb-8`}>
{editMode && (
<View style={tw`mb-4 p-3 bg-blue-50 rounded-lg border border-blue-200`}>
<MyText style={tw`text-sm text-blue-800 text-center`}>
Tap a banner below to assign it to Slot {selectedSlot}
</MyText>
</View>
)}
{banners.length === 0 ? (
<View style={tw`flex-1 justify-center items-center py-8`}>
<View style={tw`w-24 h-24 bg-slate-50 rounded-full items-center justify-center mb-6`}>
<MaterialIcons name="image" size={48} color="#94A3B8" />
</View>
<MyText style={tw`text-slate-900 text-xl font-black tracking-tight`}>No Banners Yet</MyText>
<MyText style={tw`text-slate-500 text-center mt-2 font-medium px-8`}>
Start by creating your first banner using the button above.
</MyText>
</View>
) : (
banners.map((banner) => {
const isAssignedToSlot = Object.values(slotAssignments).includes(banner.id);
const isAssignedToSelectedSlot = slotAssignments[selectedSlot || 0] === banner.id;
const canInteract = editMode;
return (
<MyTouchableOpacity
key={banner.id}
onPress={canInteract ? () => handleBannerSelect(banner.id) : undefined}
disabled={!canInteract}
style={tw`bg-white rounded-xl p-4 mb-3 shadow-sm border ${
canInteract && isAssignedToSelectedSlot
? 'border-blue-500 bg-blue-50'
: canInteract && isAssignedToSlot
? 'border-green-300 bg-green-50'
: canInteract
? 'border-gray-200'
: 'border-gray-100'
}`}
>
<View style={tw`flex-row items-center`}>
<Image
source={{ uri: banner.imageUrl }}
style={tw`w-16 h-16 rounded-lg mr-3`}
resizeMode="cover"
/>
<View style={tw`flex-1`}>
<View style={tw`flex-row items-center justify-between mb-1`}>
<MyText style={tw`font-semibold text-gray-900`} numberOfLines={1}>
{banner.name}
</MyText>
<View style={tw`flex-row items-center`}>
{canInteract && isAssignedToSelectedSlot && (
<View style={tw`mr-2`}>
<MaterialIcons name="check-circle" size={16} color="#3B82F6" />
</View>
)}
{canInteract && isAssignedToSlot && !isAssignedToSelectedSlot && (
<View style={tw`mr-2`}>
<MaterialIcons name="radio-button-checked" size={16} color="#10B981" />
</View>
)}
<View style={tw`w-2 h-2 rounded-full mr-2 ${
banner.isActive ? 'bg-green-500' : 'bg-gray-400'
}`} />
{!editMode && (
<>
<TouchableOpacity onPress={() => handleEdit(banner)} style={tw`p-1 mr-1`}>
<MaterialIcons name="edit" size={16} color="#64748B" />
</TouchableOpacity>
<TouchableOpacity onPress={() => handleDelete(banner.id)} style={tw`p-1`}>
<MaterialIcons name="delete" size={16} color="#EF4444" />
</TouchableOpacity>
</>
)}
</View>
</View>
{banner.description && (
<MyText style={tw`text-sm text-gray-500 mt-1`} numberOfLines={2}>
{banner.description}
</MyText>
)}
<View style={tw`flex-row items-center justify-between mt-2`}>
<MyText style={tw`text-xs text-gray-400`}>
Created: {new Date(banner.createdAt).toLocaleDateString()}
</MyText>
{banner.serialNum && banner.serialNum >= 1 && banner.serialNum <= 4 && (
<MyText style={tw`text-xs text-blue-600 font-medium`}>
Slot {banner.serialNum}
</MyText>
)}
</View>
</View>
</View>
</MyTouchableOpacity>
);
})
)}
</View>
</ScrollView>
{/* Floating Action Button */}
<MyTouchableOpacity
onPress={handleCreate}
style={tw`absolute bottom-6 right-6 w-14 h-14 rounded-full bg-blue-600 items-center justify-center shadow-lg`}
accessibilityLabel="Add new banner"
accessibilityRole="button"
>
<MaterialIcons name="add" size={24} color="white" />
</MyTouchableOpacity>
</View>
</AppContainer>
);
}