Skip to content

Commit c456b9e

Browse files
CopilotFrancisca105
andcommitted
feat: add image upload from URL option for speakers and companies
Co-authored-by: Francisca105 <65908870+Francisca105@users.noreply.github.com>
1 parent 53cb8bc commit c456b9e

File tree

4 files changed

+432
-44
lines changed

4 files changed

+432
-44
lines changed

frontend/src/components/companies/CompanyInfoForm.vue

Lines changed: 109 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,44 @@
4848
<Label for="company-image" class="text-sm font-medium"
4949
>Company Logo</Label
5050
>
51-
<Input
52-
id="company-image"
53-
type="file"
54-
accept="image/*"
55-
:disabled="isLoading"
56-
@change="handleImageChange"
57-
/>
58-
<p class="text-xs text-muted-foreground">
59-
Recommended: Square image, minimum 256x256px, max 10MB
60-
</p>
51+
<Tabs v-model="imageInputMode" class="w-full">
52+
<TabsList class="grid w-full grid-cols-2">
53+
<TabsTrigger value="file">Upload File</TabsTrigger>
54+
<TabsTrigger value="url">From URL</TabsTrigger>
55+
</TabsList>
56+
<TabsContent value="file" class="space-y-2">
57+
<Input
58+
id="company-image"
59+
type="file"
60+
accept="image/*"
61+
:disabled="isLoading"
62+
@change="handleImageChange"
63+
/>
64+
<p class="text-xs text-muted-foreground">
65+
Recommended: Square image, minimum 256x256px, max 10MB
66+
</p>
67+
</TabsContent>
68+
<TabsContent value="url" class="space-y-2">
69+
<Input
70+
id="imageUrl"
71+
v-model="imageUrl"
72+
type="url"
73+
placeholder="https://example.com/logo.jpg"
74+
:disabled="isLoading || isLoadingImageUrl"
75+
@blur="handleImageUrlChange"
76+
@keyup.enter="handleImageUrlChange"
77+
/>
78+
<p class="text-xs text-muted-foreground">
79+
Enter the URL of an image to use
80+
</p>
81+
<p
82+
v-if="isLoadingImageUrl"
83+
class="text-xs text-muted-foreground flex items-center gap-1"
84+
>
85+
Loading image...
86+
</p>
87+
</TabsContent>
88+
</Tabs>
6189
<span v-if="errors.image" class="text-sm text-destructive">{{
6290
errors.image
6391
}}</span>
@@ -98,6 +126,7 @@ import type { UpdateCompanyData } from "@/dto/companies";
98126
import Button from "../ui/button/Button.vue";
99127
import Input from "../ui/input/Input.vue";
100128
import Label from "../ui/label/Label.vue";
129+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
101130
102131
interface Props {
103132
isLoading?: boolean;
@@ -120,6 +149,9 @@ const emit = defineEmits<{
120149
const imagePreview = ref<string>("");
121150
const selectedImageFile = ref<File | null>(null);
122151
const errors = ref<Record<string, string>>({});
152+
const imageInputMode = ref<string>("file");
153+
const imageUrl = ref<string>("");
154+
const isLoadingImageUrl = ref<boolean>(false);
123155
124156
const formData = reactive<
125157
Pick<UpdateCompanyData, "name" | "description" | "site">
@@ -173,6 +205,73 @@ const handleImageChange = (event: Event) => {
173205
}
174206
};
175207
208+
// Handle image URL input
209+
const handleImageUrlChange = async () => {
210+
const url = imageUrl.value.trim();
211+
if (!url) {
212+
return;
213+
}
214+
215+
// Validate URL format
216+
try {
217+
new URL(url);
218+
} catch {
219+
errors.value.image = "Please enter a valid URL";
220+
return;
221+
}
222+
223+
isLoadingImageUrl.value = true;
224+
delete errors.value.image;
225+
226+
try {
227+
const response = await fetch(url);
228+
if (!response.ok) {
229+
throw new Error("Failed to fetch image");
230+
}
231+
232+
const contentType = response.headers.get("content-type");
233+
if (!contentType || !contentType.startsWith("image/")) {
234+
throw new Error("URL does not point to a valid image");
235+
}
236+
237+
const blob = await response.blob();
238+
239+
// Check file size (10MB limit)
240+
if (blob.size > 10 << 20) {
241+
errors.value.image = "Image file size must be less than 10MB";
242+
return;
243+
}
244+
245+
// Extract filename from URL or use a default
246+
const urlPath = new URL(url).pathname;
247+
const filename = urlPath.split("/").pop() || "image";
248+
const extension = contentType.split("/")[1] || "png";
249+
const finalFilename = filename.includes(".")
250+
? filename
251+
: `${filename}.${extension}`;
252+
253+
// Create a File object from the blob
254+
const file = new File([blob], finalFilename, { type: contentType });
255+
selectedImageFile.value = file;
256+
257+
// Create preview
258+
const reader = new FileReader();
259+
reader.onload = (e) => {
260+
imagePreview.value = e.target?.result as string;
261+
};
262+
reader.readAsDataURL(blob);
263+
264+
// Emit the selected file to parent component
265+
emit("imageSelected", file);
266+
} catch (error) {
267+
console.error("Error fetching image from URL:", error);
268+
errors.value.image =
269+
"Failed to load image from URL. Please check the URL and try again.";
270+
} finally {
271+
isLoadingImageUrl.value = false;
272+
}
273+
};
274+
176275
const handleSubmit = () => {
177276
if (isValid.value) {
178277
emit("submit", {

frontend/src/components/companies/CreateCompanyForm.vue

Lines changed: 106 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,45 @@
8888
<!-- Step 2: Company Logo -->
8989
<div v-if="currentStep === 2" class="space-y-4">
9090
<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>
102130
<span v-if="errors.image" class="text-sm text-destructive">{{
103131
errors.image
104132
}}</span>
@@ -122,7 +150,7 @@
122150
Back
123151
</Button>
124152
<div class="flex gap-2">
125-
<Button :disabled="isLoading" @click="nextStep">
153+
<Button :disabled="isLoading || isLoadingImageUrl" @click="nextStep">
126154
<span v-if="!imagePreview">Skip</span>
127155
<span v-else>Next</span>
128156
</Button>
@@ -275,6 +303,7 @@ import {
275303
StepperTitle,
276304
StepperTrigger,
277305
} from "@/components/ui/stepper";
306+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
278307
import CompanyAutocomplete from "./CompanyAutocomplete.vue";
279308
import {
280309
createCompany,
@@ -333,6 +362,9 @@ const formData = ref<CreateCompanyData>({
333362
// Image preview and file
334363
const imagePreview = ref<string>("");
335364
const selectedImageFile = ref<File | null>(null);
365+
const imageInputMode = ref<string>("file");
366+
const imageUrl = ref<string>("");
367+
const isLoadingImageUrl = ref<boolean>(false);
336368
337369
// Representatives data
338370
const representatives = ref<CreateCompanyRepData[]>([]);
@@ -416,6 +448,68 @@ const handleImageChange = (event: Event) => {
416448
}
417449
};
418450
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+
419513
// Representative management
420514
const addRepresentative = () => {
421515
representatives.value.push({

0 commit comments

Comments
 (0)