Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions frontend/src/components/ImageUpload.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<template>
<div class="space-y-2">
<Label v-if="label" class="text-sm font-medium">{{ label }}</Label>
<Tabs v-model="imageInputMode" class="w-full">
<TabsList class="grid w-full grid-cols-2">
<TabsTrigger value="file">Upload File</TabsTrigger>
<TabsTrigger value="url">From URL</TabsTrigger>
</TabsList>
<TabsContent value="file" class="space-y-2">
<Input
:id="inputId"
type="file"
accept="image/*"
:disabled="disabled"
@change="handleFileChange"
/>
<p class="text-xs text-muted-foreground">
Recommended: Square image, minimum 256x256px, max 10MB
</p>
</TabsContent>
<TabsContent value="url" class="space-y-2">
<Input
:id="`${inputId}-url`"
v-model="imageUrl"
type="url"
:placeholder="urlPlaceholder"
:disabled="disabled || isLoadingImageUrl"
@blur="handleUrlChange"
@keyup.enter="handleUrlChange"
/>
<p class="text-xs text-muted-foreground">
Enter the URL of an image to use
</p>
<p
v-if="isLoadingImageUrl"
class="text-xs text-muted-foreground flex items-center gap-1"
>
Loading image...
</p>
</TabsContent>
</Tabs>
<span v-if="imageError" class="text-sm text-destructive">{{
imageError
}}</span>

<!-- Image preview -->
<div v-if="imagePreview" class="space-y-2">
<Label class="text-sm font-medium">Preview</Label>
<div
:class="[
'border border-muted rounded-lg overflow-hidden',
previewSizeClass,
]"
>
<img
:src="imagePreview"
:alt="previewAlt"
class="w-full h-full object-cover"
/>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { watch } from "vue";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useImageUpload } from "@/composables/useImageUpload";

interface Props {
label?: string;
inputId?: string;
urlPlaceholder?: string;
previewAlt?: string;
previewSize?: "sm" | "md";
disabled?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
label: undefined,
inputId: "image-upload",
urlPlaceholder: "https://example.com/image.jpg",
previewAlt: "Image preview",
previewSize: "md",
disabled: false,
});

const emit = defineEmits<{
fileSelected: [file: File];
}>();

const {
imagePreview,
selectedImageFile,
imageInputMode,
imageUrl,
isLoadingImageUrl,
imageError,
handleFileChange,
handleUrlChange,
} = useImageUpload();

const previewSizeClass = props.previewSize === "sm" ? "w-20 h-20" : "w-24 h-24";

// Emit the selected file when it changes
watch(selectedImageFile, (file) => {
if (file) {
emit("fileSelected", file);
}
});

// Expose state and methods to parent
defineExpose({
imagePreview,
selectedImageFile,
isLoadingImageUrl,
imageError,
});
</script>
77 changes: 15 additions & 62 deletions frontend/src/components/companies/CompanyInfoForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,36 +44,15 @@
</div>

<!-- Image Upload Field -->
<div class="space-y-2">
<Label for="company-image" class="text-sm font-medium"
>Company Logo</Label
>
<Input
id="company-image"
type="file"
accept="image/*"
:disabled="isLoading"
@change="handleImageChange"
/>
<p class="text-xs text-muted-foreground">
Recommended: Square image, minimum 256x256px, max 10MB
</p>
<span v-if="errors.image" class="text-sm text-destructive">{{
errors.image
}}</span>

<!-- Image preview -->
<div v-if="imagePreview" class="space-y-2">
<Label class="text-sm font-medium">Preview</Label>
<div class="w-20 h-20 border border-muted rounded-lg overflow-hidden">
<img
:src="imagePreview"
alt="Company logo preview"
class="w-full h-full object-cover"
/>
</div>
</div>
</div>
<ImageUpload
label="Company Logo"
input-id="company-logo"
url-placeholder="https://example.com/logo.jpg"
preview-alt="Company logo preview"
preview-size="sm"
:disabled="isLoading"
@file-selected="handleImageSelected"
/>
</div>

<!-- Form Actions -->
Expand All @@ -93,11 +72,12 @@
</template>

<script setup lang="ts">
import { computed, reactive, watch, ref } from "vue";
import { computed, reactive, watch } from "vue";
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 ImageUpload from "@/components/ImageUpload.vue";

