PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.11
Yatra – Travel Booking & Tour Operator Software v3.0.11
3.0.16 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 All 84 releases
yatra / resources / js / components / ui / media-upload.tsx

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

284 lines 9.6 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 { __, sprintf } 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(
69 sprintf(
70 // translators: %d: maximum number of media items allowed.
71 __("Maximum %d media items allowed", "yatra"),
72 Number(maxItems) || 0,
73 ),
74 );
75 return;
76 }
77
78 // Use WordPress media library
79 if (window.wp && window.wp.media) {
80 const mediaUploader = window.wp.media({
81 title: __("Select Media", "yatra"),
82 button: { text: __("Add Media", "yatra") },
83 multiple: true, // Allow multiple selection
84 library: { type: getMediaType(acceptTypes) },
85 });
86
87 mediaUploader.on("select", () => {
88 const selection = mediaUploader.state().get("selection");
89 const newItems: MediaItem[] = [];
90
91 selection.each((attachment: any) => {
92 const mediaType = attachment.get("type");
93
94 const mediaItem: MediaItem = {
95 id: attachment.get("id").toString(),
96 attachment_id: attachment.get("id"),
97 url: attachment.get("url"),
98 type: mediaType as "image" | "video",
99 alt_text: attachment.get("alt") || "",
100 caption: attachment.get("caption") || "",
101 };
102
103 if (mediaType === "image") {
104 const sizes = attachment.get("sizes");
105 mediaItem.thumbnail_url =
106 sizes?.medium?.url ||
107 sizes?.thumbnail?.url ||
108 attachment.get("url");
109 }
110
111 newItems.push(mediaItem);
112 });
113
114 if (newItems.length > 0) {
115 onChange([...items, ...newItems]);
116 }
117 });
118
119 prepareWordPressMediaFrameOpen();
120 mediaUploader.open();
121 } else {
122 // Fallback for non-WordPress environments
123 const input = document.createElement("input");
124 input.type = "file";
125 input.multiple = true;
126 input.accept =
127 acceptTypes === "videos"
128 ? "video/*"
129 : acceptTypes === "images"
130 ? "image/*"
131 : "image/*,video/*";
132
133 input.onchange = (e) => {
134 const files = Array.from((e.target as HTMLInputElement).files || []);
135 const newItems: MediaItem[] = files
136 .slice(0, maxItems - items.length)
137 .map((file, index) => ({
138 id: `temp_${Date.now()}_${index}`,
139 attachment_id: 0, // Temporary files don't have attachment IDs
140 url: URL.createObjectURL(file),
141 type: file.type.startsWith("video/") ? "video" : "image",
142 alt_text: file.name,
143 caption: file.name,
144 }));
145
146 onChange([...items, ...newItems]);
147 };
148
149 input.click();
150 }
151 };
152
153 const handleMediaRemove = (index: number) => {
154 onChange(items.filter((_, i) => i !== index));
155 };
156
157 const isImage = (item: MediaItem) => item.type === "image";
158
159 return (
160 <Card className={className}>
161 <CardContent className="p-6">
162 <div className="space-y-4">
163 <div className="flex items-center justify-between">
164 <div>
165 <h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
166 {getMediaTitle(acceptTypes)}
167 </h3>
168 <p className="text-sm text-gray-600 dark:text-gray-400">
169 {getMediaDescription(acceptTypes)}
170 </p>
171 </div>
172 <span className="text-sm text-gray-500 dark:text-gray-400">
173 {items.length}/{maxItems}
174 </span>
175 </div>
176
177 {items.length > 0 && (
178 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
179 {items.map((item, index) => (
180 <div key={item.id} className="relative group">
181 <div className="aspect-video bg-gray-100 dark:bg-gray-800 rounded-lg flex items-center justify-center overflow-hidden">
182 {isImage(item) ? (
183 <img
184 src={item.thumbnail_url || item.url}
185 alt={
186 item.alt_text ||
187 // translators: %d: media item sequence number (1-based).
188 sprintf(__("Media %d", "yatra"), index + 1)
189 }
190 className="w-full h-full object-cover"
191 onError={(e) => {
192 console.error("Image load error:", e, "Item:", item); // Debug log
193 // Fallback to placeholder or broken image icon
194 (e.target as HTMLImageElement).style.display = "none";
195 const parent = (e.target as HTMLImageElement)
196 .parentElement;
197 if (parent) {
198 parent.innerHTML =
199 '<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">' +
200 __("Image not available", "yatra") +
201 "</div></div></div>";
202 }
203 }}
204 onLoad={() => {
205 // Image loaded successfully
206 }}
207 />
208 ) : (
209 <div className="relative w-full h-full">
210 <video
211 src={item.url}
212 className="w-full h-full object-cover"
213 muted
214 playsInline
215 />
216 <div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-30">
217 <Video className="w-12 h-12 text-white" />
218 </div>
219 </div>
220 )}
221 </div>
222
223 {/* Delete Button */}
224 <div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
225 <Button
226 type="button"
227 size="sm"
228 variant="destructive"
229 onClick={() => handleMediaRemove(index)}
230 className="h-8 w-8 p-0 bg-red-500 hover:bg-red-600 text-white"
231 >
232 <X className="w-4 h-4" />
233 </Button>
234 </div>
235
236 {/* Caption */}
237 {item.caption && (
238 <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-2">
239 <p className="text-xs text-white truncate">
240 {item.caption}
241 </p>
242 </div>
243 )}
244 </div>
245 ))}
246 </div>
247 )}
248
249 {/* Add Media Button */}
250 {items.length < maxItems && (
251 <div className="flex justify-center">
252 <Button
253 type="button"
254 onClick={handleMediaAdd}
255 variant="outline"
256 className="w-full md:w-auto"
257 >
258 <Plus className="w-4 h-4 mr-2" />
259 {acceptTypes === "videos"
260 ? __("Add Video", "yatra")
261 : acceptTypes === "images"
262 ? __("Add Image", "yatra")
263 : __("Add Media", "yatra")}
264 </Button>
265 </div>
266 )}
267
268 {items.length === 0 && (
269 <div className="text-center py-8 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg">
270 <Upload className="w-12 h-12 text-gray-400 mx-auto mb-4" />
271 <p className="text-gray-600 dark:text-gray-400">
272 {__(
273 "No media items yet. Click 'Add Media' to get started.",
274 "yatra",
275 )}
276 </p>
277 </div>
278 )}
279 </div>
280 </CardContent>
281 </Card>
282 );
283 };
284