|
88 | 88 | <!-- Step 2: Company Logo --> |
89 | 89 | <div v-if="currentStep === 2" class="space-y-4"> |
90 | 90 | <div class="space-y-2"> |
91 | | - <Label for="image" class="text-sm font-medium">Company Logo</Label> |
92 | | - <Input |
93 | | - id="image" |
94 | | - type="file" |
95 | | - accept="image/*" |
96 | | - :disabled="isLoading" |
97 | | - @change="handleImageChange" |
98 | | - /> |
99 | | - <p class="text-xs text-muted-foreground"> |
100 | | - Recommended: Square image, minimum 256x256px, max 10MB |
101 | | - </p> |
| 91 | + <Label class="text-sm font-medium">Company Logo</Label> |
| 92 | + <Tabs v-model="imageInputMode" class="w-full"> |
| 93 | + <TabsList class="grid w-full grid-cols-2"> |
| 94 | + <TabsTrigger value="file">Upload File</TabsTrigger> |
| 95 | + <TabsTrigger value="url">From URL</TabsTrigger> |
| 96 | + </TabsList> |
| 97 | + <TabsContent value="file" class="space-y-2"> |
| 98 | + <Input |
| 99 | + id="image" |
| 100 | + type="file" |
| 101 | + accept="image/*" |
| 102 | + :disabled="isLoading" |
| 103 | + @change="handleImageChange" |
| 104 | + /> |
| 105 | + <p class="text-xs text-muted-foreground"> |
| 106 | + Recommended: Square image, minimum 256x256px, max 10MB |
| 107 | + </p> |
| 108 | + </TabsContent> |
| 109 | + <TabsContent value="url" class="space-y-2"> |
| 110 | + <Input |
| 111 | + id="imageUrl" |
| 112 | + v-model="imageUrl" |
| 113 | + type="url" |
| 114 | + placeholder="https://example.com/logo.jpg" |
| 115 | + :disabled="isLoading || isLoadingImageUrl" |
| 116 | + @blur="handleImageUrlChange" |
| 117 | + @keyup.enter="handleImageUrlChange" |
| 118 | + /> |
| 119 | + <p class="text-xs text-muted-foreground"> |
| 120 | + Enter the URL of an image to use |
| 121 | + </p> |
| 122 | + <p |
| 123 | + v-if="isLoadingImageUrl" |
| 124 | + class="text-xs text-muted-foreground flex items-center gap-1" |
| 125 | + > |
| 126 | + Loading image... |
| 127 | + </p> |
| 128 | + </TabsContent> |
| 129 | + </Tabs> |
102 | 130 | <span v-if="errors.image" class="text-sm text-destructive">{{ |
103 | 131 | errors.image |
104 | 132 | }}</span> |
|
122 | 150 | Back |
123 | 151 | </Button> |
124 | 152 | <div class="flex gap-2"> |
125 | | - <Button :disabled="isLoading" @click="nextStep"> |
| 153 | + <Button :disabled="isLoading || isLoadingImageUrl" @click="nextStep"> |
126 | 154 | <span v-if="!imagePreview">Skip</span> |
127 | 155 | <span v-else>Next</span> |
128 | 156 | </Button> |
@@ -275,6 +303,7 @@ import { |
275 | 303 | StepperTitle, |
276 | 304 | StepperTrigger, |
277 | 305 | } from "@/components/ui/stepper"; |
| 306 | +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; |
278 | 307 | import CompanyAutocomplete from "./CompanyAutocomplete.vue"; |
279 | 308 | import { |
280 | 309 | createCompany, |
@@ -333,6 +362,9 @@ const formData = ref<CreateCompanyData>({ |
333 | 362 | // Image preview and file |
334 | 363 | const imagePreview = ref<string>(""); |
335 | 364 | const selectedImageFile = ref<File | null>(null); |
| 365 | +const imageInputMode = ref<string>("file"); |
| 366 | +const imageUrl = ref<string>(""); |
| 367 | +const isLoadingImageUrl = ref<boolean>(false); |
336 | 368 |
|
337 | 369 | // Representatives data |
338 | 370 | const representatives = ref<CreateCompanyRepData[]>([]); |
@@ -416,6 +448,68 @@ const handleImageChange = (event: Event) => { |
416 | 448 | } |
417 | 449 | }; |
418 | 450 |
|
| 451 | +// Handle image URL input |
| 452 | +const handleImageUrlChange = async () => { |
| 453 | + const url = imageUrl.value.trim(); |
| 454 | + if (!url) { |
| 455 | + return; |
| 456 | + } |
| 457 | +
|
| 458 | + // Validate URL format |
| 459 | + if (!isValidUrl(url)) { |
| 460 | + errors.value.image = "Please enter a valid URL"; |
| 461 | + return; |
| 462 | + } |
| 463 | +
|
| 464 | + isLoadingImageUrl.value = true; |
| 465 | + delete errors.value.image; |
| 466 | +
|
| 467 | + try { |
| 468 | + const response = await fetch(url); |
| 469 | + if (!response.ok) { |
| 470 | + throw new Error("Failed to fetch image"); |
| 471 | + } |
| 472 | +
|
| 473 | + const contentType = response.headers.get("content-type"); |
| 474 | + if (!contentType || !contentType.startsWith("image/")) { |
| 475 | + throw new Error("URL does not point to a valid image"); |
| 476 | + } |
| 477 | +
|
| 478 | + const blob = await response.blob(); |
| 479 | +
|
| 480 | + // Check file size (10MB limit) |
| 481 | + if (blob.size > 10 << 20) { |
| 482 | + errors.value.image = "Image file size must be less than 10MB"; |
| 483 | + return; |
| 484 | + } |
| 485 | +
|
| 486 | + // Extract filename from URL or use a default |
| 487 | + const urlPath = new URL(url).pathname; |
| 488 | + const filename = urlPath.split("/").pop() || "image"; |
| 489 | + const extension = contentType.split("/")[1] || "png"; |
| 490 | + const finalFilename = filename.includes(".") |
| 491 | + ? filename |
| 492 | + : `${filename}.${extension}`; |
| 493 | +
|
| 494 | + // Create a File object from the blob |
| 495 | + const file = new File([blob], finalFilename, { type: contentType }); |
| 496 | + selectedImageFile.value = file; |
| 497 | +
|
| 498 | + // Create preview |
| 499 | + const reader = new FileReader(); |
| 500 | + reader.onload = (e) => { |
| 501 | + imagePreview.value = e.target?.result as string; |
| 502 | + }; |
| 503 | + reader.readAsDataURL(blob); |
| 504 | + } catch (error) { |
| 505 | + console.error("Error fetching image from URL:", error); |
| 506 | + errors.value.image = |
| 507 | + "Failed to load image from URL. Please check the URL and try again."; |
| 508 | + } finally { |
| 509 | + isLoadingImageUrl.value = false; |
| 510 | + } |
| 511 | +}; |
| 512 | +
|
419 | 513 | // Representative management |
420 | 514 | const addRepresentative = () => { |
421 | 515 | representatives.value.push({ |
|
0 commit comments