diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 62eb234..bf36063 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.7.1", + "@fortawesome/free-regular-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.1", "@fortawesome/react-fontawesome": "^0.2.2", "@testing-library/jest-dom": "^5.17.0", @@ -2462,6 +2463,25 @@ "node": ">=6" } }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz", + "integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-regular-svg-icons/node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", + "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==", + "engines": { + "node": ">=6" + } + }, "node_modules/@fortawesome/free-solid-svg-icons": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 3a1a78a..36584df 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,6 +5,7 @@ "type": "module", "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.7.1", + "@fortawesome/free-regular-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.1", "@fortawesome/react-fontawesome": "^0.2.2", "@testing-library/jest-dom": "^5.17.0", diff --git a/frontend/src/ProtectedRoute.js b/frontend/src/ProtectedRoute.js new file mode 100644 index 0000000..4430747 --- /dev/null +++ b/frontend/src/ProtectedRoute.js @@ -0,0 +1,26 @@ +import React, { useEffect, useState } from 'react'; +import { Navigate } from 'react-router-dom'; +import axiosClient from '../axiosClient.js'; + +export default function ProtectedRoute({ children }) { + const [isAuthorized, setIsAuthorized] = useState(null); + + useEffect(() => { + const checkAuthorization = async () => { + try { + const response = await axiosClient.get('/getCurrentUser', { + headers: { Authorization: `Bearer ${localStorage.getItem('authToken')}` }, + }); + const user = response.data.user; + setIsAuthorized(user.isAdmin === 1); + } catch (error) { + console.error('Błąd podczas sprawdzania uprawnień:', error); + setIsAuthorized(false); + } + }; + + checkAuthorization(); + }, []); + + return isAuthorized ? children : ; +} diff --git a/frontend/src/components/Footer.js b/frontend/src/components/Footer.js index 0ecde75..3d28214 100644 --- a/frontend/src/components/Footer.js +++ b/frontend/src/components/Footer.js @@ -1,10 +1,14 @@ -import '../index.css' -import React from 'react' +import '../index.css'; +import React from 'react'; export default function Footer() { return ( - ) + ); } diff --git a/frontend/src/components/KebabsList.js b/frontend/src/components/KebabsList.js new file mode 100644 index 0000000..c4eac44 --- /dev/null +++ b/frontend/src/components/KebabsList.js @@ -0,0 +1,211 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faArrowDown, faHeart as solidHeart } from '@fortawesome/free-solid-svg-icons'; +import { faHeart as regularHeart } from '@fortawesome/free-regular-svg-icons'; +import axiosClient from '../axiosClient.js'; + +const daysTranslations = { + monday: 'Poniedziałek', + tuesday: 'Wtorek', + wednesday: 'Środa', + thursday: 'Czwartek', + friday: 'Piątek', + saturday: 'Sobota', + sunday: 'Niedziela', +}; + +const dayOrder = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; + +const translateStatus = (status) => { + const statusMap = { + exists: 'Istnieje', + closed: 'Zamknięty', + planned: 'Planowany', + }; + return statusMap[status] || 'Nieznany'; + }; + + export default function KebabsList({ kebabs, activeKebabIndex }) { + const [openIndex, setOpenIndex] = useState(null); + const [favorites, setFavorites] = useState([]); + const [sortedKebabs, setSortedKebabs] = useState([]); + const [userId, setUserId] = useState(null); + const kebabRefs = useRef([]); + + useEffect(() => { + if (activeKebabIndex !== null) { + setOpenIndex(activeKebabIndex); + if (kebabRefs.current[activeKebabIndex]) { + kebabRefs.current[activeKebabIndex].scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); + } + } + }, [activeKebabIndex]); + + useEffect(() => { + const fetchUser = async () => { + try { + const response = await axiosClient.get('/getCurrentUser', { + headers: { Authorization: `Bearer ${localStorage.getItem('authToken')}` }, + }); + const user = response.data.user; + setUserId(user.id); + setFavorites(user.favorites ? JSON.parse(user.favorites) : []); + } catch (error) { + console.error('Błąd pobierania danych użytkownika:', error); + } + }; + + fetchUser(); + }, []); + + useEffect(() => { + const sortKebabs = () => { + const favoriteKebabs = kebabs.filter((kebab) => favorites.includes(kebab.id)); + const nonFavoriteKebabs = kebabs.filter((kebab) => !favorites.includes(kebab.id)); + setSortedKebabs([...favoriteKebabs, ...nonFavoriteKebabs]); + }; + + sortKebabs(); + }, [favorites, kebabs]); + + const toggleDetails = (index) => { + setOpenIndex(openIndex === index ? null : index); + }; + + const handleFavoriteToggle = async (kebabId) => { + try { + if (favorites.includes(kebabId)) { + await axiosClient.post( + '/remfav', + { user_id: userId, kebab_id: kebabId }, + { + headers: { Authorization: `Bearer ${localStorage.getItem('authToken')}` }, + } + ); + setFavorites((prev) => prev.filter((id) => id !== kebabId)); + } else { + await axiosClient.post( + '/addfav', + { user_id: userId, kebab_id: kebabId }, + { + headers: { Authorization: `Bearer ${localStorage.getItem('authToken')}` }, + } + ); + setFavorites((prev) => [...prev, kebabId]); + } + } catch (error) { + console.error('Błąd przy przełączaniu ulubionych:', error); + } + }; + + return ( +
+ {sortedKebabs.map((kebab, index) => ( +
(kebabRefs.current[index] = el)} + className={`p-4 rounded-lg shadow-md bg-white ${ + activeKebabIndex === index ? 'border-2 border-blue-500' : '' + }`} + > +
+
+ handleFavoriteToggle(kebab.id)} + /> +
+ {kebab.name} +
+
+

{kebab.name}

+

{kebab.address}

+
+
+ + toggleDetails(index)} + /> +
+ +
+
+

Godziny otwarcia:

+ {dayOrder + .filter((day) => kebab.opening_hours[day]) + .map((day) => ( +

+ {daysTranslations[day]}: {kebab.opening_hours[day]} +

+ ))} + + {kebab.meats && kebab.meats.length > 0 && ( +
+

Mięsa:

+

{kebab.meats.join(', ')}

+
+ )} + + {kebab.sauces && kebab.sauces.length > 0 && ( +
+

Sosy:

+

{kebab.sauces.join(', ')}

+
+ )} + + {kebab.status && ( +
+

Status:

+

{translateStatus(kebab.status)}

+
+ )} + +
+

+ Rzemieślniczy: {kebab.is_crafted ? 'Tak' : 'Nie'} +

+

+ Na miejscu: {kebab.is_premises ? 'Tak' : 'Nie'} +

+

+ Sieciówka: {kebab.is_chainstore ? 'Tak' : 'Nie'} +

+
+ + {kebab.ordering_options && kebab.ordering_options.length > 0 && ( +
+

Opcje zamówień:

+
+ {kebab.ordering_options.map((option, index) => ( +

+ {option} +

+ ))} +
+
+ )} +
+
+
+ ))} +
+ ); +} diff --git a/frontend/src/components/Login.js b/frontend/src/components/Login.js index cb8d531..16bf088 100644 --- a/frontend/src/components/Login.js +++ b/frontend/src/components/Login.js @@ -14,22 +14,31 @@ export default function Login({ toggleForm }) { const { login } = useAppContext() const handleLogin = async (e) => { - e.preventDefault() + e.preventDefault(); try { const response = await axiosClient.post('/login', { email, password, - }) + }); + if (response.status === 200) { - toast.success('Logowanie zakończone sukcesem!', { autoClose: 2000 }) - login(response.data.token) - setTimeout(() => navigate('/map'), 2000) + toast.success('Logowanie zakończone sukcesem!', { autoClose: 2000 }); + login(response.data.token); + + const userResponse = await axiosClient.get('/getCurrentUser', { + headers: { Authorization: `Bearer ${response.data.token}` }, + }); + + console.log('Zalogowany użytkownik:', userResponse.data); + + setTimeout(() => navigate('/map'), 2000); } } catch (error) { - toast.error('Błąd logowania. Sprawdź swoje dane.', { autoClose: 2000 }) - console.error('Error logging in:', error) + toast.error('Błąd logowania. Sprawdź swoje dane.', { autoClose: 2000 }); + console.error('Error logging in:', error); } - } + }; + return (
diff --git a/frontend/src/components/ProtectedRoute.js b/frontend/src/components/ProtectedRoute.js new file mode 100644 index 0000000..9e26069 --- /dev/null +++ b/frontend/src/components/ProtectedRoute.js @@ -0,0 +1,30 @@ +import React, { useEffect, useState } from 'react'; +import { Navigate } from 'react-router-dom'; +import axiosClient from '../axiosClient.js'; + +export default function ProtectedRoute({ children }) { + const [isAuthorized, setIsAuthorized] = useState(null); + + useEffect(() => { + const checkAuthorization = async () => { + try { + const response = await axiosClient.get('/getCurrentUser', { + headers: { Authorization: `Bearer ${localStorage.getItem('authToken')}` }, + }); + const user = response.data.user; + setIsAuthorized(user.isAdmin === 1); + } catch (error) { + console.error('Błąd podczas sprawdzania uprawnień:', error); + setIsAuthorized(false); + } + }; + + checkAuthorization(); + }, []); + + if (isAuthorized === null) { + return
Ładowanie...
; + } + + return isAuthorized ? children : ; +} diff --git a/frontend/src/components/SearchPanel.js b/frontend/src/components/SearchPanel.js new file mode 100644 index 0000000..a8f77f4 --- /dev/null +++ b/frontend/src/components/SearchPanel.js @@ -0,0 +1,256 @@ +import React, { useState } from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faArrowDown } from '@fortawesome/free-solid-svg-icons'; + +export default function SearchPanel({ kebabs, onSearch }) { + const [searchQuery, setSearchQuery] = useState(''); + const [sortOrder, setSortOrder] = useState('asc'); + const [isFilterOpen, setIsFilterOpen] = useState(false); + const [showOpenNow, setShowOpenNow] = useState(false); + const [selectedSauces, setSelectedSauces] = useState([]); + const [selectedMeats, setSelectedMeats] = useState([]); + const [selectedOrderingOptions, setSelectedOrderingOptions] = useState([]); + + const allSauces = Array.from( + new Set(kebabs.flatMap((kebab) => kebab.sauces || [])) + ); + + const allMeats = Array.from( + new Set(kebabs.flatMap((kebab) => kebab.meats || [])) + ); + + const allOrderingOptions = Array.from( + new Set(kebabs.flatMap((kebab) => kebab.ordering_options || [])) + ); + + const getCurrentTimeDetails = () => { + const now = new Date(); + const currentHour = now.getHours(); + const currentMinute = now.getMinutes(); + const currentTime = currentHour + currentMinute / 60; + const currentDay = now.toLocaleString('en-US', { weekday: 'long' }).toLowerCase(); + return { currentTime, currentDay }; + }; + + const isOpenNow = (kebab) => { + const { currentTime, currentDay } = getCurrentTimeDetails(); + + if (!kebab.opening_hours || !kebab.opening_hours[currentDay]) { + return false; + } + + const [opening, closing] = kebab.opening_hours[currentDay] + .split('-') + .map((time) => { + const [hour, minute] = time.split(':').map(Number); + return hour + minute / 60; + }); + + return currentTime >= opening && currentTime < closing; + }; + + const handleSearchChange = (e) => { + const query = e.target.value; + setSearchQuery(query); + applyFilters(query, selectedSauces, selectedMeats, showOpenNow, sortOrder); + }; + + const handleSortChange = (order) => { + setSortOrder(order); + applyFilters(searchQuery, selectedSauces, selectedMeats, showOpenNow, order); + }; + + const handleSauceToggle = (sauce) => { + const updatedSauces = selectedSauces.includes(sauce) + ? selectedSauces.filter((s) => s !== sauce) + : [...selectedSauces, sauce]; + + setSelectedSauces(updatedSauces); + applyFilters(searchQuery, updatedSauces, selectedMeats, showOpenNow, sortOrder); + }; + + const handleMeatToggle = (meat) => { + const updatedMeats = selectedMeats.includes(meat) + ? selectedMeats.filter((m) => m !== meat) + : [...selectedMeats, meat]; + + setSelectedMeats(updatedMeats); + applyFilters(searchQuery, selectedSauces, updatedMeats, showOpenNow, sortOrder); + }; + + const handleOrderingOptionToggle = (option) => { + const updatedOptions = selectedOrderingOptions.includes(option) + ? selectedOrderingOptions.filter((o) => o !== option) + : [...selectedOrderingOptions, option]; + + setSelectedOrderingOptions(updatedOptions); + applyFilters(searchQuery, selectedSauces, selectedMeats, showOpenNow, sortOrder, updatedOptions); + }; + + const toggleOpenNow = () => { + const updatedShowOpenNow = !showOpenNow; + setShowOpenNow(updatedShowOpenNow); + applyFilters(searchQuery, selectedSauces, selectedMeats, updatedShowOpenNow, sortOrder); + }; + + const applyFilters = (query, sauces, meats, openNow, order, orderingOptions = []) => { + let filteredKebabs = kebabs; + + if (query) { + filteredKebabs = filteredKebabs.filter((kebab) => + kebab.name.toLowerCase().includes(query.toLowerCase()) + ); + } + + if (sauces.length > 0) { + filteredKebabs = filteredKebabs.filter((kebab) => + sauces.every((sauce) => kebab.sauces?.includes(sauce)) + ); + } + + if (meats.length > 0) { + filteredKebabs = filteredKebabs.filter((kebab) => + meats.every((meat) => kebab.meats?.includes(meat)) + ); + } + + if (orderingOptions.length > 0) { + filteredKebabs = filteredKebabs.filter((kebab) => + orderingOptions.every((option) => kebab.ordering_options?.includes(option)) + ); + } + + if (openNow) { + filteredKebabs = filteredKebabs.filter(isOpenNow); + } + + filteredKebabs = filteredKebabs.sort((a, b) => { + if (order === 'asc') { + return a.name.localeCompare(b.name); + } + return b.name.localeCompare(a.name); + }); + + onSearch(filteredKebabs); + }; + + return ( +
+ +
Sortowanie po nazwie:
+
+ + +
+ + {/* Filtrowanie */} +
setIsFilterOpen(!isFilterOpen)} + > + Filtrowanie + +
+
+
+ + + {/* Sosy */} +
+

Sosy:

+
+ {allSauces.map((sauce) => ( + + ))} +
+
+ + {/* Mięsa */} +
+

Mięsa:

+
+ {allMeats.map((meat) => ( + + ))} +
+
+ + {/* Opcje zamówienia */} +
+

Opcje zamówienia:

+
+ {allOrderingOptions.map((option) => ( + + ))} +
+
+
+
+
+ ); +} diff --git a/frontend/src/img/kebab_icon.png b/frontend/src/img/kebab_icon.png new file mode 100644 index 0000000..c0d0290 Binary files /dev/null and b/frontend/src/img/kebab_icon.png differ diff --git a/frontend/src/pages/AdminPanel.js b/frontend/src/pages/AdminPanel.js index ea00602..075b1f0 100644 --- a/frontend/src/pages/AdminPanel.js +++ b/frontend/src/pages/AdminPanel.js @@ -169,48 +169,34 @@ export default function AdminPanel() { const kebabId = selectedKebab?.id; const openKebabModal = (kebab) => { - const parsedOpeningHours = - typeof kebab.opening_hours === "string" - ? JSON.parse(kebab.opening_hours || "{}") - : kebab.opening_hours || {}; - - setSelectedKebab({ - ...kebab, - logo: kebab.logo, - opening_hours: parsedOpeningHours, - ordering_options: Array.isArray(kebab.ordering_options) - ? kebab.ordering_options - : JSON.parse(kebab.ordering_options || "[]"), - sauces: Array.isArray(kebab.sauces) - ? kebab.sauces - : JSON.parse(kebab.sauces || "[]"), - pages: Array.isArray(kebab.pages) - ? kebab.pages - : JSON.parse(kebab.pages || "[]"), - }); - - setInitialKebab({ - ...kebab, - opening_hours: parsedOpeningHours, - ordering_options: Array.isArray(kebab.ordering_options) - ? kebab.ordering_options - : JSON.parse(kebab.ordering_options || "[]"), - sauces: Array.isArray(kebab.sauces) - ? kebab.sauces - : JSON.parse(kebab.sauces || "[]"), - pages: Array.isArray(kebab.pages) - ? kebab.pages - : JSON.parse(kebab.pages || "[]"), - }); - - setLocalSauces(Array.isArray(kebab.sauces) ? kebab.sauces : JSON.parse(kebab.sauces || "[]")); - setLocalMeats([...kebab.meats]); - setLocalStatus(kebab.status || "exists"); - setLocalOpeningHours(parsedOpeningHours); - setLocalLogo(null); - setLocalOrderingOptions(Array.isArray(kebab.ordering_options) ? [...kebab.ordering_options] : []); - setLocalPages([...kebab.pages]); - setIsKebabModalOpen(true); + setSelectedKebab({ + ...kebab, + opening_hours: kebab.opening_hours || {}, + ordering_options: Array.isArray(kebab.ordering_options) + ? kebab.ordering_options + : [], + sauces: Array.isArray(kebab.sauces) ? kebab.sauces : [], + pages: kebab.pages || {}, + }); + + setInitialKebab({ + ...kebab, + opening_hours: kebab.opening_hours || {}, + ordering_options: Array.isArray(kebab.ordering_options) + ? kebab.ordering_options + : [], + sauces: Array.isArray(kebab.sauces) ? kebab.sauces : [], + pages: kebab.pages || {}, + }); + + setLocalSauces(Array.isArray(kebab.sauces) ? kebab.sauces : []); + setLocalMeats([...kebab.meats]); + setLocalStatus(kebab.status || "exists"); + setLocalOpeningHours(kebab.opening_hours || {}); + setLocalLogo(null); + setLocalOrderingOptions(Array.isArray(kebab.ordering_options) ? kebab.ordering_options : []); + setLocalPages(kebab.pages || {}); + setIsKebabModalOpen(true); }; const handleLogoChange = (file) => { @@ -848,7 +834,7 @@ export default function AdminPanel() { setSelectedKebab({ ...selectedKebab, @@ -868,7 +854,7 @@ export default function AdminPanel() { setSelectedKebab({ ...selectedKebab, diff --git a/frontend/src/pages/Home.js b/frontend/src/pages/Home.js index 0b70d40..e81c555 100644 --- a/frontend/src/pages/Home.js +++ b/frontend/src/pages/Home.js @@ -1,27 +1,73 @@ -import '../index.css' -import kebab_logo from '../img/kebab_logo.png' -import { useNavigate } from 'react-router-dom' -import Footer from '../components/Footer.js' -import { useEffect, useState } from 'react' -import 'react-toastify/dist/ReactToastify.css' -import { toast, ToastContainer } from 'react-toastify' +import '../index.css'; +import kebab_logo from '../img/kebab_logo.png'; +import { useNavigate } from 'react-router-dom'; +import Footer from '../components/Footer.js'; +import { useEffect, useState } from 'react'; +import 'react-toastify/dist/ReactToastify.css'; +import { toast, ToastContainer } from 'react-toastify'; +import axiosClient from '../axiosClient.js'; export default function Home() { - const navigate = useNavigate() - const [isLoggedIn, setIsLoggedIn] = useState(false) + const navigate = useNavigate(); + const [isLoggedIn, setIsLoggedIn] = useState(false); + const [isAdmin, setIsAdmin] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const [suggestion, setSuggestion] = useState(''); useEffect(() => { - const token = localStorage.getItem('authToken') + const token = localStorage.getItem('authToken'); if (token) { - setIsLoggedIn(true) + setIsLoggedIn(true); + + axiosClient + .get('/getCurrentUser', { + headers: { Authorization: `Bearer ${token}` }, + }) + .then((response) => { + const user = response.data.user; + setIsAdmin(user.isAdmin === 1); + }) + .catch((error) => { + console.error('Błąd pobierania danych użytkownika:', error); + }); } - }, []) + }, []); const handleLogout = () => { - localStorage.removeItem('authToken') - setIsLoggedIn(false) - toast.success('Wylogowano pomyślnie!', { autoClose: 2000 }) - } + localStorage.removeItem('authToken'); + setIsLoggedIn(false); + setIsAdmin(false); + toast.success('Wylogowano pomyślnie!', { autoClose: 2000 }); + }; + + const handleSuggestionSubmit = async () => { + const token = localStorage.getItem('authToken'); + if (!suggestion.trim()) { + toast.error('Proszę wpisać treść sugestii.'); + return; + } + + try { + const userResponse = await axiosClient.get('/getCurrentUser', { + headers: { Authorization: `Bearer ${token}` }, + }); + + const user = userResponse.data.user?.name || 'Anonimowy użytkownik'; + + await axiosClient.post( + '/suggestions', + { user, contents: suggestion }, + { headers: { Authorization: `Bearer ${token}` } } + ); + + toast.success('Sugestia została wysłana!'); + setIsModalOpen(false); + setSuggestion(''); + } catch (error) { + toast.error('Nie udało się wysłać sugestii. Spróbuj ponownie później.'); + console.error('Błąd podczas wysyłania sugestii:', error); + } + }; return (
@@ -35,8 +81,7 @@ export default function Home() { LEGNICA KEBAB CITY TOUR

- Legnica Kebab City Tour jest to aplikacja oraz witryna internetowa służąca pomocą w odnalezieniu lokalizacji wszystkich dostępnych, w planach oraz zamkniętych punktów gastronomicznych serwujących słynne Kebaby. Poniższe przyciski pokierują Cię dalej. Wybierz przycisk "Mapa" aby bezpośrednio odnaleźć Kebaby rozsiane po Legnicy. Możesz też utworzyć konto lub zalogować się aby dodać Twojego ulubionego Kebaba do zakładki "Ulubione", dzięki czemu łatwiej go odnajdziesz!! -

+ Legnica Kebab City Tour jest to aplikacja oraz witryna internetowa służąca pomocą w odnalezieniu lokalizacji wszystkich dostępnych, w planach oraz zamkniętych punktów gastronomicznych serwujących słynne Kebaby. Poniższe przyciski pokierują Cię dalej. Wybierz przycisk "Mapa" aby bezpośrednio odnaleźć Kebaby rozsiane po Legnicy. Możesz też utworzyć konto lub zalogować się aby na mapie dodać Twojego ulubionego Kebaba, dzięki czemu łatwiej go odnajdziesz!! Dodatkowo po zalogowaniu pojawi się przycisk "Sugestia", nie krępuj się i zostaw nam swoją opinię!

+ <> + + + {isAdmin && ( + + )} + ) : (