From 53cb8bc3a7a5aa8f81ba90c89957e222552ffdf5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 17:39:14 +0000 Subject: [PATCH 1/4] Initial plan From c456b9e800975ca4c8e33930b3f655c0db8beb42 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 17:51:00 +0000 Subject: [PATCH 2/4] feat: add image upload from URL option for speakers and companies Co-authored-by: Francisca105 <65908870+Francisca105@users.noreply.github.com> --- .../components/companies/CompanyInfoForm.vue | 119 +++++++++++++++-- .../companies/CreateCompanyForm.vue | 118 +++++++++++++++-- .../components/speakers/CreateSpeakerForm.vue | 120 ++++++++++++++++-- .../components/speakers/SpeakerInfoForm.vue | 119 +++++++++++++++-- 4 files changed, 432 insertions(+), 44 deletions(-) diff --git a/frontend/src/components/companies/CompanyInfoForm.vue b/frontend/src/components/companies/CompanyInfoForm.vue index 97487d6c..8fca7a01 100644 --- a/frontend/src/components/companies/CompanyInfoForm.vue +++ b/frontend/src/components/companies/CompanyInfoForm.vue @@ -48,16 +48,44 @@ - -

- Recommended: Square image, minimum 256x256px, max 10MB -

+ + + Upload File + From URL + + + +

+ Recommended: Square image, minimum 256x256px, max 10MB +

+
+ + +

+ Enter the URL of an image to use +

+

+ Loading image... +