interface Props {
isLoading?: boolean;
Expand All @@ -116,10 +96,10 @@ const emit = defineEmits<{
imageSelected: [file: File];
}>();

// Image handling
const imagePreview = ref<string>("");
const selectedImageFile = ref<File | null>(null);
const errors = ref<Record<string, string>>({});
// Handle image selection from ImageUpload component
const handleImageSelected = (file: File) => {
emit("imageSelected", file);
};

const formData = reactive<
Pick<UpdateCompanyData, "name" | "description" | "site">
Expand All @@ -146,33 +126,6 @@ const isValid = computed(() => {
return formData.name?.trim();
});

// Image handling
const handleImageChange = (event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0];
if (file) {
// Check file size (10MB limit)
if (file.size > 10 << 20) {
errors.value.image = "Image file size must be less than 10MB";
return;
}

// Clear any previous image errors
delete errors.value.image;

selectedImageFile.value = file;

// Create preview
const reader = new FileReader();
reader.onload = (e) => {
imagePreview.value = e.target?.result as string;
};
reader.readAsDataURL(file);

// Emit the selected file to parent component
emit("imageSelected", file);
}
};

const handleSubmit = () => {
if (isValid.value) {
emit("submit", {
Expand Down
78 changes: 22 additions & 56 deletions frontend/src/components/companies/CreateCompanyForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,43 +87,27 @@

<!-- Step 2: Company Logo -->
<div v-if="currentStep === 2" class="space-y-4">
<div class="space-y-2">
<Label for="image" class="text-sm font-medium">Company Logo</Label>
<Input
id="image"
type="file"
accept="image/*"
:disabled="isLoading"
@change="handleImageChange"
/>
<p class="text-xs text-muted-foreground">
Recommended: Square image, minimum 256x256px, max 10MB
</p>
<span v-if="errors.image" class="text-sm text-destructive">{{
errors.image
}}</span>
</div>

<!-- Image preview -->
<div v-if="imagePreview" class="space-y-2">
<Label class="text-sm font-medium">Preview</Label>
<div class="w-24 h-24 border border-muted rounded-lg overflow-hidden">
<img
:src="imagePreview"
alt="Logo preview"
class="w-full h-full object-cover"
/>
</div>
</div>
<ImageUpload
ref="imageUploadRef"
label="Company Logo"
input-id="company-logo"
url-placeholder="https://example.com/logo.jpg"
preview-alt="Logo preview"
:disabled="isLoading"
@file-selected="handleImageSelected"
/>

<!-- Step 2 Actions -->
<div class="flex justify-between pt-4">
<Button variant="outline" :disabled="isLoading" @click="previousStep">
Back
</Button>
<div class="flex gap-2">
<Button :disabled="isLoading" @click="nextStep">
<span v-if="!imagePreview">Skip</span>
<Button
:disabled="isLoading || imageUploadRef?.isLoadingImageUrl"
@click="nextStep"
>
<span v-if="!imageUploadRef?.imagePreview">Skip</span>
<span v-else>Next</span>
</Button>
</div>
Expand Down Expand Up @@ -276,6 +260,7 @@ import {
StepperTrigger,
} from "@/components/ui/stepper";
import CompanyAutocomplete from "./CompanyAutocomplete.vue";
import ImageUpload from "@/components/ImageUpload.vue";
import {
createCompany,
createCompanyParticipation,
Expand Down Expand Up @@ -330,10 +315,15 @@ const formData = ref<CreateCompanyData>({
site: "",
});

// Image preview and file
const imagePreview = ref<string>("");
// Image upload ref
const imageUploadRef = ref<InstanceType<typeof ImageUpload> | null>(null);
const selectedImageFile = ref<File | null>(null);

// Handle image selection from ImageUpload component
const handleImageSelected = (file: File) => {
selectedImageFile.value = file;
};

// Representatives data
const representatives = ref<CreateCompanyRepData[]>([]);

Expand Down Expand Up @@ -392,30 +382,6 @@ const isValidUrl = (url: string) => {
}
};

// Image handling
const handleImageChange = (event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0];
if (file) {
// Check file size (10MB limit)
if (file.size > 10 << 20) {
errors.value.image = "Image file size must be less than 10MB";
return;
}

// Clear any previous image errors
delete errors.value.image;

selectedImageFile.value = file;

// Create preview
const reader = new FileReader();
reader.onload = (e) => {
imagePreview.value = e.target?.result as string;
};
reader.readAsDataURL(file);
}
};

// Representative management
const addRepresentative = () => {
representatives.value.push({
Expand Down
Loading