feat: add size warning
This commit is contained in:
@@ -60,6 +60,20 @@ const UserOnboardingPage: FC = (): ReactElement => {
|
|||||||
const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
|
// Validate file size (max 2MB)
|
||||||
|
if (file.size > 2 * 1024 * 1024) {
|
||||||
|
toast.error('The file is too large. Maximum size is 2MB.');
|
||||||
|
e.target.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file type
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
toast.error('The file must be an image');
|
||||||
|
e.target.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setAvatarFile(file);
|
setAvatarFile(file);
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onloadend = () => {
|
reader.onloadend = () => {
|
||||||
@@ -150,7 +164,7 @@ const UserOnboardingPage: FC = (): ReactElement => {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-500 mt-2 text-center">
|
<p className="text-xs text-gray-500 dark:text-gray-500 mt-2 text-center">
|
||||||
Optional, but highly recommended
|
Optional, but highly recommended. Max 2MB
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -84,15 +84,17 @@ const ProfilePage: FC<ProfileModalProps> = ({
|
|||||||
const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
// Validate file size (max 5MB)
|
// Validate file size (max 2MB)
|
||||||
if (file.size > 5 * 1024 * 1024) {
|
if (file.size > 2 * 1024 * 1024) {
|
||||||
toast.error('The file is too large. Maximum 5MB');
|
toast.error('The file is too large. Maximum size is 2MB.');
|
||||||
|
e.target.value = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate file type
|
// Validate file type
|
||||||
if (!file.type.startsWith('image/')) {
|
if (!file.type.startsWith('image/')) {
|
||||||
toast.error('The file must be an image');
|
toast.error('The file must be an image');
|
||||||
|
e.target.value = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +237,7 @@ const ProfilePage: FC<ProfileModalProps> = ({
|
|||||||
<p className="text-sm text-gray-500 dark:text-neutral-400 text-center font-sans">
|
<p className="text-sm text-gray-500 dark:text-neutral-400 text-center font-sans">
|
||||||
Click the camera icon to change your photo
|
Click the camera icon to change your photo
|
||||||
<br />
|
<br />
|
||||||
Format: JPG, PNG. Max 5MB
|
Format: JPG, PNG. Max 2MB
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|||||||
|
|
||||||
import { CitySelect } from '../../../../components/city-select';
|
import { CitySelect } from '../../../../components/city-select';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB
|
||||||
|
|
||||||
const EditTeamPage: FC = (): ReactElement => {
|
const EditTeamPage: FC = (): ReactElement => {
|
||||||
const { teamId } = useParams<{ teamId: string }>();
|
const { teamId } = useParams<{ teamId: string }>();
|
||||||
@@ -85,6 +88,11 @@ const EditTeamPage: FC = (): ReactElement => {
|
|||||||
const handleLogoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleLogoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
|
if (file.size > MAX_FILE_SIZE) {
|
||||||
|
toast.error('Logo image is too large. Maximum size is 2MB.');
|
||||||
|
e.target.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
setLogoFile(file);
|
setLogoFile(file);
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onloadend = () => {
|
reader.onloadend = () => {
|
||||||
@@ -97,6 +105,11 @@ const EditTeamPage: FC = (): ReactElement => {
|
|||||||
const handleBannerChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleBannerChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
|
if (file.size > MAX_FILE_SIZE) {
|
||||||
|
toast.error('Banner image is too large. Maximum size is 2MB.');
|
||||||
|
e.target.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
setBannerFile(file);
|
setBannerFile(file);
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onloadend = () => {
|
reader.onloadend = () => {
|
||||||
@@ -183,7 +196,7 @@ const EditTeamPage: FC = (): ReactElement => {
|
|||||||
Click to upload banner
|
Click to upload banner
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
||||||
1200x400 recommended
|
1200x400 recommended. Max 2MB
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
@@ -215,32 +228,35 @@ const EditTeamPage: FC = (): ReactElement => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col md:flex-row items-start justify-start gap-1">
|
<div>
|
||||||
<label htmlFor="logo" className="cursor-pointer">
|
<div className="flex flex-col md:flex-row items-start justify-start gap-1">
|
||||||
<span className="px-4 py-2 text-sm bg-primary-600 text-white rounded-lg hover:bg-primary-700 inline-block">
|
<label htmlFor="logo" className="cursor-pointer">
|
||||||
{logoPreview ? 'Change Logo' : 'Upload Logo'}
|
<span className="px-4 py-2 text-sm bg-primary-600 text-white rounded-lg hover:bg-primary-700 inline-block">
|
||||||
</span>
|
{logoPreview ? 'Change Logo' : 'Upload Logo'}
|
||||||
<input
|
</span>
|
||||||
id="logo"
|
<input
|
||||||
type="file"
|
id="logo"
|
||||||
accept="image/*"
|
type="file"
|
||||||
className="hidden"
|
accept="image/*"
|
||||||
onChange={handleLogoChange}
|
className="hidden"
|
||||||
/>
|
onChange={handleLogoChange}
|
||||||
</label>
|
/>
|
||||||
{logoPreview && (
|
</label>
|
||||||
<button
|
{logoPreview && (
|
||||||
type="button"
|
<button
|
||||||
onClick={() => {
|
type="button"
|
||||||
setLogoFile(null);
|
onClick={() => {
|
||||||
setLogoPreview('');
|
setLogoFile(null);
|
||||||
form.setValue('logo', null);
|
setLogoPreview('');
|
||||||
}}
|
form.setValue('logo', null);
|
||||||
className="px-4 py-2 text-sm bg-danger-600 text-white rounded-lg hover:bg-danger-700 cursor-pointer"
|
}}
|
||||||
>
|
className="px-4 py-2 text-sm bg-danger-600 text-white rounded-lg hover:bg-danger-700 cursor-pointer"
|
||||||
Remove
|
>
|
||||||
</button>
|
Remove
|
||||||
)}
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-400 dark:text-gray-500 mt-2">Max 2MB</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ import {
|
|||||||
useAuthStore,
|
useAuthStore,
|
||||||
} from '@imphnen-frontend-service/service';
|
} from '@imphnen-frontend-service/service';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
const MIN_TEAM_MEMBERS = 2; // Minimum members required to submit (including leader)
|
const MIN_TEAM_MEMBERS = 2; // Minimum members required to submit (including leader)
|
||||||
|
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB
|
||||||
|
|
||||||
const SubmitProjectPage: FC = (): ReactElement => {
|
const SubmitProjectPage: FC = (): ReactElement => {
|
||||||
const { teamId } = useParams<{ teamId: string }>();
|
const { teamId } = useParams<{ teamId: string }>();
|
||||||
@@ -89,6 +91,14 @@ const SubmitProjectPage: FC = (): ReactElement => {
|
|||||||
const files = e.target.files;
|
const files = e.target.files;
|
||||||
if (!files) return;
|
if (!files) return;
|
||||||
|
|
||||||
|
// Validate file sizes
|
||||||
|
const oversizedFiles = Array.from(files).filter(file => file.size > MAX_FILE_SIZE);
|
||||||
|
if (oversizedFiles.length > 0) {
|
||||||
|
toast.error(`${oversizedFiles.length} file(s) are too large. Maximum size is 2MB per file.`);
|
||||||
|
e.target.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const uploadPromises = Array.from(files).map((file) => uploadFile(file));
|
const uploadPromises = Array.from(files).map((file) => uploadFile(file));
|
||||||
const results = await Promise.all(uploadPromises);
|
const results = await Promise.all(uploadPromises);
|
||||||
@@ -96,6 +106,7 @@ const SubmitProjectPage: FC = (): ReactElement => {
|
|||||||
setScreenshots([...screenshots, ...urls]);
|
setScreenshots([...screenshots, ...urls]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to upload screenshots:', error);
|
console.error('Failed to upload screenshots:', error);
|
||||||
|
toast.error('Failed to upload screenshots. Please try again.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -271,7 +282,7 @@ const SubmitProjectPage: FC = (): ReactElement => {
|
|||||||
Click to upload screenshots
|
Click to upload screenshots
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1 font-sans">
|
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1 font-sans">
|
||||||
PNG, JPG up to 5MB each
|
PNG, JPG. Max 2MB each
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ import { useNavigate } from 'react-router';
|
|||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
import { teamCreateSchema, TTeamCreateForm, useCreateTeam, ETeamVisibility, useUploadFile } from '@imphnen-frontend-service/service';
|
import { teamCreateSchema, TTeamCreateForm, useCreateTeam, ETeamVisibility, useUploadFile } from '@imphnen-frontend-service/service';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import { CitySelect } from '../../../components/city-select';
|
import { CitySelect } from '../../../components/city-select';
|
||||||
|
|
||||||
|
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB
|
||||||
|
|
||||||
const CreateTeamPage: FC = (): ReactElement => {
|
const CreateTeamPage: FC = (): ReactElement => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [logoFile, setLogoFile] = useState<File | null>(null);
|
const [logoFile, setLogoFile] = useState<File | null>(null);
|
||||||
@@ -31,6 +34,11 @@ const CreateTeamPage: FC = (): ReactElement => {
|
|||||||
const handleLogoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleLogoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
|
if (file.size > MAX_FILE_SIZE) {
|
||||||
|
toast.error('Logo image is too large. Maximum size is 2MB.');
|
||||||
|
e.target.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
setLogoFile(file);
|
setLogoFile(file);
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onloadend = () => {
|
reader.onloadend = () => {
|
||||||
@@ -43,6 +51,11 @@ const CreateTeamPage: FC = (): ReactElement => {
|
|||||||
const handleBannerChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleBannerChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
|
if (file.size > MAX_FILE_SIZE) {
|
||||||
|
toast.error('Banner image is too large. Maximum size is 2MB.');
|
||||||
|
e.target.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
setBannerFile(file);
|
setBannerFile(file);
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onloadend = () => {
|
reader.onloadend = () => {
|
||||||
@@ -73,9 +86,20 @@ const CreateTeamPage: FC = (): ReactElement => {
|
|||||||
banner: bannerUrl,
|
banner: bannerUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
toast.success('Team created successfully!');
|
||||||
navigate(`/teams/${result.data.id}`);
|
navigate(`/teams/${result.data.id}`);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Failed to create team:', error);
|
console.error('Failed to create team:', error);
|
||||||
|
|
||||||
|
// Handle specific error messages
|
||||||
|
const message = error?.message || '';
|
||||||
|
if (message.includes('413') || message.includes('length limit') || message.includes('too large')) {
|
||||||
|
toast.error('Image file is too large. Please use smaller images (max 2MB each).');
|
||||||
|
} else if (message.includes('already a member')) {
|
||||||
|
toast.error(message);
|
||||||
|
} else {
|
||||||
|
toast.error(message || 'Failed to create team. Please try again.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -119,7 +143,7 @@ const CreateTeamPage: FC = (): ReactElement => {
|
|||||||
<label className="flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-300 dark:border-neutral-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-neutral-800">
|
<label className="flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-300 dark:border-neutral-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-neutral-800">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-gray-500 dark:text-neutral-400">Click to upload banner</p>
|
<p className="text-gray-500 dark:text-neutral-400">Click to upload banner</p>
|
||||||
<p className="text-xs text-gray-400 dark:text-neutral-500 mt-1">1200x400 recommended</p>
|
<p className="text-xs text-gray-400 dark:text-neutral-500 mt-1">1200x400 recommended. Max 2MB</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
@@ -149,30 +173,33 @@ const CreateTeamPage: FC = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="logo" className="cursor-pointer">
|
<div>
|
||||||
<span className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 inline-block">
|
<label htmlFor="logo" className="cursor-pointer">
|
||||||
{logoPreview ? 'Change Logo' : 'Upload Logo'}
|
<span className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 inline-block">
|
||||||
</span>
|
{logoPreview ? 'Change Logo' : 'Upload Logo'}
|
||||||
<input
|
</span>
|
||||||
id="logo"
|
<input
|
||||||
type="file"
|
id="logo"
|
||||||
accept="image/*"
|
type="file"
|
||||||
className="hidden"
|
accept="image/*"
|
||||||
onChange={handleLogoChange}
|
className="hidden"
|
||||||
/>
|
onChange={handleLogoChange}
|
||||||
</label>
|
/>
|
||||||
{logoPreview && (
|
</label>
|
||||||
<button
|
{logoPreview && (
|
||||||
type="button"
|
<button
|
||||||
onClick={() => {
|
type="button"
|
||||||
setLogoFile(null);
|
onClick={() => {
|
||||||
setLogoPreview('');
|
setLogoFile(null);
|
||||||
}}
|
setLogoPreview('');
|
||||||
className="ml-3 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600"
|
}}
|
||||||
>
|
className="ml-3 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600"
|
||||||
Remove
|
>
|
||||||
</button>
|
Remove
|
||||||
)}
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-400 dark:text-neutral-500 mt-2">Max 2MB</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user