diff --git a/src/components/AuthMiddleware.tsx b/src/components/AuthMiddleware.tsx index a1bab8f..6bfae92 100644 --- a/src/components/AuthMiddleware.tsx +++ b/src/components/AuthMiddleware.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { AppDispatch, RootState } from '../redux/store'; import { getUserData } from '../redux/thunks/userThunks'; +import { logout } from '../redux/slices/userSlice'; // Simple debounce utility const useDebounce = (callback: Function, delay: number) => { @@ -28,7 +29,7 @@ const AuthMiddleware: React.FC = ({ children }) => { const { token, role, loading, error } = user; const [hasAttempted, setHasAttempted] = useState(false); const attemptCountRef = useRef(0); - const MAX_ATTEMPTS = 3; + const MAX_ATTEMPTS = 1; // Debug log whenever user state changes useEffect(() => { @@ -47,11 +48,24 @@ const AuthMiddleware: React.FC = ({ children }) => { attemptCountRef.current += 1; const response = await dispatch(getUserData()); console.log('Fetch response:', response); - const role = useSelector((state: RootState) => state.user.role); - console.log('Fetched role:', role); + // Don't use hooks here - removed the invalid hook call } }, 1000); + // Effect to check for max attempts and clear token if needed + useEffect(() => { + if ( + attemptCountRef.current >= MAX_ATTEMPTS && + error && + token && + error.includes("404") + ) { + console.log('Maximum attempts reached with 404 errors. Clearing invalid token.'); + dispatch(logout()); + // No need for window.location.reload() as the state change will trigger a re-render + } + }, [attemptCountRef.current, error, token, dispatch]); + useEffect(() => { // If we have a token but no role and haven't tried fetching yet if (token && !role && !loading && !hasAttempted) { @@ -74,34 +88,6 @@ const AuthMiddleware: React.FC = ({ children }) => { } }, [token, role, loading, hasAttempted, debouncedFetchUserData]); - // For extreme debugging, force-update the role from localStorage if available - useEffect(() => { - // This is a workaround if the reducer isn't working properly - const tryRestoreRoleFromLocalStorage = () => { - try { - // Only do this if we've exhausted our API attempts - if (attemptCountRef.current >= MAX_ATTEMPTS && token && !role) { - const savedUserData = localStorage.getItem('userData'); - if (savedUserData) { - const userData = JSON.parse(savedUserData); - if (userData.role) { - console.log('Restoring role from localStorage:', userData.role); - // You would need to add a setRole action to your userSlice - // dispatch(setRole(userData.role)); - window.location.reload(); // Extreme measure, reload the page - } - } - } - } catch (e) { - console.error('Error restoring role from localStorage:', e); - } - }; - - if (attemptCountRef.current >= MAX_ATTEMPTS) { - tryRestoreRoleFromLocalStorage(); - } - }, [attemptCountRef.current, token, role, dispatch]); - return <>{children}; }; diff --git a/src/components/NotificationPermission/NotificationPermission.module.css b/src/components/NotificationPermission/NotificationPermission.module.css deleted file mode 100644 index 145b6a1..0000000 --- a/src/components/NotificationPermission/NotificationPermission.module.css +++ /dev/null @@ -1,70 +0,0 @@ -.notificationPrompt { - position: fixed; - bottom: 20px; - right: 20px; - background: white; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - z-index: 1000; - max-width: 400px; - width: 100%; - overflow: hidden; -} - -.promptContent { - padding: 16px; - display: flex; - flex-direction: column; - align-items: center; -} - -.bellIcon { - font-size: 48px; - color: #3b82f6; - margin-bottom: 16px; -} - -.promptText { - text-align: center; - margin-bottom: 16px; -} - -.promptText h3 { - margin: 0 0 8px 0; - font-size: 18px; - font-weight: 600; -} - -.promptText p { - margin: 0; - color: #6b7280; - font-size: 14px; -} - -.promptActions { - display: flex; - justify-content: center; - gap: 12px; - width: 100%; -} - -.allowButton { - background-color: #3b82f6 !important; - color: white !important; - padding: 8px 16px !important; - font-weight: 500 !important; -} - -.dismissButton { - color: #6b7280 !important; -} - -@media (max-width: 480px) { - .notificationPrompt { - bottom: 0; - right: 0; - width: 100%; - max-width: 100%; - border-radius: 8px 8px 0 0; - } -} \ No newline at end of file diff --git a/src/components/NotificationPermission/NotificationPermission.tsx b/src/components/NotificationPermission/NotificationPermission.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/feature/AccountSettings/AccountSettingsModal.module.css b/src/feature/AccountSettings/AccountSettingsModal.module.css new file mode 100644 index 0000000..cec5d2a --- /dev/null +++ b/src/feature/AccountSettings/AccountSettingsModal.module.css @@ -0,0 +1,237 @@ +.modalOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + backdrop-filter: blur(4px); +} + +.modalContent { + background: white; + border-radius: 16px; + width: 90%; + max-width: 450px; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + animation: slideIn 0.3s ease; +} + +@keyframes slideIn { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +.modalHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.modalHeader h2 { + margin: 0; + font-size: 20px; + font-weight: 700; + color: #111827; +} + +.closeButton { + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: #6b7280; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: all 0.2s ease; +} + +.closeButton:hover { + background-color: #f3f4f6; + color: #111827; +} + +.modalBody { + padding: 24px; +} + +.loadingSpinner { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 30px 0; +} + +.spinner { + border: 4px solid rgba(0, 0, 0, 0.05); + border-radius: 50%; + border-top: 4px solid #b0f484; + width: 36px; + height: 36px; + animation: spin 1s linear infinite; + margin-bottom: 12px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.profileSection { + display: flex; + align-items: center; + margin-bottom: 24px; +} + +.avatarContainer { + width: 64px; + height: 64px; + border-radius: 50%; + background: linear-gradient(135deg, #50703C 0%, #7fa25c 100%); + display: flex; + justify-content: center; + align-items: center; + margin-right: 20px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.avatarInitials { + color: white; + font-size: 24px; + font-weight: 600; +} + +.profileInfo { + flex: 1; +} + +.userName { + margin: 0; + font-size: 20px; + font-weight: 600; + color: #111827; +} + +.infoSection { + background-color: #f9fafb; + border-radius: 12px; + padding: 16px; + margin-bottom: 24px; +} + +.infoItem { + margin-bottom: 16px; +} + +.infoItem:last-child { + margin-bottom: 0; +} + +.infoLabel { + font-size: 12px; + color: #6b7280; + margin-bottom: 4px; +} + +.infoValue { + font-size: 16px; + color: #111827; + font-weight: 500; +} + +.inputField { + width: 100%; + padding: 10px 12px; + border: 1px solid #e5e7eb; + border-radius: 8px; + font-size: 16px; + transition: all 0.2s; + background-color: white; +} + +.inputField:focus { + border-color: #b0f484; + outline: none; + box-shadow: 0 0 0 3px rgba(176, 244, 132, 0.15); +} + +.actionsSection { + display: flex; + flex-direction: column; + gap: 12px; +} + +.actionButton { + padding: 12px 16px; + border-radius: 8px; + font-size: 16px; + font-weight: 500; + cursor: pointer; + border: none; + background-color: #f3f4f6; + color: #111827; + transition: all 0.2s ease; + text-align: center; +} + +.actionButton:hover { + background-color: #e5e7eb; + transform: translateY(-1px); +} + +.actionButton:active { + transform: translateY(0); +} + +.saveButton { + background-color: #b0f484; + color: #111827; +} + +.saveButton:hover { + background-color: #86efac; +} + +.logoutButton { + background-color: #fee2e2; + color: #b91c1c; +} + +.logoutButton:hover { + background-color: #fecaca; +} + +@media (max-width: 640px) { + .modalContent { + width: 95%; + border-radius: 16px; + max-height: 80vh; + } + + .modalHeader { + padding: 16px 20px; + } + + .modalBody { + padding: 20px; + } +} \ No newline at end of file diff --git a/src/feature/AccountSettings/AccountSettingsModal.tsx b/src/feature/AccountSettings/AccountSettingsModal.tsx new file mode 100644 index 0000000..c2c1acc --- /dev/null +++ b/src/feature/AccountSettings/AccountSettingsModal.tsx @@ -0,0 +1,235 @@ +import React, { useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { + updateEmail, + updatePassword, + updateUsername, + getUserData +} from '../../redux/thunks/userThunks'; +import { logout } from '../../redux/slices/userSlice'; +import { RootState, AppDispatch } from '../../redux/store'; +import styles from './AccountSettingsModal.module.css'; +import { IoCloseOutline, IoPersonOutline, IoMailOutline, IoKeyOutline, IoLogOutOutline } from 'react-icons/io5'; + +interface AccountSettingsModalProps { + isOpen: boolean; + onClose: () => void; +} + +const AccountSettingsModal: React.FC = ({ isOpen, onClose }) => { + const dispatch = useDispatch(); + const { name_surname, email, phoneNumber, loading } = useSelector((state: RootState) => state.user); + + const [isEditing, setIsEditing] = useState(false); + const [editedValues, setEditedValues] = useState({ + name_surname: '', + email: '', + }); + + // Update local state when user data changes + useEffect(() => { + setEditedValues({ + name_surname: name_surname || '', + email: email || '', + }); + }, [name_surname, email]); + + // Handle modal close + const handleClose = () => { + setIsEditing(false); + onClose(); + }; + + // Handle edit mode toggle + const handleEditToggle = () => { + if (isEditing) { + handleSaveChanges(); + } else { + setIsEditing(true); + } + }; + + // Handle saving changes + const handleSaveChanges = async () => { + if (window.confirm('Do you want to save these changes?')) { + const updates = []; + + if (editedValues.name_surname !== name_surname) { + updates.push(dispatch(updateUsername({ newUsername: editedValues.name_surname }))); + } + + if (editedValues.email !== email) { + updates.push(dispatch(updateEmail({ + oldEmail: email || '', + newEmail: editedValues.email + }))); + } + + if (updates.length > 0) { + try { + const results = await Promise.all(updates); + const hasErrors = results.some((result) => result.type.endsWith('/rejected')); + + if (!hasErrors) { + alert('Profile updated successfully'); + dispatch(getUserData()); + setIsEditing(false); + } else { + alert('Some updates failed. Please try again.'); + } + } catch (error) { + alert('Failed to update profile'); + } + } else { + setIsEditing(false); + } + } else { + // Cancel editing + setEditedValues({ + name_surname: name_surname || '', + email: email || '', + }); + setIsEditing(false); + } + }; + + // Handle password reset + const handlePasswordReset = () => { + const oldPassword = prompt('Enter your current password'); + if (oldPassword) { + const newPassword = prompt('Enter your new password'); + if (newPassword) { + dispatch(updatePassword({ + oldPassword: oldPassword, + newPassword: newPassword, + })) + .then((resultAction) => { + if (updatePassword.fulfilled.match(resultAction)) { + alert('Password updated successfully'); + } else { + alert(resultAction.payload || 'Failed to update password'); + } + }) + .catch(() => { + alert('Failed to update password'); + }); + } + } + }; + + // Handle logout + const handleLogout = () => { + if (window.confirm('Are you sure you want to logout?')) { + try { + localStorage.removeItem('userToken'); + dispatch(logout()); + handleClose(); + window.location.href = '/'; + } catch (error) { + console.error('Logout failed:', error); + } + } + }; + + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()}> +
+

