PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.4
Yatra – Travel Booking & Tour Operator Software v3.0.4
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.4, at resources/js/components/ui/media-upload.tsx

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