From a847a8768d143be0f08fc8cc94eb3113013243d7 Mon Sep 17 00:00:00 2001 From: Andrii Solianyk Date: Sun, 26 Apr 2026 11:04:04 +0200 Subject: [PATCH] ADD: Registration page and update image handling in notices --- src/App.jsx | 2 + src/api/notices.js | 14 +-- src/components/NoticeCard.jsx | 50 ++++++++-- src/pages/EditNotice.jsx | 2 +- src/pages/NoticeDetails.jsx | 55 +++++++--- src/pages/Registration.jsx | 183 ++++++++++++++++++++++++++++++++++ src/store/noticesStore.js | 4 +- 7 files changed, 279 insertions(+), 31 deletions(-) create mode 100644 src/pages/Registration.jsx diff --git a/src/App.jsx b/src/App.jsx index 0848865..98f379a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -7,6 +7,7 @@ import MyNotices from "./pages/MyNotices"; import NoticeDetails from "./pages/NoticeDetails"; import NoticesPage from "./pages/NoticesPage"; import { useAuthStore } from "./store/authStore"; +import Registration from "@/pages/Registration.jsx"; const ProtectedRoute = ({ children }) => { const token = useAuthStore((state) => state.token); @@ -21,6 +22,7 @@ function App() { } /> + } /> diff --git a/src/api/notices.js b/src/api/notices.js index 648b6ca..af4c657 100644 --- a/src/api/notices.js +++ b/src/api/notices.js @@ -2,7 +2,6 @@ import axios from "axios"; import { useAuthStore } from "../store/authStore"; const API_URL = "/api/v1"; -const FALLBACK_IMAGE_URL = "https://http.cat/404.jpg"; const getAuthHeaders = () => { const { token } = useAuthStore.getState(); @@ -88,11 +87,11 @@ export async function getImageByNoticeId(noticeId) { }); const imageName = listResponse.data[0]; if (!imageName) { - return FALLBACK_IMAGE_URL; + return null; } return await fetchImageAsBlobUrl(buildImageUrl(imageName)); } catch { - return FALLBACK_IMAGE_URL; + return null; } } @@ -104,19 +103,20 @@ export async function getAllImagesByNoticeId(noticeId) { }); if (listResponse.data && listResponse.data.length > 0) { - return await Promise.all( + const imageUrls = await Promise.all( listResponse.data.map(async (imageName) => { try { return await fetchImageAsBlobUrl(buildImageUrl(imageName)); } catch { - return FALLBACK_IMAGE_URL; + return null; } }) ); + return imageUrls.filter((url) => typeof url === "string" && url.trim()); } - return [FALLBACK_IMAGE_URL]; + return []; } catch { - return [FALLBACK_IMAGE_URL]; + return []; } } diff --git a/src/components/NoticeCard.jsx b/src/components/NoticeCard.jsx index c496395..754a6dc 100644 --- a/src/components/NoticeCard.jsx +++ b/src/components/NoticeCard.jsx @@ -31,7 +31,8 @@ const getCategoryLabelMap = async () => { }; export default function NoticeCard({ notice, actions }) { - const [imageUrl, setImageUrl] = useState("https://http.cat/404.jpg"); + const [imageUrl, setImageUrl] = useState(null); + const [isImageLoading, setIsImageLoading] = useState(true); const [categoryLabel, setCategoryLabel] = useState(notice?.category || ""); useEffect(() => { @@ -39,18 +40,32 @@ export default function NoticeCard({ notice, actions }) { const loadImage = async () => { if (!notice?.noticeId) { + if (isMounted) { + setImageUrl(null); + setIsImageLoading(false); + } return; } + + if (isMounted) { + setIsImageLoading(true); + } + try { const image = await getImageByNoticeId(notice.noticeId); if (isMounted) { - setImageUrl(image); + const normalizedImage = + typeof image === "string" && image.trim() ? image : null; + + setImageUrl(normalizedImage); + setIsImageLoading(false); } else if (typeof image === "string" && image.startsWith("blob:")) { URL.revokeObjectURL(image); } } catch { if (isMounted) { - setImageUrl("https://http.cat/404.jpg"); + setImageUrl(null); + setIsImageLoading(false); } } }; @@ -99,11 +114,30 @@ export default function NoticeCard({ notice, actions }) { return (
- {notice.title} + {isImageLoading || !imageUrl ? ( + isImageLoading ? ( +
+ +
+ ) : ( +
+ + No Image + +
+ ) + ) : ( + {notice.title} setImageUrl(null)} + src={imageUrl} + /> + )}
diff --git a/src/pages/EditNotice.jsx b/src/pages/EditNotice.jsx index f0a4131..cc2bfa6 100644 --- a/src/pages/EditNotice.jsx +++ b/src/pages/EditNotice.jsx @@ -57,7 +57,7 @@ export default function EditNotice() { setCategories(categoriesData); setCurrentNotice({ ...notice, - images: imageUrls.filter((url) => !url.includes("http.cat/404.jpg")), + images: imageUrls, }); } } finally { diff --git a/src/pages/NoticeDetails.jsx b/src/pages/NoticeDetails.jsx index 2a22047..ba604dd 100644 --- a/src/pages/NoticeDetails.jsx +++ b/src/pages/NoticeDetails.jsx @@ -12,21 +12,33 @@ export default function NoticeDetails() { const [notice, setNotice] = useState(null); const [images, setImages] = useState([]); const [isLoading, setIsLoading] = useState(true); + const [isImagesLoading, setIsImagesLoading] = useState(true); useEffect(() => { let isMounted = true; - const loadData = async () => { + const loadNotice = async () => { setIsLoading(true); try { let found = notices.find((item) => String(item.noticeId) === String(id)); if (!found) { found = await getNoticeById(id); } - - const imageUrls = await getAllImagesByNoticeId(id); if (isMounted) { setNotice(found); + } + } finally { + if (isMounted) { + setIsLoading(false); + } + } + }; + + const loadImages = async () => { + setIsImagesLoading(true); + try { + const imageUrls = await getAllImagesByNoticeId(id); + if (isMounted) { setImages(imageUrls); } else { imageUrls.forEach((imageUrl) => { @@ -37,12 +49,13 @@ export default function NoticeDetails() { } } finally { if (isMounted) { - setIsLoading(false); + setIsImagesLoading(false); } } }; - loadData(); + loadNotice(); + loadImages(); return () => { isMounted = false; @@ -104,14 +117,30 @@ export default function NoticeDetails() {
- {images.map((image, index) => ( - {`${notice.title} - ))} + {isImagesLoading ? ( +
+ +
+ ) : images.length > 0 ? ( + images.map((image, index) => ( + {`${notice.title} + )) + ) : ( +
+ + No Image + +
+ )}

Opis

{notice.description}

diff --git a/src/pages/Registration.jsx b/src/pages/Registration.jsx new file mode 100644 index 0000000..6925c1c --- /dev/null +++ b/src/pages/Registration.jsx @@ -0,0 +1,183 @@ +import { useState } from 'react'; +import { useNavigate, Link } from 'react-router-dom'; +import { useAuthStore } from '../store/authStore'; +import { Eye, EyeOff, ArrowRight } from 'lucide-react'; + +export default function Registration() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [emailError, setEmailError] = useState(''); + const [passwordError, setPasswordError] = useState(''); + const [formError, setFormError] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const { signUp, isLoading } = useAuthStore(); + const navigate = useNavigate(); + + const validateEmail = (email) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + const handleRegistration = async (e) => { + e.preventDefault(); + setFormError(''); + + if (!email || !password || !confirmPassword) { + setFormError('Proszę uzupełnić wszystkie pola.'); + return; + } + + if (!validateEmail(email)) { + setEmailError('Nieprawidłowy format adresu email'); + return; + } + + if (password !== confirmPassword) { + setPasswordError('Hasła nie są takie same'); + return; + } + + try { + await signUp({ email, password }); + navigate('/'); + } catch (e) { + setFormError(e.response?.data?.message || e.message || 'Błąd rejestracji'); + } + }; + + if (isLoading) { + return ( +
+
+
+ ); + } + + return ( +
+
+
+

Rejestracja

+
+ Masz już konto? + + Zaloguj się + + +
+
+ +
+ {formError &&

{formError}

} + +
+ {emailError &&

{emailError}

} + { + setEmail(e.target.value); + setFormError(''); + if (e.target.value && !validateEmail(e.target.value)) { + setEmailError('Nieprawidłowy format adresu email'); + } else { + setEmailError(''); + } + }} + className={`w-full px-4 py-3 rounded-lg border ${emailError ? 'border-red-500' : 'border-gray-300'} focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all`} + required + /> +
+ +
+ { + const nextPassword = e.target.value; + setPassword(nextPassword); + setFormError(''); + if (confirmPassword && nextPassword !== confirmPassword) { + setPasswordError('Hasła nie są takie same'); + } else { + setPasswordError(''); + } + }} + className={`w-full px-4 py-3 rounded-lg border ${passwordError ? 'border-red-500' : 'border-gray-300'} focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all`} + required + /> + +
+ +
+ {passwordError &&

{passwordError}

} + { + const nextConfirmPassword = e.target.value; + setConfirmPassword(nextConfirmPassword); + setFormError(''); + if (password && password !== nextConfirmPassword) { + setPasswordError('Hasła nie są takie same'); + } else { + setPasswordError(''); + } + }} + className={`w-full px-4 py-3 rounded-lg border ${passwordError ? 'border-red-500' : 'border-gray-300'} focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all`} + required + /> + +
+ + +
+ +
+
+
+
+
+ lub +
+
+ + +
+
+ ); +} diff --git a/src/store/noticesStore.js b/src/store/noticesStore.js index 5038a7e..363779a 100644 --- a/src/store/noticesStore.js +++ b/src/store/noticesStore.js @@ -46,7 +46,7 @@ export const useNoticesStore = create((set, get) => ({ const updatedNotice = await api.editNotice(noticeId, notice); set((state) => ({ notices: state.notices.map((n) => - n.noticeId == noticeId ? updatedNotice : n + n.noticeId === noticeId ? updatedNotice : n ), })); return updatedNotice; @@ -67,7 +67,7 @@ export const useNoticesStore = create((set, get) => ({ try { return await api.getAllImagesByNoticeId(noticeId); } catch { - return ["https://http.cat/404.jpg"]; + return []; } },