Account Settings

+ +
+ +
+ {loading ? ( +
+
+ Loading... +
+ ) : ( + <> +
+
+
+ {name_surname?.split(' ').map(n => n[0]).join('').toUpperCase() || '?'} +
+
+
+ {isEditing ? ( + setEditedValues({...editedValues, name_surname: e.target.value})} + placeholder="Full Name" + /> + ) : ( +

{name_surname}

+ )} +
+
+ +
+
+
Email
+
+ {isEditing ? ( + setEditedValues({...editedValues, email: e.target.value})} + placeholder="Email address" + /> + ) : ( + {email} + )} +
+
+ + {phoneNumber && ( +
+
Phone
+
{phoneNumber}
+
+ )} +
+ +
+ + + + + +
+ + )} +
+
+
+ ); +}; + +export default AccountSettingsModal; + diff --git a/src/feature/Header/Header.module.css b/src/feature/Header/Header.module.css index cd059e1..a5dac4f 100644 --- a/src/feature/Header/Header.module.css +++ b/src/feature/Header/Header.module.css @@ -99,4 +99,57 @@ text-overflow: ellipsis; white-space: nowrap; } +} + +/* Add this to your existing Header.module.css */ + +.notificationButton { + background: none; + border: none; + margin-right: 20px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + position: relative; + padding: 5px; +} + +.notificationIcon { + font-size: 24px; + color: #333; + transition: color 0.2s ease; +} + +.notificationButton:hover .notificationIcon { + color: #007bff; +} + +.accountButton { + background: none; + border: none; + color: #50703C; + cursor: pointer; + padding: 8px; + margin-right: 8px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.accountButton:hover { + background-color: rgba(80, 112, 60, 0.1); + transform: translateY(-1px); +} + +.settingsIcon { + font-size: 20px; +} + +@media (max-width: 768px) { + .accountButton { + margin-right: 4px; + } } \ No newline at end of file diff --git a/src/feature/Header/Header.tsx b/src/feature/Header/Header.tsx index 86ff90d..8028b03 100644 --- a/src/feature/Header/Header.tsx +++ b/src/feature/Header/Header.tsx @@ -1,16 +1,22 @@ -import React, {useEffect} from 'react'; +import React, { useEffect, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useSelector, useDispatch } from 'react-redux'; import styles from './Header.module.css'; import logo from '../../assets/fresh-deal-logo.svg'; import { RootState, AppDispatch } from '../../redux/store'; import { logout } from '../../redux/slices/userSlice'; -import { IoLogOutOutline } from 'react-icons/io5'; +import { IoLogOutOutline, IoSettingsOutline } from 'react-icons/io5'; +import { IoMdNotificationsOutline, IoMdNotifications } from 'react-icons/io'; +import NotificationModal from './components/NotificationModal'; +import AccountSettingsModal from '../AccountSettings/AccountSettingsModal'; const Header: React.FC = () => { const navigate = useNavigate(); const dispatch = useDispatch(); - const { token, name_surname, role } = useSelector((state: RootState) => state.user); + const { token, name_surname} = useSelector((state: RootState) => state.user); + const [isNotificationModalOpen, setIsNotificationModalOpen] = useState(false); + const [isAccountModalOpen, setIsAccountModalOpen] = useState(false); + const [notificationStatus, setNotificationStatus] = useState(false); useEffect(() => { const token = localStorage.getItem('userToken'); @@ -20,8 +26,35 @@ const Header: React.FC = () => { else { console.error('Could not find token'); } + + // Check notification permission on load + if ('Notification' in window && navigator.serviceWorker) { + checkNotificationStatus(); + } }, []); + const checkNotificationStatus = async () => { + if ('serviceWorker' in navigator) { + try { + const registration = await navigator.serviceWorker.ready; + const subscription = await registration.pushManager.getSubscription(); + setNotificationStatus(!!subscription); + } catch (error) { + console.error('Error checking notification status:', error); + } + } + }; + + const handleNotificationToggle = () => { + setIsNotificationModalOpen(!isNotificationModalOpen); + }; + + const handleCloseModal = () => { + setIsNotificationModalOpen(false); + // Check notification status again after modal is closed + checkNotificationStatus(); + }; + const handleLogout = async () => { try { localStorage.removeItem('userToken'); @@ -32,6 +65,10 @@ const Header: React.FC = () => { } }; + const handleAccountSettings = () => { + setIsAccountModalOpen(true); + }; + return (
@@ -40,6 +77,29 @@ const Header: React.FC = () => {
+ {token && ( + <> + + + + + )} + {token ? (
{name_surname} @@ -58,6 +118,16 @@ const Header: React.FC = () => { )}
+ + + + setIsAccountModalOpen(false)} + />
); }; diff --git a/src/feature/Header/components/NotificationModal.module.css b/src/feature/Header/components/NotificationModal.module.css new file mode 100644 index 0000000..79d54ee --- /dev/null +++ b/src/feature/Header/components/NotificationModal.module.css @@ -0,0 +1,113 @@ +.modalOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: flex-start; + justify-content: flex-end; + z-index: 1000; +} + +.modalContent { + background: white; + border-radius: 5px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + width: 320px; + margin-top: 60px; + margin-right: 20px; + animation: slideIn 0.2s ease-out; + overflow: hidden; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.modalHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + border-bottom: 1px solid #eee; +} + +.modalHeader h3 { + margin: 0; + font-size: 18px; +} + +.closeButton { + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: #555; +} + +.closeButton:hover { + color: #333; +} + +.statusContainer { + padding: 15px 20px; + border-bottom: 1px solid #eee; +} + +.statusContainer p { + margin: 5px 0; + font-size: 14px; +} + +.error { + padding: 10px 20px; + background-color: #fff0f0; + color: #e74c3c; + font-size: 14px; + border-bottom: 1px solid #eee; +} + +.actions { + padding: 15px 20px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.button { + padding: 8px 16px; + font-size: 14px; + border: none; + border-radius: 4px; + cursor: pointer; + background-color: #007bff; + color: white; + font-weight: 500; + transition: background-color 0.2s; +} + +.button:disabled { + background-color: #cccccc; + cursor: not-allowed; +} + +.button:hover:not(:disabled) { + background-color: #0069d9; +} + +.testButton { + background-color: #28a745; +} + +.testButton:hover { + background-color: #218838; +} \ No newline at end of file diff --git a/src/feature/RestaurantDetails/components/NotificationTest/NotificationTest.tsx b/src/feature/Header/components/NotificationModal.tsx similarity index 56% rename from src/feature/RestaurantDetails/components/NotificationTest/NotificationTest.tsx rename to src/feature/Header/components/NotificationModal.tsx index ed4ac3d..d34e638 100644 --- a/src/feature/RestaurantDetails/components/NotificationTest/NotificationTest.tsx +++ b/src/feature/Header/components/NotificationModal.tsx @@ -1,32 +1,51 @@ import React, { useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { RootState } from '../../../../redux/store'; -import { API_BASE_URL } from '../../../../redux/Api/apiService'; -import styles from './NotificationTest.module.css'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../../redux/store'; +import { API_BASE_URL } from '../../../redux/Api/apiService'; +import styles from './NotificationModal.module.css'; -const NotificationTest: React.FC = () => { +interface NotificationModalProps { + isOpen: boolean; + onClose: () => void; +} + +const NotificationModal: React.FC = ({ isOpen, onClose }) => { const [status, setStatus] = useState<'default' | 'granted' | 'denied'>('default'); const [isSubscribed, setIsSubscribed] = useState(false); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const token = useSelector((state: RootState) => state.user.token); - const dispatch = useDispatch(); useEffect(() => { if ('Notification' in window) { setStatus(Notification.permission); - console.log('Initial notification permission state:', Notification.permission); } }, []); + useEffect(() => { + // Check subscription status when modal opens + if (isOpen) { + checkSubscriptionStatus(); + } + }, [isOpen]); + + const checkSubscriptionStatus = async () => { + if ('serviceWorker' in navigator) { + try { + const registration = await navigator.serviceWorker.ready; + const subscription = await registration.pushManager.getSubscription(); + setIsSubscribed(!!subscription); + } catch (error) { + console.error('Error checking subscription status:', error); + } + } + }; + const registerServiceWorker = async () => { try { - console.log('Registering service worker...'); const registration = await navigator.serviceWorker.register('/service-worker.js'); - console.log('Service worker registered successfully'); const subscription = await registration.pushManager.getSubscription(); - console.log('Current subscription status:', !!subscription); setIsSubscribed(!!subscription); return registration; } catch (error) { @@ -36,38 +55,22 @@ const NotificationTest: React.FC = () => { }; const urlBase64ToUint8Array = (base64String: string) => { - console.log('Converting base64 to Uint8Array. Raw input:', base64String); - - // 1. Check if the key is in PEM format (starts with MF...) + // Check if the key is in PEM format if (base64String.startsWith('MF')) { - console.log('Detected PEM format key, converting...'); - try { - // 2. Convert from standard base64 to a raw binary string const rawBinary = atob(base64String); - console.log('Length after standard base64 decode:', rawBinary.length); - - // 3. Extract key bits (65 bytes for P-256) const extractedKey = rawBinary.slice(-65); - console.log('Extracted key length:', extractedKey.length); - - // 4. Convert the binary string to Uint8Array const uint8Array = new Uint8Array(extractedKey.length); for (let i = 0; i < extractedKey.length; i++) { uint8Array[i] = extractedKey.charCodeAt(i); } - - console.log('Final Uint8Array length:', uint8Array.length); - console.log('First few bytes:', Array.from(uint8Array.slice(0, 5))); - return uint8Array; } catch (e) { - console.error('Error converting PEM format key:', e); throw new Error('Failed to convert applicationServerKey from PEM format'); } } - // Standard URL-safe base64 processing for already correctly formatted keys + // Standard URL-safe base64 processing let normalizedBase64 = base64String .replace(/-/g, '+') .replace(/_/g, '/'); @@ -78,24 +81,15 @@ const NotificationTest: React.FC = () => { normalizedBase64 += '='.repeat(4 - paddingNeeded); } - console.log('Processed base64 string:', normalizedBase64); - try { const binaryString = atob(normalizedBase64); - console.log('Decoded binary string length:', binaryString.length); - const uint8Array = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { uint8Array[i] = binaryString.charCodeAt(i); } - - console.log('Final Uint8Array length:', uint8Array.length); - console.log('First few bytes:', Array.from(uint8Array.slice(0, 5))); - return uint8Array; } catch (e) { - console.error('Error in standard base64 conversion:', e); - throw new Error('Failed to convert applicationServerKey: ' + e.message); + throw new Error(`Failed to convert applicationServerKey: ${e instanceof Error ? e.message : 'Unknown error'}`); } }; @@ -109,52 +103,41 @@ const NotificationTest: React.FC = () => { setError(null); try { - console.log('Requesting notification permission...'); const permission = await Notification.requestPermission(); - console.log('Permission response:', permission); setStatus(permission); if (permission !== 'granted') { throw new Error('Notification permission denied'); } - console.log('Registering service worker...'); const registration = await registerServiceWorker(); let subscription = await registration.pushManager.getSubscription(); if (!subscription) { - console.log('No existing subscription, fetching VAPID key...'); const response = await fetch(`${API_BASE_URL}/web-push/vapid-public-key`); - console.log('VAPID key response status:', response.status); if (!response.ok) { throw new Error('Failed to fetch VAPID key'); } const responseData = await response.json(); - console.log('VAPID key response data:', responseData); - const { publicKey } = responseData; + if (!publicKey) { throw new Error('Invalid VAPID key received from server'); } try { const applicationServerKey = urlBase64ToUint8Array(publicKey); - - console.log('Attempting to subscribe with converted key...'); subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey }); - console.log('Subscription successful:', subscription); } catch (subError) { - console.error('Subscription error detail:', subError); throw new Error(`Failed to subscribe: ${subError instanceof Error ? subError.message : 'Unknown error'}`); } } - console.log('Sending subscription to server...'); const subscribeResponse = await fetch(`${API_BASE_URL}/web-push/subscribe`, { method: 'POST', headers: { @@ -163,7 +146,6 @@ const NotificationTest: React.FC = () => { }, body: JSON.stringify({ subscription: subscription.toJSON() }) }); - console.log('Server response status:', subscribeResponse.status); if (!subscribeResponse.ok) { throw new Error('Failed to register subscription with server'); @@ -171,9 +153,7 @@ const NotificationTest: React.FC = () => { setIsSubscribed(true); setError(null); - console.log('Subscription process completed successfully'); } catch (err) { - console.error('Subscription error:', err); setError(err instanceof Error ? err.message : 'Failed to subscribe to notifications'); setIsSubscribed(false); } finally { @@ -185,59 +165,63 @@ const NotificationTest: React.FC = () => { if (!token) return; try { - console.log('Sending test notification request...'); const response = await fetch(`${API_BASE_URL}/web-push/test`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` } }); - console.log('Test notification response status:', response.status); if (!response.ok) { throw new Error('Failed to send test notification'); } - console.log('Test notification sent successfully'); } catch (error) { - console.error('Test notification error:', error); setError('Failed to send test notification'); } }; + if (!isOpen) return null; + return ( -
-

Notification Status

-
-

Permission: {status}

-

Subscribed: {isSubscribed ? 'Yes' : 'No'}

-
+
+
e.stopPropagation()}> +
+