+
+
{{ errors.image }} @@ -98,6 +126,7 @@ import type { UpdateCompanyData } from "@/dto/companies"; import Button from "../ui/button/Button.vue"; import Input from "../ui/input/Input.vue"; import Label from "../ui/label/Label.vue"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; interface Props { isLoading?: boolean; @@ -120,6 +149,9 @@ const emit = defineEmits<{ const imagePreview = ref(""); const selectedImageFile = ref(null); const errors = ref>({}); +const imageInputMode = ref("file"); +const imageUrl = ref(""); +const isLoadingImageUrl = ref(false); const formData = reactive< Pick @@ -173,6 +205,73 @@ const handleImageChange = (event: Event) => { } }; +// Handle image URL input +const handleImageUrlChange = async () => { + const url = imageUrl.value.trim(); + if (!url) { + return; + } + + // Validate URL format + try { + new URL(url); + } catch { + errors.value.image = "Please enter a valid URL"; + return; + } + + isLoadingImageUrl.value = true; + delete errors.value.image; + + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error("Failed to fetch image"); + } + + const contentType = response.headers.get("content-type"); + if (!contentType || !contentType.startsWith("image/")) { + throw new Error("URL does not point to a valid image"); + } + + const blob = await response.blob(); + + // Check file size (10MB limit) + if (blob.size > 10 << 20) { + errors.value.image = "Image file size must be less than 10MB"; + return; + } + + // Extract filename from URL or use a default + const urlPath = new URL(url).pathname; + const filename = urlPath.split("/").pop() || "image"; + const extension = contentType.split("/")[1] || "png"; + const finalFilename = filename.includes(".") + ? filename + : `${filename}.${extension}`; + + // Create a File object from the blob + const file = new File([blob], finalFilename, { type: contentType }); + selectedImageFile.value = file; + + // Create preview + const reader = new FileReader(); + reader.onload = (e) => { + imagePreview.value = e.target?.result as string; + }; + reader.readAsDataURL(blob); + + // Emit the selected file to parent component + emit("imageSelected", file); + } catch (error) { + console.error("Error fetching image from URL:", error); + errors.value.image = + "Failed to load image from URL. Please check the URL and try again."; + } finally { + isLoadingImageUrl.value = false; + } +}; + const handleSubmit = () => { if (isValid.value) { emit("submit", { diff --git a/frontend/src/components/companies/CreateCompanyForm.vue b/frontend/src/components/companies/CreateCompanyForm.vue index 64529b73..e7641c0b 100644 --- a/frontend/src/components/companies/CreateCompanyForm.vue +++ b/frontend/src/components/companies/CreateCompanyForm.vue @@ -88,17 +88,45 @@
- - -

- Recommended: Square image, minimum 256x256px, max 10MB -

+ + + + Upload File + From URL + + + +

+ Recommended: Square image, minimum 256x256px, max 10MB +

+
+ + +

+ Enter the URL of an image to use +

+

+ Loading image... +

+
+
{{ errors.image }} @@ -122,7 +150,7 @@ Back
- @@ -275,6 +303,7 @@ import { StepperTitle, StepperTrigger, } from "@/components/ui/stepper"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import CompanyAutocomplete from "./CompanyAutocomplete.vue"; import { createCompany, @@ -333,6 +362,9 @@ const formData = ref({ // Image preview and file const imagePreview = ref(""); const selectedImageFile = ref(null); +const imageInputMode = ref("file"); +const imageUrl = ref(""); +const isLoadingImageUrl = ref(false); // Representatives data const representatives = ref([]); @@ -416,6 +448,68 @@ const handleImageChange = (event: Event) => { } }; +// Handle image URL input +const handleImageUrlChange = async () => { + const url = imageUrl.value.trim(); + if (!url) { + return; + } + + // Validate URL format + if (!isValidUrl(url)) { + errors.value.image = "Please enter a valid URL"; + return; + } + + isLoadingImageUrl.value = true; + delete errors.value.image; + + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error("Failed to fetch image"); + } + + const contentType = response.headers.get("content-type"); + if (!contentType || !contentType.startsWith("image/")) { + throw new Error("URL does not point to a valid image"); + } + + const blob = await response.blob(); + + // Check file size (10MB limit) + if (blob.size > 10 << 20) { + errors.value.image = "Image file size must be less than 10MB"; + return; + } + + // Extract filename from URL or use a default + const urlPath = new URL(url).pathname; + const filename = urlPath.split("/").pop() || "image"; + const extension = contentType.split("/")[1] || "png"; + const finalFilename = filename.includes(".") + ? filename + : `${filename}.${extension}`; + + // Create a File object from the blob + const file = new File([blob], finalFilename, { type: contentType }); + selectedImageFile.value = file; + + // Create preview + const reader = new FileReader(); + reader.onload = (e) => { + imagePreview.value = e.target?.result as string; + }; + reader.readAsDataURL(blob); + } catch (error) { + console.error("Error fetching image from URL:", error); + errors.value.image = + "Failed to load image from URL. Please check the URL and try again."; + } finally { + isLoadingImageUrl.value = false; + } +}; + // Representative management const addRepresentative = () => { representatives.value.push({ diff --git a/frontend/src/components/speakers/CreateSpeakerForm.vue b/frontend/src/components/speakers/CreateSpeakerForm.vue index 8e483992..9ca0fdf0 100644 --- a/frontend/src/components/speakers/CreateSpeakerForm.vue +++ b/frontend/src/components/speakers/CreateSpeakerForm.vue @@ -101,17 +101,45 @@
- - -

- Recommended: Square image, minimum 256x256px, max 10MB -

+ + + + Upload File + From URL + + + +

+ Recommended: Square image, minimum 256x256px, max 10MB +

+
+ + +

+ Enter the URL of an image to use +

+

+ Loading image... +

+
+
{{ errors.image }} @@ -135,7 +163,7 @@ Back
- @@ -184,6 +212,7 @@ import { StepperTitle, StepperTrigger, } from "@/components/ui/stepper"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import SpeakerAutocomplete from "./SpeakerAutocomplete.vue"; import ContactForm from "../companies/ContactForm.vue"; import { @@ -249,6 +278,9 @@ const formData = ref({ // Image preview and file const imagePreview = ref(""); const selectedImageFile = ref(null); +const imageInputMode = ref("file"); +const imageUrl = ref(""); +const isLoadingImageUrl = ref(false); // Contact data - store the submitted contact data const contactData = ref({ @@ -335,6 +367,70 @@ const handleImageChange = (event: Event) => { } }; +// Handle image URL input +const handleImageUrlChange = async () => { + const url = imageUrl.value.trim(); + if (!url) { + return; + } + + // Validate URL format + try { + new URL(url); + } catch { + errors.value.image = "Please enter a valid URL"; + return; + } + + isLoadingImageUrl.value = true; + delete errors.value.image; + + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error("Failed to fetch image"); + } + + const contentType = response.headers.get("content-type"); + if (!contentType || !contentType.startsWith("image/")) { + throw new Error("URL does not point to a valid image"); + } + + const blob = await response.blob(); + + // Check file size (10MB limit) + if (blob.size > 10 << 20) { + errors.value.image = "Image file size must be less than 10MB"; + return; + } + + // Extract filename from URL or use a default + const urlPath = new URL(url).pathname; + const filename = urlPath.split("/").pop() || "image"; + const extension = contentType.split("/")[1] || "png"; + const finalFilename = filename.includes(".") + ? filename + : `${filename}.${extension}`; + + // Create a File object from the blob + const file = new File([blob], finalFilename, { type: contentType }); + selectedImageFile.value = file; + + // Create preview + const reader = new FileReader(); + reader.onload = (e) => { + imagePreview.value = e.target?.result as string; + }; + reader.readAsDataURL(blob); + } catch (error) { + console.error("Error fetching image from URL:", error); + errors.value.image = + "Failed to load image from URL. Please check the URL and try again."; + } finally { + isLoadingImageUrl.value = false; + } +}; + // Speaker creation const createSpeakerAndFinish = async () => { if (!validateStep1()) return; diff --git a/frontend/src/components/speakers/SpeakerInfoForm.vue b/frontend/src/components/speakers/SpeakerInfoForm.vue index cf820962..db73543f 100644 --- a/frontend/src/components/speakers/SpeakerInfoForm.vue +++ b/frontend/src/components/speakers/SpeakerInfoForm.vue @@ -56,16 +56,44 @@ - -

- Recommended: Square image, minimum 256x256px, max 10MB -

+ + + Upload File + From URL + + + +

+ Recommended: Square image, minimum 256x256px, max 10MB +

+
+ + +

+ Enter the URL of an image to use +

+

+ Loading image... +

+
+
{{ errors.image }} @@ -119,6 +147,7 @@ import type { UpdateSpeakerData } from "@/dto/speakers"; import Button from "../ui/button/Button.vue"; import Input from "../ui/input/Input.vue"; import Label from "../ui/label/Label.vue"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; interface Props { isLoading?: boolean; @@ -149,6 +178,9 @@ const emit = defineEmits<{ const imagePreview = ref(""); const selectedImageFile = ref(null); const errors = ref>({}); +const imageInputMode = ref("file"); +const imageUrl = ref(""); +const isLoadingImageUrl = ref(false); const formData = reactive< Pick @@ -206,6 +238,73 @@ const handleImageChange = (event: Event) => { } }; +// Handle image URL input +const handleImageUrlChange = async () => { + const url = imageUrl.value.trim(); + if (!url) { + return; + } + + // Validate URL format + try { + new URL(url); + } catch { + errors.value.image = "Please enter a valid URL"; + return; + } + + isLoadingImageUrl.value = true; + delete errors.value.image; + + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error("Failed to fetch image"); + } + + const contentType = response.headers.get("content-type"); + if (!contentType || !contentType.startsWith("image/")) { + throw new Error("URL does not point to a valid image"); + } + + const blob = await response.blob(); + + // Check file size (10MB limit) + if (blob.size > 10 << 20) { + errors.value.image = "Image file size must be less than 10MB"; + return; + } + + // Extract filename from URL or use a default + const urlPath = new URL(url).pathname; + const filename = urlPath.split("/").pop() || "image"; + const extension = contentType.split("/")[1] || "png"; + const finalFilename = filename.includes(".") + ? filename + : `${filename}.${extension}`; + + // Create a File object from the blob + const file = new File([blob], finalFilename, { type: contentType }); + selectedImageFile.value = file; + + // Create preview + const reader = new FileReader(); + reader.onload = (e) => { + imagePreview.value = e.target?.result as string; + }; + reader.readAsDataURL(blob); + + // Emit the selected file to parent component + emit("imageSelected", file); + } catch (error) { + console.error("Error fetching image from URL:", error); + errors.value.image = + "Failed to load image from URL. Please check the URL and try again."; + } finally { + isLoadingImageUrl.value = false; + } +}; + const handleSubmit = () => { if (isValid.value) { emit("submit", { From fea7b4355bb0abab849791e4cccc8cda7d02ff98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:39:48 +0000 Subject: [PATCH 3/4] refactor: extract ImageUpload component and useImageUpload composable Co-authored-by: Francisca105 <65908870+Francisca105@users.noreply.github.com> --- frontend/src/components/ImageUpload.vue | 121 ++++++++++++ .../components/companies/CompanyInfoForm.vue | 176 ++---------------- .../companies/CreateCompanyForm.vue | 172 +++-------------- .../components/speakers/CreateSpeakerForm.vue | 173 +++-------------- .../components/speakers/SpeakerInfoForm.vue | 176 ++---------------- frontend/src/composables/useImageUpload.ts | 128 +++++++++++++ 6 files changed, 323 insertions(+), 623 deletions(-) create mode 100644 frontend/src/components/ImageUpload.vue create mode 100644 frontend/src/composables/useImageUpload.ts diff --git a/frontend/src/components/ImageUpload.vue b/frontend/src/components/ImageUpload.vue new file mode 100644 index 00000000..a5619ede --- /dev/null +++ b/frontend/src/components/ImageUpload.vue @@ -0,0 +1,121 @@ + + + diff --git a/frontend/src/components/companies/CompanyInfoForm.vue b/frontend/src/components/companies/CompanyInfoForm.vue index 8fca7a01..960313a3 100644 --- a/frontend/src/components/companies/CompanyInfoForm.vue +++ b/frontend/src/components/companies/CompanyInfoForm.vue @@ -44,64 +44,15 @@
-
- - - - Upload File - From URL - - - -

- Recommended: Square image, minimum 256x256px, max 10MB -

-
- - -

- Enter the URL of an image to use -

-

- Loading image... -

-
-
- {{ - errors.image - }} - - -
- -
- Company logo preview -
-
-
+
@@ -121,12 +72,12 @@