diff --git a/src/App.jsx b/src/App.jsx
index 8fad7b4..0848865 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,8 +1,12 @@
-import React from 'react';
-import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
-import Home from './pages/Home';
-import Login from './pages/Login';
-import { useAuthStore } from './store/authStore';
+import { BrowserRouter as Router, Navigate, Route, Routes } from "react-router-dom";
+import AppLayout from "./components/AppLayout";
+import Login from "./pages/Login";
+import CreateNotice from "./pages/CreateNotice";
+import EditNotice from "./pages/EditNotice";
+import MyNotices from "./pages/MyNotices";
+import NoticeDetails from "./pages/NoticeDetails";
+import NoticesPage from "./pages/NoticesPage";
+import { useAuthStore } from "./store/authStore";
const ProtectedRoute = ({ children }) => {
const token = useAuthStore((state) => state.token);
@@ -18,14 +22,19 @@ function App() {
} />
-
+
}
- />
- {/* Fallback */}
+ >
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
} />
diff --git a/src/api/categories.js b/src/api/categories.js
new file mode 100644
index 0000000..2e58b38
--- /dev/null
+++ b/src/api/categories.js
@@ -0,0 +1,12 @@
+import axios from "axios";
+import { useAuthStore } from "../store/authStore";
+
+const API_URL = "/api/v1";
+
+export async function listCategories() {
+ const { token } = useAuthStore.getState();
+ const headers = token ? { Authorization: `Bearer ${token}` } : {};
+
+ const response = await axios.get(`${API_URL}/vars/categories`, { headers });
+ return Array.isArray(response.data) ? response.data : [];
+}
diff --git a/src/api/notices.js b/src/api/notices.js
index 78bd291..648b6ca 100644
--- a/src/api/notices.js
+++ b/src/api/notices.js
@@ -2,10 +2,26 @@ 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();
+ return token ? { Authorization: `Bearer ${token}` } : {};
+};
+
+const buildImageUrl = (imageName) => `${API_URL}/images/get/${imageName}`;
+
+const fetchImageAsBlobUrl = async (imageUrl) => {
+ const headers = getAuthHeaders();
+ const response = await axios.get(imageUrl, {
+ headers,
+ responseType: "blob",
+ });
+ return URL.createObjectURL(response.data);
+};
export async function listNotices() {
- const { token } = useAuthStore.getState();
- const headers = token ? { Authorization: `Bearer ${token}` } : {};
+ const headers = getAuthHeaders();
const response = await fetch(`${API_URL}/notices/get/all`, {
headers: headers,
@@ -19,7 +35,10 @@ export async function listNotices() {
}
export async function getNoticeById(noticeId) {
- const response = await fetch(`${API_URL}/notices/get/${noticeId}`);
+ const headers = getAuthHeaders();
+ const response = await fetch(`${API_URL}/notices/get/${noticeId}`, {
+ headers,
+ });
const data = await response.json();
if (!response.ok) {
throw new Error("Error fetching notice");
@@ -28,16 +47,28 @@ export async function getNoticeById(noticeId) {
}
export async function createNotice(notice) {
- const { token } = useAuthStore.getState();
- const headers = token ? { Authorization: `Bearer ${token}` } : {};
+ const headers = getAuthHeaders();
+
+ const payload = {
+ title: notice.title,
+ description: notice.description,
+ price: notice.price,
+ category: notice.category,
+ status: notice.status,
+ attributes: notice.attributes,
+ };
+
try {
- const response = await axios.post(`${API_URL}/notices/add`, notice, {
+ const response = await axios.post(`${API_URL}/notices/add`, payload, {
headers: headers,
});
if (response.data.noticeId !== null && notice.image) {
for (let i = 0; i < notice.image.length; i++) {
- await uploadImage(response.data.noticeId, notice.image[i], i === 0);
+ const image = notice.image[i];
+ if (image instanceof File) {
+ await uploadImage(response.data.noticeId, image, i === 0);
+ }
}
}
@@ -49,41 +80,48 @@ export async function createNotice(notice) {
}
export async function getImageByNoticeId(noticeId) {
+ const headers = getAuthHeaders();
+
try {
- const listResponse = await axios.get(`${API_URL}/images/list/${noticeId}`);
+ const listResponse = await axios.get(`${API_URL}/images/list/${noticeId}`, {
+ headers,
+ });
const imageName = listResponse.data[0];
- return `${API_URL}/images/get/${imageName}`;
- } catch (err) {
- return "https://http.cat/404.jpg";
+ if (!imageName) {
+ return FALLBACK_IMAGE_URL;
+ }
+ return await fetchImageAsBlobUrl(buildImageUrl(imageName));
+ } catch {
+ return FALLBACK_IMAGE_URL;
}
}
export async function getAllImagesByNoticeId(noticeId) {
- const { token } = useAuthStore.getState();
- const headers = token ? { Authorization: `Bearer ${token}` } : {};
+ const headers = getAuthHeaders();
try {
const listResponse = await axios.get(`${API_URL}/images/list/${noticeId}`, {
headers: headers,
});
if (listResponse.data && listResponse.data.length > 0) {
- return listResponse.data.map((imageName) => ({
- uri: `${API_URL}/images/get/${imageName}`,
- headers: headers,
- }));
+ return await Promise.all(
+ listResponse.data.map(async (imageName) => {
+ try {
+ return await fetchImageAsBlobUrl(buildImageUrl(imageName));
+ } catch {
+ return FALLBACK_IMAGE_URL;
+ }
+ })
+ );
}
- return [{ uri: "https://http.cat/404.jpg" }];
- } catch (err) {
- return [{ uri: "https://http.cat/404.jpg" }];
+ return [FALLBACK_IMAGE_URL];
+ } catch {
+ return [FALLBACK_IMAGE_URL];
}
}
export const uploadImage = async (noticeId, file, isFirst) => {
- const { token } = useAuthStore.getState();
- const headers = {
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
- "Content-Type": "multipart/form-data",
- };
+ const headers = getAuthHeaders();
const formData = new FormData();
formData.append("file", file);
@@ -101,64 +139,62 @@ export const uploadImage = async (noticeId, file, isFirst) => {
};
export const deleteNotice = async (noticeId) => {
- const { token } = useAuthStore.getState();
- const headers = token ? { Authorization: `Bearer ${token}` } : {};
-
- try {
- const response = await axios.delete(
- `${API_URL}/notices/delete/${noticeId}`,
- { headers: headers }
- );
- return response.data;
- } catch (error) {
- throw error;
- }
+ const headers = getAuthHeaders();
+ const response = await axios.delete(
+ `${API_URL}/notices/delete/${noticeId}`,
+ { headers: headers }
+ );
+ return response.data;
};
export const editNotice = async (noticeId, notice) => {
- const { token } = useAuthStore.getState();
- const headers = token ? { Authorization: `Bearer ${token}` } : {};
+ const headers = getAuthHeaders();
+ const response = await axios.put(
+ `${API_URL}/notices/edit/${noticeId}`,
+ {
+ title: notice.title,
+ description: notice.description,
+ price: notice.price,
+ category: notice.category,
+ status: notice.status,
+ attributes: notice.attributes,
+ },
+ { headers: headers }
+ );
- try {
- const response = await axios.put(
- `${API_URL}/notices/edit/${noticeId}`,
- {
- title: notice.title,
- description: notice.description,
- price: notice.price,
- category: notice.category,
- status: notice.status,
- attributes: notice.attributes,
- },
- { headers: headers }
- );
-
- if (response.data && notice.image && notice.image.length > 0) {
- for (let i = 0; i < notice.image.length; i++) {
- const image = notice.image[i];
- if (image instanceof File) {
- await uploadImage(noticeId, image, i === 0);
- }
+ if (response.data && notice.image && notice.image.length > 0) {
+ for (let i = 0; i < notice.image.length; i++) {
+ const image = notice.image[i];
+ if (image instanceof File) {
+ await uploadImage(noticeId, image, i === 0);
}
}
-
- return response.data;
- } catch (error) {
- throw error;
}
+
+ return response.data;
};
export const deleteImage = async (filename) => {
- const { token } = useAuthStore.getState();
- const headers = token ? { Authorization: `Bearer ${token}` } : {};
+ const headers = getAuthHeaders();
+ const response = await axios.delete(
+ `${API_URL}/images/delete/${filename}`,
+ { headers: headers }
+ );
+ return response.data;
+};
+
+export async function listImageNamesByNoticeId(noticeId) {
+ const headers = getAuthHeaders();
try {
- const response = await axios.delete(
- `${API_URL}/images/delete/${filename}`,
- { headers: headers }
- );
- return response.data;
+ const response = await axios.get(`${API_URL}/images/list/${noticeId}`, {
+ headers,
+ });
+ return Array.isArray(response.data) ? response.data : [];
} catch (error) {
+ if (error.response?.status === 404) {
+ return [];
+ }
throw error;
}
-};
+}
diff --git a/src/components/AppLayout.jsx b/src/components/AppLayout.jsx
new file mode 100644
index 0000000..986bb1e
--- /dev/null
+++ b/src/components/AppLayout.jsx
@@ -0,0 +1,50 @@
+import { LogOut, PlusCircle } from "lucide-react";
+import { NavLink, Outlet } from "react-router-dom";
+import { useAuthStore } from "../store/authStore";
+
+const navClass = ({ isActive }) =>
+ `px-3 py-2 text-sm font-medium rounded-md transition-colors ${
+ isActive ? "bg-primary/10 text-primary" : "text-gray-600 hover:text-gray-900"
+ }`;
+
+export default function AppLayout() {
+ const { signOut } = useAuthStore();
+
+ return (
+
+
+
+
+ ArtisanConnect
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/NoticeCard.jsx b/src/components/NoticeCard.jsx
new file mode 100644
index 0000000..c496395
--- /dev/null
+++ b/src/components/NoticeCard.jsx
@@ -0,0 +1,136 @@
+import { useEffect, useState } from "react";
+import { Link } from "react-router-dom";
+import { listCategories } from "../api/categories";
+import { getImageByNoticeId } from "../api/notices";
+
+let categoryLabelCache = null;
+let categoryLabelPromise = null;
+
+const getCategoryLabelMap = async () => {
+ if (categoryLabelCache) {
+ return categoryLabelCache;
+ }
+ if (categoryLabelPromise) {
+ return categoryLabelPromise;
+ }
+
+ categoryLabelPromise = listCategories()
+ .then((items) => {
+ const map = new Map(
+ (items || []).map((item) => [String(item.value), item.label])
+ );
+ categoryLabelCache = map;
+ return map;
+ })
+ .catch(() => new Map())
+ .finally(() => {
+ categoryLabelPromise = null;
+ });
+
+ return categoryLabelPromise;
+};
+
+export default function NoticeCard({ notice, actions }) {
+ const [imageUrl, setImageUrl] = useState("https://http.cat/404.jpg");
+ const [categoryLabel, setCategoryLabel] = useState(notice?.category || "");
+
+ useEffect(() => {
+ let isMounted = true;
+
+ const loadImage = async () => {
+ if (!notice?.noticeId) {
+ return;
+ }
+ try {
+ const image = await getImageByNoticeId(notice.noticeId);
+ if (isMounted) {
+ setImageUrl(image);
+ } else if (typeof image === "string" && image.startsWith("blob:")) {
+ URL.revokeObjectURL(image);
+ }
+ } catch {
+ if (isMounted) {
+ setImageUrl("https://http.cat/404.jpg");
+ }
+ }
+ };
+
+ loadImage();
+
+ return () => {
+ isMounted = false;
+ };
+ }, [notice?.noticeId]);
+
+ useEffect(() => {
+ return () => {
+ if (typeof imageUrl === "string" && imageUrl.startsWith("blob:")) {
+ URL.revokeObjectURL(imageUrl);
+ }
+ };
+ }, [imageUrl]);
+
+ useEffect(() => {
+ let isMounted = true;
+
+ const resolveCategoryLabel = async () => {
+ if (!notice?.category) {
+ setCategoryLabel("");
+ return;
+ }
+
+ const map = await getCategoryLabelMap();
+ if (isMounted) {
+ setCategoryLabel(map.get(String(notice.category)) || notice.category);
+ }
+ };
+
+ resolveCategoryLabel();
+
+ return () => {
+ isMounted = false;
+ };
+ }, [notice?.category]);
+
+ if (!notice) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+ {notice.title}
+
+
+ {notice.price} zl
+
+
+
{categoryLabel}
+ {/* {notice.status && (
+
+ {notice.status}
+
+ )} */}
+ {actions ?
{actions}
: null}
+
+
+ );
+}
diff --git a/src/components/NoticeForm.jsx b/src/components/NoticeForm.jsx
new file mode 100644
index 0000000..0f1c5da
--- /dev/null
+++ b/src/components/NoticeForm.jsx
@@ -0,0 +1,213 @@
+import { useEffect, useMemo, useState } from "react";
+import { attributes } from "../data/attributesData";
+
+const defaultErrors = {
+ title: false,
+ description: false,
+ price: false,
+ category: false,
+};
+
+export default function NoticeForm({
+ initialValues,
+ categories,
+ submitLabel,
+ isSubmitting,
+ status = "ACTIVE",
+ onSubmit,
+}) {
+ const [title, setTitle] = useState(initialValues?.title ?? "");
+ const [description, setDescription] = useState(initialValues?.description ?? "");
+ const [price, setPrice] = useState(initialValues?.price?.toString() ?? "");
+ const [category, setCategory] = useState(initialValues?.category ?? "");
+ const [images, setImages] = useState(initialValues?.images ?? []);
+ const [selectedAttributes, setSelectedAttributes] = useState(
+ initialValues?.selectedAttributes ?? {}
+ );
+ const [errors, setErrors] = useState(defaultErrors);
+
+ const previewUrls = useMemo(
+ () =>
+ images.map((img) => (img instanceof File ? URL.createObjectURL(img) : img)),
+ [images]
+ );
+
+ useEffect(() => {
+ return () => {
+ previewUrls.forEach((url) => {
+ if (url.startsWith("blob:")) {
+ URL.revokeObjectURL(url);
+ }
+ });
+ };
+ }, [previewUrls]);
+
+ const handleFilesChange = (event) => {
+ const fileList = Array.from(event.target.files ?? []);
+ setImages(fileList);
+ };
+
+ const handleSubmit = async (event) => {
+ event.preventDefault();
+
+ const nextErrors = {
+ title: !title.trim(),
+ description: !description.trim(),
+ price: !price.toString().trim(),
+ category: !category.trim(),
+ };
+ setErrors(nextErrors);
+
+ if (Object.values(nextErrors).some(Boolean)) {
+ return;
+ }
+
+ const formattedAttributes = Object.entries(selectedAttributes)
+ .filter(([, value]) => value)
+ .map(([name, value]) => ({ name, value }));
+
+ await onSubmit({
+ title: title.trim(),
+ description: description.trim(),
+ price: price.toString().trim(),
+ category,
+ status,
+ image: images,
+ attributes: formattedAttributes,
+ });
+ };
+
+ return (
+
+ );
+}
diff --git a/src/data/attributesData.js b/src/data/attributesData.js
new file mode 100644
index 0000000..2b951c5
--- /dev/null
+++ b/src/data/attributesData.js
@@ -0,0 +1,28 @@
+export const attributes = {
+ Kolor: [
+ "Zielony",
+ "Czerwony",
+ "Niebieski",
+ "Zolty",
+ "Bialy",
+ "Czarny",
+ "Rozowy",
+ "Szary",
+ "Fioletowy",
+ "Pomaranczowy",
+ "Inny",
+ ],
+ Material: [
+ "Bawelna",
+ "Welna",
+ "Syntetyk",
+ "Skora",
+ "Len",
+ "Jedwab",
+ "Poliester",
+ "Akryl",
+ "Wiskoza",
+ "Nylon",
+ "Inny",
+ ],
+};
diff --git a/src/pages/CreateNotice.jsx b/src/pages/CreateNotice.jsx
new file mode 100644
index 0000000..fdbf416
--- /dev/null
+++ b/src/pages/CreateNotice.jsx
@@ -0,0 +1,55 @@
+import { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { listCategories } from "../api/categories";
+import NoticeForm from "../components/NoticeForm";
+import { useNoticesStore } from "../store/noticesStore";
+
+export default function CreateNotice() {
+ const navigate = useNavigate();
+ const { addNotice, fetchNotices } = useNoticesStore();
+ const [categories, setCategories] = useState([]);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ useEffect(() => {
+ let isMounted = true;
+
+ const loadCategories = async () => {
+ const data = await listCategories();
+ if (isMounted) {
+ setCategories(data);
+ }
+ };
+
+ loadCategories();
+
+ return () => {
+ isMounted = false;
+ };
+ }, []);
+
+ const handleSubmit = async (payload) => {
+ setIsSubmitting(true);
+ try {
+ const result = await addNotice(payload);
+ if (result) {
+ await fetchNotices();
+ navigate("/dashboard/my-notices");
+ }
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/pages/EditNotice.jsx b/src/pages/EditNotice.jsx
new file mode 100644
index 0000000..f0a4131
--- /dev/null
+++ b/src/pages/EditNotice.jsx
@@ -0,0 +1,149 @@
+import { useEffect, useMemo, useState } from "react";
+import { Link, useNavigate, useParams } from "react-router-dom";
+import { listCategories } from "../api/categories";
+import { getNoticeById } from "../api/notices";
+import NoticeForm from "../components/NoticeForm";
+import { useAuthStore } from "../store/authStore";
+import { useNoticesStore } from "../store/noticesStore";
+
+const mapAttributes = (notice) => {
+ const attrs = {};
+ if (!Array.isArray(notice?.attributes)) {
+ return attrs;
+ }
+ notice.attributes.forEach((attr) => {
+ if (attr?.name) {
+ attrs[attr.name] = attr.value;
+ }
+ });
+ return attrs;
+};
+
+export default function EditNotice() {
+ const { id } = useParams();
+ const navigate = useNavigate();
+ const { user_id } = useAuthStore();
+ const { fetchNotices, getAllImagesByNoticeId, editNotice } = useNoticesStore();
+ const [categories, setCategories] = useState([]);
+ const [currentNotice, setCurrentNotice] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ useEffect(() => {
+ let isMounted = true;
+
+ const loadData = async () => {
+ setIsLoading(true);
+ try {
+ const categoriesPromise = listCategories();
+ const cachedNotices = useNoticesStore.getState().notices;
+ if (!cachedNotices.length) {
+ await fetchNotices();
+ }
+
+ let notice = useNoticesStore
+ .getState()
+ .notices.find((item) => String(item.noticeId) === String(id));
+ if (!notice) {
+ notice = await getNoticeById(id);
+ }
+
+ const [categoriesData, imageUrls] = await Promise.all([
+ categoriesPromise,
+ getAllImagesByNoticeId(id),
+ ]);
+
+ if (isMounted) {
+ setCategories(categoriesData);
+ setCurrentNotice({
+ ...notice,
+ images: imageUrls.filter((url) => !url.includes("http.cat/404.jpg")),
+ });
+ }
+ } finally {
+ if (isMounted) {
+ setIsLoading(false);
+ }
+ }
+ };
+
+ loadData();
+
+ return () => {
+ isMounted = false;
+ };
+ }, [fetchNotices, getAllImagesByNoticeId, id]);
+
+ const isOwner = useMemo(() => {
+ if (!currentNotice) {
+ return false;
+ }
+ return String(currentNotice.clientId) === String(user_id);
+ }, [currentNotice, user_id]);
+
+ const initialValues = useMemo(() => {
+ if (!currentNotice) {
+ return null;
+ }
+ return {
+ title: currentNotice.title,
+ description: currentNotice.description,
+ price: currentNotice.price,
+ category: currentNotice.category,
+ images: currentNotice.images ?? [],
+ selectedAttributes: mapAttributes(currentNotice),
+ };
+ }, [currentNotice]);
+
+ const handleSubmit = async (payload) => {
+ setIsSubmitting(true);
+ try {
+ await editNotice(id, payload);
+ await fetchNotices();
+ navigate("/dashboard/my-notices");
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+ Ladowanie danych...
+
+ );
+ }
+
+ if (!currentNotice) {
+ return (
+
+ Nie znaleziono ogloszenia.
+
+ );
+ }
+
+ if (!isOwner) {
+ return (
+
+
Nie masz uprawnien do edycji tego ogloszenia.
+
+ Wroc do moich ogloszen
+
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx
index 3c2ebe4..5f8546c 100644
--- a/src/pages/Home.jsx
+++ b/src/pages/Home.jsx
@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+import { useEffect } from 'react';
import { useNoticesStore } from '../store/noticesStore';
import { useAuthStore } from '../store/authStore';
import { LogOut, Search, PlusCircle, Heart, User } from 'lucide-react';
@@ -9,7 +9,7 @@ export default function Home() {
useEffect(() => {
fetchNotices();
- }, []);
+ }, [fetchNotices]);
const activeNotices = notices.filter((notice) => notice.status === "ACTIVE");
diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx
index e611fac..14849ef 100644
--- a/src/pages/Login.jsx
+++ b/src/pages/Login.jsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useAuthStore } from '../store/authStore';
import { Eye, EyeOff, ArrowRight } from 'lucide-react';
diff --git a/src/pages/MyNotices.jsx b/src/pages/MyNotices.jsx
new file mode 100644
index 0000000..f958547
--- /dev/null
+++ b/src/pages/MyNotices.jsx
@@ -0,0 +1,105 @@
+import { useEffect, useMemo, useState } from "react";
+import { Link } from "react-router-dom";
+import NoticeCard from "../components/NoticeCard";
+import { useAuthStore } from "../store/authStore";
+import { useNoticesStore } from "../store/noticesStore";
+
+export default function MyNotices() {
+ const { user_id } = useAuthStore();
+ const { notices, deleteNotice, fetchNotices } = useNoticesStore();
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ let isMounted = true;
+
+ const loadNotices = async () => {
+ setIsLoading(true);
+ try {
+ await fetchNotices();
+ } finally {
+ if (isMounted) {
+ setIsLoading(false);
+ }
+ }
+ };
+
+ loadNotices();
+
+ return () => {
+ isMounted = false;
+ };
+ }, [fetchNotices]);
+
+ const userNotices = useMemo(
+ () =>
+ notices
+ .filter((notice) => String(notice.clientId) === String(user_id))
+ .sort((a, b) => new Date(b.publishDate) - new Date(a.publishDate)),
+ [notices, user_id]
+ );
+
+ const handleDelete = async (noticeId) => {
+ const confirmed = window.confirm("Usunac ogloszenie?");
+ if (!confirmed) {
+ return;
+ }
+ await deleteNotice(noticeId);
+ await fetchNotices();
+ };
+
+ return (
+
+
+
Moje ogloszenia
+
+ Dodaj nowe
+
+
+
+ {isLoading ? (
+
+ Ladowanie...
+
+ ) : null}
+
+ {!isLoading && userNotices.length === 0 ? (
+
+ Nie masz jeszcze ogloszen.
+
+ ) : null}
+
+ {!isLoading && userNotices.length > 0 ? (
+
+ {userNotices.map((notice) => (
+
+ {/* {notice.status === "INACTIVE" ? (*/
+
+ Edytuj
+
+ /*) : null} */}
+
+ >
+ }
+ key={notice.noticeId}
+ notice={notice}
+ />
+ ))}
+
+ ) : null}
+
+ );
+}
diff --git a/src/pages/NoticeDetails.jsx b/src/pages/NoticeDetails.jsx
new file mode 100644
index 0000000..2a22047
--- /dev/null
+++ b/src/pages/NoticeDetails.jsx
@@ -0,0 +1,170 @@
+import { useEffect, useMemo, useState } from "react";
+import { Link, useNavigate, useParams } from "react-router-dom";
+import { getAllImagesByNoticeId, getNoticeById } from "../api/notices";
+import { useAuthStore } from "../store/authStore";
+import { useNoticesStore } from "../store/noticesStore";
+
+export default function NoticeDetails() {
+ const { id } = useParams();
+ const navigate = useNavigate();
+ const { user_id } = useAuthStore();
+ const { notices, deleteNotice, fetchNotices } = useNoticesStore();
+ const [notice, setNotice] = useState(null);
+ const [images, setImages] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ let isMounted = true;
+
+ const loadData = 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);
+ setImages(imageUrls);
+ } else {
+ imageUrls.forEach((imageUrl) => {
+ if (typeof imageUrl === "string" && imageUrl.startsWith("blob:")) {
+ URL.revokeObjectURL(imageUrl);
+ }
+ });
+ }
+ } finally {
+ if (isMounted) {
+ setIsLoading(false);
+ }
+ }
+ };
+
+ loadData();
+
+ return () => {
+ isMounted = false;
+ };
+ }, [id, notices]);
+
+ useEffect(() => {
+ return () => {
+ images.forEach((imageUrl) => {
+ if (typeof imageUrl === "string" && imageUrl.startsWith("blob:")) {
+ URL.revokeObjectURL(imageUrl);
+ }
+ });
+ };
+ }, [images]);
+
+ const isOwner = useMemo(() => {
+ if (!notice) {
+ return false;
+ }
+ return String(notice.clientId) === String(user_id);
+ }, [notice, user_id]);
+
+ const handleDelete = async () => {
+ const confirmed = window.confirm("Usunac ogloszenie?");
+ if (!confirmed) {
+ return;
+ }
+
+ await deleteNotice(Number(id));
+ await fetchNotices();
+ navigate("/dashboard/my-notices");
+ };
+
+ if (isLoading) {
+ return (
+
+ Ladowanie ogloszenia...
+
+ );
+ }
+
+ if (!notice) {
+ return (
+
+ Nie znaleziono ogloszenia.
+
+ );
+ }
+
+ return (
+
+
+
+
{notice.title}
+
{notice.category}
+
+
+
+
+
+ {images.map((image, index) => (
+

+ ))}
+
+
Opis
+
{notice.description}
+
{notice.price} zl
+
+
+
+
+ {Array.isArray(notice.attributes) && notice.attributes.length > 0 ? (
+
+
+ Atrybuty
+
+
+ {notice.attributes.map((attr) => (
+
+ {attr.name}: {attr.value}
+
+ ))}
+
+
+ ) : null}
+
+
+
+ Wroc do listy
+
+ {isOwner ? (
+ <>
+ {notice.status === "INACTIVE" ? (
+
+ Edytuj
+
+ ) : null}
+
+ >
+ ) : null}
+
+
+ );
+}
diff --git a/src/pages/NoticesPage.jsx b/src/pages/NoticesPage.jsx
new file mode 100644
index 0000000..c7cc45c
--- /dev/null
+++ b/src/pages/NoticesPage.jsx
@@ -0,0 +1,215 @@
+import { useEffect, useMemo, useState } from "react";
+import { Link } from "react-router-dom";
+import NoticeCard from "../components/NoticeCard";
+import { listCategories } from "../api/categories";
+import { attributes } from "../data/attributesData";
+import { useNoticesStore } from "../store/noticesStore";
+
+const sortNotices = (items, sort) => {
+ if (sort === "latest") {
+ return [...items].sort((a, b) => new Date(b.publishDate) - new Date(a.publishDate));
+ }
+ if (sort === "oldest") {
+ return [...items].sort((a, b) => new Date(a.publishDate) - new Date(b.publishDate));
+ }
+ if (sort === "cheapest") {
+ return [...items].sort((a, b) => Number(a.price) - Number(b.price));
+ }
+ if (sort === "expensive") {
+ return [...items].sort((a, b) => Number(b.price) - Number(a.price));
+ }
+ return items;
+};
+
+export default function NoticesPage() {
+ const { notices, fetchNotices } = useNoticesStore();
+ const [isLoading, setIsLoading] = useState(true);
+ const [categories, setCategories] = useState([]);
+ const [search, setSearch] = useState("");
+ const [category, setCategory] = useState("");
+ const [sort, setSort] = useState("");
+ const [priceFrom, setPriceFrom] = useState("");
+ const [priceTo, setPriceTo] = useState("");
+ const [selectedAttributes, setSelectedAttributes] = useState({});
+
+ useEffect(() => {
+ let isMounted = true;
+
+ const loadData = async () => {
+ setIsLoading(true);
+ try {
+ const [categoriesData] = await Promise.all([listCategories(), fetchNotices()]);
+ if (isMounted) {
+ setCategories(categoriesData);
+ }
+ } finally {
+ if (isMounted) {
+ setIsLoading(false);
+ }
+ }
+ };
+
+ loadData();
+
+ return () => {
+ isMounted = false;
+ };
+ }, [fetchNotices]);
+
+ const filteredNotices = useMemo(() => {
+ let result = notices.filter((notice) => notice.status === "ACTIVE");
+
+ if (category) {
+ result = result.filter((notice) => notice.category === category);
+ }
+
+ if (search.trim()) {
+ const searchTerm = search.trim().toLowerCase();
+ result = result.filter((notice) => notice.title.toLowerCase().includes(searchTerm));
+ }
+
+ if (priceFrom) {
+ result = result.filter((notice) => Number(notice.price) >= Number(priceFrom));
+ }
+
+ if (priceTo) {
+ result = result.filter((notice) => Number(notice.price) <= Number(priceTo));
+ }
+
+ Object.entries(selectedAttributes).forEach(([attributeName, attributeValue]) => {
+ if (!attributeValue) {
+ return;
+ }
+ result = result.filter((notice) =>
+ notice.attributes?.some(
+ (attr) => attr.name === attributeName && attr.value === attributeValue
+ )
+ );
+ });
+
+ return sortNotices(result, sort);
+ }, [category, notices, priceFrom, priceTo, search, selectedAttributes, sort]);
+
+ const clearFilters = () => {
+ setSearch("");
+ setCategory("");
+ setSort("");
+ setPriceFrom("");
+ setPriceTo("");
+ setSelectedAttributes({});
+ };
+
+ return (
+
+
+
Ogloszenia
+
+ Dodaj ogloszenie
+
+
+
+
+
+ {isLoading ? (
+
+ Ladowanie ogloszen...
+
+ ) : null}
+
+ {!isLoading && filteredNotices.length === 0 ? (
+
+ Brak ogloszen dla wybranych filtrow.
+
+ ) : null}
+
+ {!isLoading && filteredNotices.length > 0 ? (
+
+ {filteredNotices.map((notice) => (
+
+ ))}
+
+ ) : null}
+
+ );
+}
diff --git a/src/store/noticesStore.js b/src/store/noticesStore.js
index 1b18598..5038a7e 100644
--- a/src/store/noticesStore.js
+++ b/src/store/noticesStore.js
@@ -4,6 +4,7 @@ import * as api from "../api/notices";
export const useNoticesStore = create((set, get) => ({
notices: [],
error: null,
+
fetchNotices: async () => {
set({ error: null });
try {
@@ -31,6 +32,17 @@ export const useNoticesStore = create((set, get) => ({
editNotice: async (noticeId, notice) => {
try {
+ const hasNewImages = Array.isArray(notice.image)
+ ? notice.image.some((img) => img instanceof File)
+ : false;
+
+ if (hasNewImages) {
+ const currentImageNames = await api.listImageNamesByNoticeId(noticeId);
+ for (const filename of currentImageNames) {
+ await api.deleteImage(filename);
+ }
+ }
+
const updatedNotice = await api.editNotice(noticeId, notice);
set((state) => ({
notices: state.notices.map((n) =>
@@ -51,6 +63,14 @@ export const useNoticesStore = create((set, get) => ({
);
},
+ getAllImagesByNoticeId: async (noticeId) => {
+ try {
+ return await api.getAllImagesByNoticeId(noticeId);
+ } catch {
+ return ["https://http.cat/404.jpg"];
+ }
+ },
+
deleteNotice: async (noticeId) => {
try {
await api.deleteNotice(noticeId);