Notification Status

+ +
- {error && ( -
- {error} +
+

Permission: {status}

+

Subscribed: {isSubscribed ? 'Yes' : 'No'}

- )} - -
- - - {isSubscribed && ( + + {error && ( +
+ {error} +
+ )} + +
- )} + + {isSubscribed && ( + + )} +
); }; -export default NotificationTest; \ No newline at end of file +export default NotificationModal; \ No newline at end of file diff --git a/src/feature/Login/Login.module.css b/src/feature/Login/Login.module.css index 1c8a7ed..fa4cf8e 100644 --- a/src/feature/Login/Login.module.css +++ b/src/feature/Login/Login.module.css @@ -99,16 +99,15 @@ .loginContainer { background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border-radius: 20px; padding: 40px; - border-radius: 24px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); width: 100%; - max-width: 400px; + max-width: 450px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); position: relative; z-index: 2; - backdrop-filter: blur(10px); - border: 1px solid rgba(74, 222, 128, 0.2); - animation: fadeIn 0.4s ease-out; + animation: fadeIn 0.6s ease-out; } .loginContainer h1 { @@ -232,6 +231,83 @@ text-decoration: underline; } +/* Login toggle styles */ +.loginToggle { + display: flex; + margin-bottom: 20px; + background: rgba(236, 252, 243, 0.7); + border-radius: 12px; + padding: 4px; +} + +.toggleButton { + flex: 1; + background: transparent; + border: none; + padding: 12px; + border-radius: 10px; + font-weight: 500; + color: #4b5563; + cursor: pointer; + transition: all 0.3s ease; +} + +.toggleButton.active { + background: #16a34a; + color: white; + box-shadow: 0 2px 8px rgba(22, 163, 74, 0.3); +} + +/* Phone input group styles */ +.phoneInputGroup { + display: flex; + gap: 8px; + align-items: stretch; +} + +.countryCodeSelect { + flex: 0 0 auto; + min-width: 120px; + border-radius: 10px; + border: 1px solid #d1d5db; + background-color: #f9fafb; + padding: 0 10px; + font-size: 16px; + color: #374151; + height: 50px; + transition: all 0.3s; + cursor: pointer; +} + +.countryCodeSelect:focus { + outline: none; + border-color: #16a34a; + box-shadow: 0 0 0 2px rgba(22, 163, 74, 0.2); +} + +.phoneInput { + flex: 1; + height: 50px; + border-radius: 10px; + border: 1px solid #d1d5db; + padding: 0 16px; + font-size: 16px; + color: #374151; + transition: all 0.3s; +} + +.phoneInput:focus { + outline: none; + border-color: #16a34a; + box-shadow: 0 0 0 2px rgba(22, 163, 74, 0.2); +} + +.phoneHint { + margin-top: 6px; + font-size: 12px; + color: #6b7280; +} + @media (max-width: 768px) { .loginPage { padding: 16px; @@ -254,4 +330,5 @@ .circle3 { display: none; } -} \ No newline at end of file +} + diff --git a/src/feature/Login/Login.tsx b/src/feature/Login/Login.tsx index d4ac724..71b1fdf 100644 --- a/src/feature/Login/Login.tsx +++ b/src/feature/Login/Login.tsx @@ -1,16 +1,52 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate, Link } from 'react-router-dom'; import styles from './Login.module.css'; import { AppDispatch, RootState } from '../../redux/store'; -import { setEmail, setPassword, setToken } from '../../redux/slices/userSlice'; +import { + setEmail, + setPassword, + setToken, + setPhoneNumber, + setSelectedCode, + setLoginType, + setPasswordLogin +} from '../../redux/slices/userSlice'; import { loginUser } from '../../redux/thunks/userThunks'; import { IoArrowBack } from 'react-icons/io5'; +// Country codes for the dropdown +const countryCodes = [ + { code: '+90', country: 'Turkey' }, + { code: '+1', country: 'USA' }, + { code: '+44', country: 'UK' }, + { code: '+49', country: 'Germany' }, + { code: '+33', country: 'France' }, + { code: '+39', country: 'Italy' }, + { code: '+34', country: 'Spain' }, + { code: '+31', country: 'Netherlands' }, + { code: '+46', country: 'Sweden' }, + { code: '+47', country: 'Norway' }, + // Add more countries as needed +]; + const Login: React.FC = () => { const dispatch = useDispatch(); const navigate = useNavigate(); - const { email, password, loading, error, token, role } = useSelector((state: RootState) => state.user); + const { + email, + password, + loading, + error, + token, + role, + phoneNumber, + selectedCode, + login_type + } = useSelector((state: RootState) => state.user); + + // Local state for form control + const [showPhoneLogin, setShowPhoneLogin] = useState(login_type === 'phone_number'); useEffect(() => { if (token && role) { @@ -23,16 +59,54 @@ const Login: React.FC = () => { } }, [token, role, navigate]); + // Toggle between email and phone login + const toggleLoginMethod = () => { + setShowPhoneLogin(prev => { + const newValue = !prev; + dispatch(setLoginType(newValue ? 'phone_number' : 'email')); + return newValue; + }); + }; + const handleLogin = async () => { try { - const result = await dispatch( - loginUser({ + // Set password login mode + dispatch(setPasswordLogin(true)); + + let loginPayload; + + if (showPhoneLogin) { + // For phone number login + if (!phoneNumber) { + alert('Please enter a phone number'); + return; + } + + // Prepare phone number with country code + const fullPhoneNumber = `${selectedCode}${phoneNumber}`; + + loginPayload = { + phone_number: fullPhoneNumber, + password: password.trim(), + login_type: "phone_number", + password_login: true + }; + } else { + // For email login + if (!email) { + alert('Please enter an email'); + return; + } + + loginPayload = { email: email.trim(), password: password.trim(), login_type: "email", - password_login: true, - }) - ).unwrap(); + password_login: true + }; + } + + const result = await dispatch(loginUser(loginPayload)).unwrap(); if (result && result.token) { dispatch(setToken(result.token)); @@ -61,17 +135,60 @@ const Login: React.FC = () => {

Welcome Back

Sign in to your account

+
+ + +
+
-
- - dispatch(setEmail(e.target.value))} - placeholder="your@email.com" - /> -
+ {showPhoneLogin ? ( +
+ +
+ + dispatch(setPhoneNumber(e.target.value))} + placeholder="5079805011" + className={styles.phoneInput} + /> +
+

Enter your phone number without the country code

+
+ ) : ( +
+ + dispatch(setEmail(e.target.value))} + placeholder="your@email.com" + /> +
+ )}
@@ -108,4 +225,5 @@ const Login: React.FC = () => { ); }; -export default Login; \ No newline at end of file +export default Login; + diff --git a/src/feature/Partnership/components/addBusinessModel/addBusinessModel.module.css b/src/feature/Partnership/components/addBusinessModel/addBusinessModel.module.css index 8edf00f..0cf3840 100644 --- a/src/feature/Partnership/components/addBusinessModel/addBusinessModel.module.css +++ b/src/feature/Partnership/components/addBusinessModel/addBusinessModel.module.css @@ -503,6 +503,13 @@ button.completeButton { } } +@media (min-width: 2000px) { + .outerDiv { + max-width: 1800px; + } + +} + diff --git a/src/feature/Partnership/components/addBusinessModel/addBusinessModel.tsx b/src/feature/Partnership/components/addBusinessModel/addBusinessModel.tsx index 9f01ec3..182fd68 100644 --- a/src/feature/Partnership/components/addBusinessModel/addBusinessModel.tsx +++ b/src/feature/Partnership/components/addBusinessModel/addBusinessModel.tsx @@ -46,6 +46,29 @@ const AddBusinessModel: React.FC = ({ isEditing = false, restaurant, }) => { + // Extract phone number components if editing + const parsePhoneNumber = () => { + if (!isEditing || !restaurant?.restaurantPhone) return { areaCode: "+90", number: "" }; + + // Common area codes to check + const areaCodes = ["+90", "+1", "+44"]; + let phoneAreaCode = "+90"; + let phoneNumber = restaurant.restaurantPhone; + + // Check if the phone number starts with any of the area codes + for (const code of areaCodes) { + if (restaurant.restaurantPhone.startsWith(code)) { + phoneAreaCode = code; + phoneNumber = restaurant.restaurantPhone.substring(code.length); + break; + } + } + + return { areaCode: phoneAreaCode, number: phoneNumber }; + }; + + const { areaCode, number } = parsePhoneNumber(); + const [formData, setFormData] = useState({ restaurantName: isEditing ? restaurant?.restaurantName || "" : "", restaurantDescription: isEditing ? restaurant?.restaurantDescription || "" : "", @@ -56,7 +79,7 @@ const AddBusinessModel: React.FC = ({ workingHoursStart: isEditing ? restaurant?.workingHoursStart || "" : "", workingHoursEnd: isEditing ? restaurant?.workingHoursEnd || "" : "", restaurantEmail: isEditing ? restaurant?.restaurantEmail || "" : "", - restaurantPhone: isEditing ? restaurant?.restaurantPhone || "" : "", + restaurantPhone: number, // Use extracted number without area code pickup: isEditing ? restaurant?.pickup || false : false, delivery: isEditing ? restaurant?.delivery || false : false, maxDeliveryDistance: @@ -75,12 +98,15 @@ const AddBusinessModel: React.FC = ({ const [currentStep, setCurrentStep] = useState(1); const [phoneModalOpen, setPhoneModalOpen] = useState(false); const [categoryModalOpen, setCategoryModalOpen] = useState(false); - const [selectedAreaCode, setSelectedAreaCode] = useState("+90"); + const [selectedAreaCode, setSelectedAreaCode] = useState(areaCode); // Use extracted area code const [selectedCategory, setSelectedCategory] = useState( isEditing ? restaurant?.category || "" : "" ); const [daysModalOpen, setDaysModalOpen] = useState(false); const [uploadedFile, setUploadedFile] = useState(null); + const [existingImageUrl, setExistingImageUrl] = useState( + isEditing && restaurant?.image_url ? restaurant.image_url : null + ); const [error, setError] = useState(null); const navigate = useNavigate(); @@ -236,6 +262,7 @@ const AddBusinessModel: React.FC = ({ return; } setUploadedFile(file); + setExistingImageUrl(null); // Clear existing image when new one is uploaded setInvalidFields((f) => f.filter((x) => x !== "image")); setError(null); } @@ -272,7 +299,9 @@ const AddBusinessModel: React.FC = ({ }); if (!formData.workingDays.length) invalid.push("workingDays"); - if (!uploadedFile) invalid.push("image"); + + // Only require image upload if there's no existing image and no new upload + if (!uploadedFile && !existingImageUrl) invalid.push("image"); if (formData.workingHoursStart && !validateTimeFormat(formData.workingHoursStart)) { invalid.push("workingHoursStart"); @@ -311,6 +340,7 @@ const AddBusinessModel: React.FC = ({ setInvalidFields(invalid); return !invalid.length; }; + const handleComplete = async () => { if (!validateStep2()) { setError("Please fill in all required fields"); @@ -369,6 +399,15 @@ const AddBusinessModel: React.FC = ({ } } }; + + // Helper function to get a shortened version of the image URL for display + const getShortImageName = (url: string) => { + if (!url) return ""; + const urlParts = url.split('/'); + const fileName = urlParts[urlParts.length - 1]; + return fileName.length > 20 ? fileName.substring(0, 17) + '...' : fileName; + }; + return (
@@ -621,16 +660,22 @@ const AddBusinessModel: React.FC = ({ : "" }`} > - {!uploadedFile ? ( - - Add your restaurant image - - ) : ( + {uploadedFile ? (
{uploadedFile.name}
+ ) : existingImageUrl ? ( +
+ + Current image: {getShortImageName(existingImageUrl)} + +
+ ) : ( + + Add your restaurant image + )}
diff --git a/src/feature/RestaurantDetails/components/NotificationTest/NotificationTest.module.css b/src/feature/RestaurantDetails/components/NotificationTest/NotificationTest.module.css deleted file mode 100644 index a81e7b9..0000000 --- a/src/feature/RestaurantDetails/components/NotificationTest/NotificationTest.module.css +++ /dev/null @@ -1,57 +0,0 @@ -.container { - background: #ffffff; - border-radius: 8px; - padding: 1.5rem; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - margin: 1rem; -} - -.statusContainer { - margin: 1rem 0; - padding: 1rem; - background: #f5f5f5; - border-radius: 4px; -} - -.actions { - display: flex; - gap: 1rem; - margin-top: 1rem; -} - -.button { - padding: 0.75rem 1.5rem; - border: none; - border-radius: 4px; - background: #007bff; - color: white; - cursor: pointer; - font-weight: 500; - transition: background-color 0.2s; -} - -.button:hover { - background: #0056b3; -} - -.button:disabled { - background: #cccccc; - cursor: not-allowed; -} - -.testButton { - background: #28a745; -} - -.testButton:hover { - background: #218838; -} - -.error { - color: #dc3545; - padding: 0.75rem; - margin: 1rem 0; - background: #f8d7da; - border-radius: 4px; - border: 1px solid #f5c6cb; -} \ No newline at end of file diff --git a/src/feature/RestaurantDetails/components/Orders/Orders.module.css b/src/feature/RestaurantDetails/components/Orders/Orders.module.css index 3d49683..70646c2 100644 --- a/src/feature/RestaurantDetails/components/Orders/Orders.module.css +++ b/src/feature/RestaurantDetails/components/Orders/Orders.module.css @@ -1,12 +1,14 @@ .purchasesContainer { background: white; border-radius: 16px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.02), 0 1px 3px rgba(0, 0, 0, 0.03); padding: 24px; height: 100%; display: flex; flex-direction: column; gap: 24px; + border: 1px solid rgba(0, 0, 0, 0.05); + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; } .purchasesHeader { @@ -14,18 +16,24 @@ justify-content: space-between; align-items: center; margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); } .purchasesHeader h2 { - color: #1f2937; - font-size: 1.5rem; - font-weight: 600; + color: #111827; + font-size: 28px; + font-weight: 700; margin: 0; + letter-spacing: -0.025em; } .loadingIndicator { color: #6b7280; font-size: 0.875rem; + display: flex; + align-items: center; + gap: 8px; } .statusGrid { @@ -40,11 +48,18 @@ .statusSection { background: white; border-radius: 12px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.02), 0 1px 3px rgba(0, 0, 0, 0.03); display: flex; flex-direction: column; height: fit-content; max-height: 100%; + border: 1px solid rgba(0, 0, 0, 0.05); + transition: all 0.2s ease; +} + +.statusSection:hover { + transform: translateY(-2px); + box-shadow: 0 10px 15px rgba(0, 0, 0, 0.04), 0 2px 6px rgba(0, 0, 0, 0.02); } .statusHeader { @@ -63,7 +78,7 @@ } .statusTitle { - color: #1f2937; + color: #111827; font-size: 1.1rem; font-weight: 600; margin: 0; @@ -75,7 +90,10 @@ .orderCount { font-size: 0.875rem; color: #6b7280; - font-weight: normal; + font-weight: 500; + padding: 4px 8px; + background-color: #F9FAFB; + border-radius: 20px; } .expandButton { @@ -86,11 +104,15 @@ padding: 8px; border-radius: 8px; transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; } .expandButton:hover { background: rgba(0, 0, 0, 0.05); - color: #1f2937; + color: #111827; + transform: translateY(-1px); } .expandIcon { @@ -110,13 +132,14 @@ background: white; border-radius: 12px; padding: 16px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.02), 0 1px 3px rgba(0, 0, 0, 0.03); transition: all 0.2s ease; + border: 1px solid rgba(0, 0, 0, 0.05); } .purchaseItem:hover { transform: translateY(-2px); - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + box-shadow: 0 10px 15px rgba(0, 0, 0, 0.04), 0 2px 6px rgba(0, 0, 0, 0.02); } .purchaseHeader { @@ -130,34 +153,36 @@ .orderId { font-weight: 600; - color: #1f2937; + color: #111827; + font-size: 15px; } .status { - padding: 4px 12px; - border-radius: 999px; - font-size: 0.875rem; - font-weight: 500; + padding: 6px 12px; + border-radius: 20px; + font-size: 13px; + font-weight: 600; + letter-spacing: -0.01em; } .pending .status { - background: #fef3c7; - color: #92400e; + background: #FEF3C7; + color: #92400E; } .accepted .status { - background: #dbeafe; - color: #1e40af; + background: #DBEAFE; + color: #1E40AF; } .completed .status { - background: #d1fae5; - color: #047857; + background: #D1FAE5; + color: #065F46; } .rejected .status { - background: #fee2e2; - color: #b91c1c; + background: #FEE2E2; + color: #B91C1C; } .date { @@ -173,32 +198,36 @@ } .itemTitle { - color: #1f2937; + color: #111827; font-weight: 500; + font-size: 15px; } .totalPrice { color: #059669; font-weight: 600; + font-size: 15px; } .deliveryInfo { - background: #f9fafb; + background: #F9FAFB; padding: 12px; - border-radius: 8px; + border-radius: 10px; margin-top: 12px; + border: 1px solid rgba(0, 0, 0, 0.05); } .deliveryInfo strong { display: block; margin-bottom: 4px; - color: #1f2937; + color: #111827; + font-weight: 600; } .deliveryInfo p { margin: 0 0 8px; - color: #4b5563; - font-size: 0.875rem; + color: #4B5563; + font-size: 14px; } .purchaseActions { @@ -208,31 +237,48 @@ } .acceptButton, .rejectButton { - padding: 8px 16px; - border-radius: 8px; - font-weight: 500; + padding: 10px 18px; + border-radius: 10px; + font-weight: 600; + font-size: 14px; cursor: pointer; - transition: all 0.2s ease; + transition: all 0.15s ease; border: none; flex: 1; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); } .acceptButton { - background: #d1fae5; - color: #047857; + background: #b0f484; + color: #111827; } .acceptButton:hover:not(:disabled) { - background: #a7f3d0; + background: #86efac; + transform: translateY(-1px); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); +} + +.acceptButton:active:not(:disabled) { + transform: translateY(0); } .rejectButton { - background: #fee2e2; - color: #b91c1c; + background: #FEE2E2; + color: #B91C1C; } .rejectButton:hover:not(:disabled) { background: #fecaca; + transform: translateY(-1px); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); +} + +.rejectButton:active:not(:disabled) { + transform: translateY(0); } .imageUploadSection { @@ -250,24 +296,33 @@ } .fileInputLabel { - padding: 8px 16px; - background: #f3f4f6; - border-radius: 8px; + padding: 10px 18px; + background: #F9FAFB; + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 10px; cursor: pointer; text-align: center; - color: #4b5563; - transition: all 0.2s ease; + color: #4B5563; + transition: all 0.15s ease; + font-weight: 500; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02); + font-size: 14px; } .fileInputLabel:hover { - background: #e5e7eb; + background: #F3F4F6; + border-color: rgba(0, 0, 0, 0.12); + transform: translateY(-1px); + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.04); } .imagePreview { width: 100%; - max-height: 200px; + height: 200px; overflow: hidden; - border-radius: 8px; + border-radius: 10px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + border: 1px solid rgba(0, 0, 0, 0.05); } .previewImage { @@ -282,37 +337,56 @@ } .uploadButton, .cancelButton, .addImageButton { - padding: 8px 16px; - border-radius: 8px; - font-weight: 500; + padding: 10px 18px; + border-radius: 10px; + font-weight: 600; + font-size: 14px; cursor: pointer; - transition: all 0.2s ease; + transition: all 0.15s ease; border: none; flex: 1; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); } .uploadButton { - background: #d1fae5; - color: #047857; + background: #b0f484; + color: #111827; } .uploadButton:hover:not(:disabled) { - background: #a7f3d0; + background: #86efac; + transform: translateY(-1px); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); +} + +.uploadButton:active:not(:disabled) { + transform: translateY(0); } .cancelButton, .addImageButton { - background: #f3f4f6; - color: #4b5563; + background: #F3F4F6; + color: #4B5563; } .cancelButton:hover, .addImageButton:hover { - background: #e5e7eb; + background: #E5E7EB; + transform: translateY(-1px); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); +} + +.cancelButton:active, .addImageButton:active { + transform: translateY(0); } .imageContainer { margin-top: 16px; - border-radius: 8px; + border-radius: 10px; overflow: hidden; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.02), 0 1px 3px rgba(0, 0, 0, 0.03); + border: 1px solid rgba(0, 0, 0, 0.05); } .completionImage { @@ -322,22 +396,25 @@ } .emptyState { - color: #6b7280; + color: #6B7280; text-align: center; padding: 24px; - font-size: 0.875rem; + font-size: 14px; + font-weight: 500; } .errorContainer { - background: #fee2e2; - border-radius: 8px; - padding: 12px; + background: #FEE2E2; + border-radius: 10px; + padding: 16px; + border: 1px solid rgba(185, 28, 28, 0.1); } .errorMessage { - color: #b91c1c; + color: #B91C1C; margin: 0; - font-size: 0.875rem; + font-size: 14px; + font-weight: 500; } .modalOverlay { @@ -362,6 +439,8 @@ max-width: 800px; max-height: 90vh; overflow-y: auto; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); + border: 1px solid rgba(0, 0, 0, 0.05); } .modalHeader { @@ -369,6 +448,16 @@ justify-content: space-between; align-items: center; margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.modalHeader h3 { + margin: 0; + font-size: 20px; + font-weight: 700; + color: #111827; + letter-spacing: -0.025em; } .modalClose { @@ -380,11 +469,14 @@ padding: 4px 8px; border-radius: 8px; transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; } .modalClose:hover { - background: #f3f4f6; - color: #1f2937; + background: #F3F4F6; + color: #111827; } .modalList { @@ -395,9 +487,29 @@ .noOrders { text-align: center; - color: #6b7280; + color: #6B7280; padding: 48px; - font-size: 1rem; + font-size: 16px; + font-weight: 500; + background-color: white; + border-radius: 12px; + border: 1px solid rgba(0, 0, 0, 0.05); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.02), 0 1px 3px rgba(0, 0, 0, 0.03); +} + +/* Loading spinner */ +.loadingSpinner { + border: 4px solid rgba(0, 0, 0, 0.05); + border-radius: 50%; + border-top: 4px solid #b0f484; + width: 24px; + height: 24px; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } } @media (max-width: 768px) { diff --git a/src/feature/RestaurantDetails/components/Orders/Orders.tsx b/src/feature/RestaurantDetails/components/Orders/Orders.tsx index 5521153..d10ae9c 100644 --- a/src/feature/RestaurantDetails/components/Orders/Orders.tsx +++ b/src/feature/RestaurantDetails/components/Orders/Orders.tsx @@ -9,6 +9,7 @@ import { } from '../../../../redux/thunks/purchaseThunks.ts'; import { AppDispatch, RootState } from "../../../../redux/store.ts"; import { Purchase } from "../../../../redux/slices/purchaseSlice.ts"; +import {IoCheckmark} from "react-icons/io5"; interface ModalProps { isOpen: boolean; @@ -268,7 +269,14 @@ const Orders: React.FC = ({ restaurantId }) => { onClick={() => handleAcceptOrder(purchase.purchase_id)} disabled={acceptingOrder === purchase.purchase_id} > - {acceptingOrder === purchase.purchase_id ? 'Accepting...' : 'Accept Order'} + {acceptingOrder === purchase.purchase_id ? + <> +
Accepting... + : + <> + Accept Order + + } +
{groupPurchasesByStatus(purchases).pending.map(purchase => renderPurchaseItem(purchase) diff --git a/src/feature/RestaurantDetails/components/addListingModel/ListingModel.module.css b/src/feature/RestaurantDetails/components/addListingModel/ListingModel.module.css index 96b54ae..cf2c427 100644 --- a/src/feature/RestaurantDetails/components/addListingModel/ListingModel.module.css +++ b/src/feature/RestaurantDetails/components/addListingModel/ListingModel.module.css @@ -337,4 +337,52 @@ transform: translateX(-4px); background: rgba(255, 255, 255, 1) !important; box-shadow: 0 4px 12px rgba(74, 222, 128, 0.2); +} + +/* Add these styles to your existing ListingModel.module.css file */ + +.uploadContainer { + display: flex; + flex-direction: column; + width: 100%; +} + +.imagePreviewContainer { + display: flex; + margin-top: 10px; + justify-content: center; +} + +.imagePreview, .currentImage { + position: relative; + width: 180px; + height: 180px; + border-radius: 8px; + overflow: hidden; + margin-right: 10px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.thumbnailPreview { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 6px; +} + +/* Improve the file upload appearance */ +.fileLabel { + cursor: pointer; + padding: 12px 16px; + background-color: #f5f5f5; + border-radius: 4px; + display: inline-block; + text-align: center; + border: 1px dashed #ccc; + transition: all 0.3s; +} + +.fileLabel:hover { + background-color: #ebebeb; + border-color: #aaa; } \ No newline at end of file diff --git a/src/feature/RestaurantDetails/components/addListingModel/ListingModel.tsx b/src/feature/RestaurantDetails/components/addListingModel/ListingModel.tsx index e20a84e..718642e 100644 --- a/src/feature/RestaurantDetails/components/addListingModel/ListingModel.tsx +++ b/src/feature/RestaurantDetails/components/addListingModel/ListingModel.tsx @@ -45,6 +45,7 @@ const ListingModel: React.FC = ({ }) => { const dispatch = useDispatch(); const [uploadedFile, setUploadedFile] = useState(null); + const [imagePreview, setImagePreview] = useState(null); const [invalidFields, setInvalidFields] = useState([]); const [formData, setFormData] = useState(initialFormData); const [isExiting, setIsExiting] = useState(false); @@ -116,11 +117,25 @@ const ListingModel: React.FC = ({ return; } setUploadedFile(file); + + // Create image preview URL + const previewUrl = URL.createObjectURL(file); + setImagePreview(previewUrl); + setInvalidFields((prev) => prev.filter((f) => f !== "image")); setError(null); } }; + // Clean up object URL when component unmounts or when file changes + useEffect(() => { + return () => { + if (imagePreview) { + URL.revokeObjectURL(imagePreview); + } + }; + }, [imagePreview]); + const validateForm = (): boolean => { const requiredFields = [ "title", @@ -270,7 +285,7 @@ const ListingModel: React.FC = ({