PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 All 83 releases
yatra / resources / js / components / ui / media-upload.tsx

media-upload.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.3, at resources/js/components/ui/media-upload.tsx

272 lines 9.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Media Upload Component
3 * Handles image and video uploads for itinerary entries
4 */
5
6 import React from "react";
7 import { Upload, X, Video, Plus } from "lucide-react";
8 import { Button } from "./button";
9 import { Card, CardContent } from "./card";
10 import { __ } from "../../lib/i18n";
11 import { prepareWordPressMediaFrameOpen } from "../../lib/wp-media-open";
12 import { MediaItem } from "../trip-form/types";
13
14 interface MediaUploadProps {
15 items: MediaItem[];
16 onChange: (items: MediaItem[]) => void;
17 maxItems?: number;
18 acceptTypes?: "images" | "videos" | "both";
19 title?: string;
20 description?: string;
21 className?: string;
22 }
23
24 export const MediaUpload: React.FC<MediaUploadProps> = ({
25 items = [],
26 onChange,
27 maxItems = 10,
28 acceptTypes = "both",
29 title = __("Media Gallery", "yatra"),
30 description = __("Add images and videos to showcase this activity", "yatra"),
31 className = "",
32 }) => {
33 const getMediaType = (acceptTypes: string): string => {
34 switch (acceptTypes) {
35 case "images":
36 return "image";
37 case "videos":
38 return "video";
39 default:
40 return "image,video";
41 }
42 };
43
44 const getMediaTitle = (acceptTypes: string): string => {
45 switch (acceptTypes) {
46 case "images":
47 return __("Photo Gallery", "yatra");
48 case "videos":
49 return __("Video Gallery", "yatra");
50 default:
51 return title;
52 }
53 };
54
55 const getMediaDescription = (acceptTypes: string): string => {
56 switch (acceptTypes) {
57 case "images":
58 return __("Upload images to showcase this activity", "yatra");
59 case "videos":
60 return __("Add videos to showcase this activity", "yatra");
61 default:
62 return description;
63 }
64 };
65
66 const handleMediaAdd = () => {
67 if (items.length >= maxItems) {
68 alert(__(`Maximum ${maxItems} media items allowed`, "yatra"));
69 return;
70 }
71
72 // Use WordPress media library
73 if (window.wp && window.wp.media) {
74 const mediaUploader = window.wp.media({
75 title: __("Select Media", "yatra"),
76 button: { text: __("Add Media", "yatra") },
77 multiple: true, // Allow multiple selection
78 library: { type: getMediaType(acceptTypes) },
79 });
80
81 mediaUploader.on("select", () => {
82 const selection = mediaUploader.state().get("selection");
83 const newItems: MediaItem[] = [];
84
85 selection.each((attachment: any) => {
86 const mediaType = attachment.get("type");
87
88 const mediaItem: MediaItem = {
89 id: attachment.get("id").toString(),
90 attachment_id: attachment.get("id"),
91 url: attachment.get("url"),
92 type: mediaType as "image" | "video",
93 alt_text: attachment.get("alt") || "",
94 caption: attachment.get("caption") || "",
95 };
96
97 if (mediaType === "image") {
98 const sizes = attachment.get("sizes");
99 mediaItem.thumbnail_url =
100 sizes?.medium?.url ||
101 sizes?.thumbnail?.url ||
102 attachment.get("url");
103 }
104
105 newItems.push(mediaItem);
106 });
107
108 if (newItems.length > 0) {
109 onChange([...items, ...newItems]);
110 }
111 });
112
113 prepareWordPressMediaFrameOpen();
114 mediaUploader.open();
115 } else {
116 // Fallback for non-WordPress environments
117 const input = document.createElement("input");
118 input.type = "file";
119 input.multiple = true;
120 input.accept =
121 acceptTypes === "videos"
122 ? "video/*"
123 : acceptTypes === "images"
124 ? "image/*"
125 : "image/*,video/*";
126
127 input.onchange = (e) => {
128 const files = Array.from((e.target as HTMLInputElement).files || []);
129 const newItems: MediaItem[] = files
130 .slice(0, maxItems - items.length)
131 .map((file, index) => ({
132 id: `temp_${Date.now()}_${index}`,
133 attachment_id: 0, // Temporary files don't have attachment IDs
134 url: URL.createObjectURL(file),
135 type: file.type.startsWith("video/") ? "video" : "image",
136 alt_text: file.name,
137 caption: file.name,
138 }));
139
140 onChange([...items, ...newItems]);
141 };
142
143 input.click();
144 }
145 };
146
147 const handleMediaRemove = (index: number) => {
148 onChange(items.filter((_, i) => i !== index));
149 };
150
151 const isImage = (item: MediaItem) => item.type === "image";
152
153 return (
154 <Card className={className}>
155 <CardContent className="p-6">
156 <div className="space-y-4">
157 <div className="flex items-center justify-between">
158 <div>
159 <h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
160 {getMediaTitle(acceptTypes)}
161 </h3>
162 <p className="text-sm text-gray-600 dark:text-gray-400">
163 {getMediaDescription(acceptTypes)}
164 </p>
165 </div>
166 <span className="text-sm text-gray-500 dark:text-gray-400">
167 {items.length}/{maxItems}
168 </span>
169 </div>
170
171 {items.length > 0 && (
172 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
173 {items.map((item, index) => (
174 <div key={item.id} className="relative group">
175 <div className="aspect-video bg-gray-100 dark:bg-gray-800 rounded-lg flex items-center justify-center overflow-hidden">
176 {isImage(item) ? (
177 <img
178 src={item.thumbnail_url || item.url}
179 alt={item.alt_text || `Media ${index + 1}`}
180 className="w-full h-full object-cover"
181 onError={(e) => {
182 console.error("Image load error:", e, "Item:", item); // Debug log
183 // Fallback to placeholder or broken image icon
184 (e.target as HTMLImageElement).style.display = "none";
185 const parent = (e.target as HTMLImageElement)
186 .parentElement;
187 if (parent) {
188 parent.innerHTML =
189 '<div class="flex items-center justify-center w-full h-full"><div class="text-gray-400 text-center"><div class="text-4xl mb-2">🖼️</div><div class="text-sm">Image not available</div></div></div>';
190 }
191 }}
192 onLoad={() => {
193 // Image loaded successfully
194 }}
195 />
196 ) : (
197 <div className="relative w-full h-full">
198 <video
199 src={item.url}
200 className="w-full h-full object-cover"
201 muted
202 playsInline
203 />
204 <div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-30">
205 <Video className="w-12 h-12 text-white" />
206 </div>
207 </div>
208 )}
209 </div>
210
211 {/* Delete Button */}
212 <div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
213 <Button
214 type="button"
215 size="sm"
216 variant="destructive"
217 onClick={() => handleMediaRemove(index)}
218 className="h-8 w-8 p-0 bg-red-500 hover:bg-red-600 text-white"
219 >
220 <X className="w-4 h-4" />
221 </Button>
222 </div>
223
224 {/* Caption */}
225 {item.caption && (
226 <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-2">
227 <p className="text-xs text-white truncate">
228 {item.caption}
229 </p>
230 </div>
231 )}
232 </div>
233 ))}
234 </div>
235 )}
236
237 {/* Add Media Button */}
238 {items.length < maxItems && (
239 <div className="flex justify-center">
240 <Button
241 type="button"
242 onClick={handleMediaAdd}
243 variant="outline"
244 className="w-full md:w-auto"
245 >
246 <Plus className="w-4 h-4 mr-2" />
247 {acceptTypes === "videos"
248 ? __("Add Video", "yatra")
249 : acceptTypes === "images"
250 ? __("Add Image", "yatra")
251 : __("Add Media", "yatra")}
252 </Button>
253 </div>
254 )}
255
256 {items.length === 0 && (
257 <div className="text-center py-8 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg">
258 <Upload className="w-12 h-12 text-gray-400 mx-auto mb-4" />
259 <p className="text-gray-600 dark:text-gray-400">
260 {__(
261 "No media items yet. Click 'Add Media' to get started.",
262 "yatra",
263 )}
264 </p>
265 </div>
266 )}
267 </div>
268 </CardContent>
269 </Card>
270 );
271 };
272