feat: add size warning

This commit is contained in:
Maulana Sodiqin
2025-11-28 19:54:48 +07:00
parent 1ee04ada14
commit 692a7a7258
5 changed files with 129 additions and 59 deletions
@@ -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>
+6 -4
View File
@@ -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
+53 -26
View File
@@ -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>