diff --git a/Frontend/src/_components/Dropdown.tsx b/Frontend/src/_components/Dropdown.tsx index b8848ba8..c368acdb 100644 --- a/Frontend/src/_components/Dropdown.tsx +++ b/Frontend/src/_components/Dropdown.tsx @@ -17,9 +17,9 @@ const styledVariant: Interpolation<{ $variant: Variant }> = ({ $variant }) => box-shadow: 0 2px 12px 0 rgb(0 0 0 / 6%); `, ink: css` - background-color: #23272a; - color: #fff; - border: 1px solid #23272a; + background-color: #ffffffff; + color: #23272a; + border: 1px solid #eee; box-shadow: 0 2px 12px 0 rgb(0 0 0 / 20%); `, })[$variant] diff --git a/Frontend/src/_components/Header/Menu/compare/CompareMenu.tsx b/Frontend/src/_components/Header/Menu/compare/CompareMenu.tsx index 41bb64e2..f79b3c62 100644 --- a/Frontend/src/_components/Header/Menu/compare/CompareMenu.tsx +++ b/Frontend/src/_components/Header/Menu/compare/CompareMenu.tsx @@ -1,6 +1,8 @@ import type { FC } from 'react' -import { useState } from 'react' +import { useState, useEffect } from 'react' import styled from 'styled-components' +import { usePinContext } from '@/_components/ContextHooks/usePinContext' +import { LatLng } from 'leaflet' const AccordionItem = styled.div<{ isExpanded?: boolean }>` background-color: white; @@ -81,7 +83,6 @@ const ToggleButton = styled.button<{ disabled?: boolean }>` margin-top: 8px; opacity: ${({ disabled }) => (disabled ? 0.5 : 1)}; pointer-events: ${({ disabled }) => (disabled ? 'none' : 'auto')}; - &:hover { background-color: #005f99; } @@ -103,7 +104,6 @@ const ResultItem = styled.div<{ selected?: boolean }>` border-radius: 4px; cursor: pointer; background-color: ${({ selected }) => (selected ? '#c2e9ff' : 'transparent')}; - &:hover { background-color: #d0eaff; } @@ -118,16 +118,19 @@ interface CompareMenuProps { closeLoc2: () => void } -const australianStates = [ - 'New South Wales', - 'Victoria', - 'Queensland', - 'South Australia', - 'Western Australia', - 'Tasmania', - 'Northern Territory', - 'Australian Capital Territory', -] +// Dummy coordinates for Australian states +const stateCoordinates: Record = { + 'New South Wales': new LatLng(-33.8688, 151.2093), + Victoria: new LatLng(-37.8136, 144.9631), + Queensland: new LatLng(-27.4698, 153.0251), + 'South Australia': new LatLng(-34.9285, 138.6007), + 'Western Australia': new LatLng(-31.9505, 115.8605), + Tasmania: new LatLng(-42.8821, 147.3272), + 'Northern Territory': new LatLng(-12.4634, 130.8456), + 'Australian Capital Territory': new LatLng(-35.2809, 149.13), +} + +const australianStates = Object.keys(stateCoordinates) const CompareMenu: FC = ({ isLoc1Open, @@ -137,6 +140,8 @@ const CompareMenu: FC = ({ closeLoc1, closeLoc2, }) => { + const { locationOnePin, locationTwoPin, addPin, removePin } = usePinContext() + const [searchInput1, setSearchInput1] = useState('') const [searchInput2, setSearchInput2] = useState('') const [results1, setResults1] = useState([]) @@ -147,54 +152,75 @@ const CompareMenu: FC = ({ const [pendingSelection2, setPendingSelection2] = useState( null, ) - const [selected1, setSelected1] = useState(null) - const [selected2, setSelected2] = useState(null) + const [locked1, setLocked1] = useState(false) + const [locked2, setLocked2] = useState(false) - const handleSearch1 = (value: string) => { - setSearchInput1(value) - setResults1( - australianStates.filter((state) => - state.toLowerCase().includes(value.toLowerCase()), - ), - ) - setPendingSelection1(null) - } + useEffect(() => { + if (locationOnePin) { + setSearchInput1(locationOnePin.locationName) + setLocked1(true) + setPendingSelection1(null) + setResults1([]) + } else { + setSearchInput1('') + setLocked1(false) + } + }, [locationOnePin]) - const handleSearch2 = (value: string) => { - setSearchInput2(value) - setResults2( + useEffect(() => { + if (locationTwoPin) { + setSearchInput2(locationTwoPin.locationName) + setLocked2(true) + setPendingSelection2(null) + setResults2([]) + } else { + setSearchInput2('') + setLocked2(false) + } + }, [locationTwoPin]) + + const handleSearch = ( + value: string, + setInput: React.Dispatch>, + setResults: React.Dispatch>, + locked: boolean, + setPending: React.Dispatch>, + ) => { + if (locked) return + setInput(value) + setResults( australianStates.filter((state) => state.toLowerCase().includes(value.toLowerCase()), ), ) - setPendingSelection2(null) + setPending(null) } - const handleClickResult1 = (loc: string) => setPendingSelection1(loc) - const handleClickResult2 = (loc: string) => setPendingSelection2(loc) - - const toggleSelection1 = () => { - if (pendingSelection1) { - setSelected1(pendingSelection1) - setSearchInput1(pendingSelection1) - setResults1([]) - setPendingSelection1(null) - } else { - setSelected1(null) - setSearchInput1('') - } + const handleClickResult = ( + loc: string, + setPending: React.Dispatch>, + ) => { + setPending(loc) } - const toggleSelection2 = () => { - if (pendingSelection2) { - setSelected2(pendingSelection2) - setSearchInput2(pendingSelection2) - setResults2([]) - setPendingSelection2(null) - } else { - setSelected2(null) - setSearchInput2('') + const toggleSelection = async ( + pendingSelection: string | null, + locked: boolean, + setLocked: React.Dispatch>, + locationPin: typeof locationOnePin | typeof locationTwoPin, + setInput: React.Dispatch>, + setResults: React.Dispatch>, + ) => { + if (!locked && pendingSelection) { + const coords = stateCoordinates[pendingSelection] + if (!coords) return + await addPin(coords) + } else if (locked && locationPin) { + removePin(locationPin.id) + setInput('') } + setLocked(!locked) + setResults([]) } return ( @@ -214,14 +240,23 @@ const CompareMenu: FC = ({ handleSearch1(e.target.value)} + onChange={(e) => + handleSearch( + e.target.value, + setSearchInput1, + setResults1, + locked1, + setPendingSelection1, + ) + } + disabled={locked1} /> {results1.length > 0 && ( {results1.map((item, idx) => ( handleClickResult1(item)} + onClick={() => handleClickResult(item, setPendingSelection1)} selected={pendingSelection1 === item} > {item} @@ -230,10 +265,19 @@ const CompareMenu: FC = ({ )} + toggleSelection( + pendingSelection1, + locked1, + setLocked1, + locationOnePin, + setSearchInput1, + setResults1, + ) + } > - {selected1 ? 'Deselect' : 'Select'} + {locked1 ? 'Deselect' : 'Select'} @@ -253,14 +297,23 @@ const CompareMenu: FC = ({ handleSearch2(e.target.value)} + onChange={(e) => + handleSearch( + e.target.value, + setSearchInput2, + setResults2, + locked2, + setPendingSelection2, + ) + } + disabled={locked2} /> {results2.length > 0 && ( {results2.map((item, idx) => ( handleClickResult2(item)} + onClick={() => handleClickResult(item, setPendingSelection2)} selected={pendingSelection2 === item} > {item} @@ -269,10 +322,19 @@ const CompareMenu: FC = ({ )} + toggleSelection( + pendingSelection2, + locked2, + setLocked2, + locationTwoPin, + setSearchInput2, + setResults2, + ) + } > - {selected2 ? 'Deselect' : 'Select'} + {locked2 ? 'Deselect' : 'Select'} diff --git a/Frontend/src/_components/Sidebar/ConfirmModal.tsx b/Frontend/src/_components/Sidebar/ConfirmModal.tsx new file mode 100644 index 00000000..d647e4dd --- /dev/null +++ b/Frontend/src/_components/Sidebar/ConfirmModal.tsx @@ -0,0 +1,67 @@ +import type { FC } from 'react' +import { Modal } from './Modal' +import styled from 'styled-components' + +interface ConfirmModalProps { + isOpen: boolean + message: string + onConfirm: () => void + onCancel: () => void + confirmText?: string + cancelText?: string +} + +const ModalButtons = styled.div` + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 16px; +` + +const Button = styled.button<{ $variant?: 'primary' | 'secondary' }>` + padding: 6px 16px; + border-radius: 6px; + border: 1px solid + ${({ $variant }) => ($variant === 'primary' ? '#a0d1ff' : '#d1d5db')}; + background: ${({ $variant }) => + $variant === 'primary' ? '#b3e0ff' : 'white'}; + color: ${({ $variant }) => ($variant === 'primary' ? '#1d3c66' : '#333')}; + cursor: pointer; + font-family: 'Instrument Sans', sans-serif; + font-size: 14px; + + &:hover { + background: ${({ $variant }) => + $variant === 'primary' ? '#99ccff' : '#f3f4f6'}; + } +` + +const Message = styled.p` + margin: 0; + color: #1d3c66; +` + +export const ConfirmModal: FC = ({ + isOpen, + message, + onConfirm, + onCancel, + confirmText = 'Yes', + cancelText = 'Cancel', +}) => { + return ( + + {message} + + + + + + ) +} + +export default ConfirmModal diff --git a/Frontend/src/_components/Sidebar/DeleteModal.tsx b/Frontend/src/_components/Sidebar/DeleteModal.tsx new file mode 100644 index 00000000..973a8b08 --- /dev/null +++ b/Frontend/src/_components/Sidebar/DeleteModal.tsx @@ -0,0 +1,63 @@ +import type { FC } from 'react' +import { Modal } from './Modal' +import styled from 'styled-components' + +interface DeleteModalProps { + isOpen: boolean + message: string + onConfirm: () => void + onCancel: () => void +} + +const ModalButtons = styled.div` + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 16px; +` + +const Button = styled.button<{ $variant?: 'primary' | 'secondary' }>` + padding: 6px 16px; + border-radius: 6px; + border: 1px solid + ${({ $variant }) => ($variant === 'primary' ? '#a0d1ff' : '#d1d5db')}; + background: ${({ $variant }) => + $variant === 'primary' ? '#b3e0ff' : 'white'}; + color: ${({ $variant }) => ($variant === 'primary' ? '#1d3c66' : '#333')}; + cursor: pointer; + font-family: 'Instrument Sans', sans-serif; + font-size: 14px; + + &:hover { + background: ${({ $variant }) => + $variant === 'primary' ? '#99ccff' : '#f3f4f6'}; + } +` + +const Message = styled.p` + margin: 0; + color: #1d3c66; +` + +const DeleteModal: FC = ({ + isOpen, + message, + onConfirm, + onCancel, +}) => { + return ( + + {message} + + + + + + ) +} + +export default DeleteModal diff --git a/Frontend/src/_components/Sidebar/EditTabModal.tsx b/Frontend/src/_components/Sidebar/EditTabModal.tsx index 8f6dd9d1..5564c6ec 100644 --- a/Frontend/src/_components/Sidebar/EditTabModal.tsx +++ b/Frontend/src/_components/Sidebar/EditTabModal.tsx @@ -1,7 +1,8 @@ -import type { FC } from 'react' -import { useState, useEffect } from 'react' -import styled from 'styled-components' +import { useState, useEffect, type FC } from 'react' import type { TabData } from '@/_components/ContextHooks/TabsContext' +import { Modal } from './Modal' +import { ModalButton } from './ModalButton' +import styled from 'styled-components' interface EditTabModalProps { tab: TabData @@ -9,139 +10,52 @@ interface EditTabModalProps { onCancel: () => void } -const ModalOverlay = styled.div` - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 10000; -` - -const ModalContent = styled.div` - background: white; - border-radius: 12px; - padding: 24px; - width: 400px; - max-width: 90vw; - box-shadow: - 0 20px 25px -5px rgba(0, 0, 0, 0.1), - 0 10px 10px -5px rgba(0, 0, 0, 0.04); -` - -const ModalHeader = styled.div` - margin-bottom: 20px; -` +const MAX_NAME_LENGTH = 30 -const ModalTitle = styled.h3` - font-family: 'Instrument Sans', sans-serif; - font-size: 18px; +const ModalHeader = styled.h3` + margin: 0 0 16px 0; + font-size: 16px; font-weight: 600; - color: #1f2937; - margin: 0 0 8px 0; + color: #1d3c66; ` -const ModalDescription = styled.p` - font-family: 'Instrument Sans', sans-serif; - font-size: 14px; - color: #6b7280; - margin: 0; -` - -const ModalBody = styled.div` - margin-bottom: 24px; -` - -const Label = styled.label` - display: block; - font-family: 'Instrument Sans', sans-serif; - font-size: 14px; - font-weight: 500; - color: #374151; - margin-bottom: 8px; -` - -const Input = styled.input` +const ModalInput = styled.input<{ $isInvalid: boolean }>` width: 100%; - height: 40px; padding: 8px 12px; - border: 1px solid #d1d5db; border-radius: 6px; - font-family: 'Instrument Sans', sans-serif; + border: 1px solid ${({ $isInvalid }) => ($isInvalid ? '#f87171' : '#d1d5db')}; font-size: 14px; - color: #1f2937; - transition: border-color 0.2s ease; - - &:focus { - outline: none; - border-color: #007acc; - box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.2); - } - - &::placeholder { - color: #9ca3af; - } -` - -const ModalFooter = styled.div` - display: flex; - gap: 12px; - justify-content: flex-end; -` - -const Button = styled.button<{ $variant?: 'primary' | 'secondary' }>` - height: 36px; - padding: 0 16px; - border-radius: 6px; font-family: 'Instrument Sans', sans-serif; - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - border: 1px solid - ${({ $variant }) => ($variant === 'primary' ? '#007acc' : '#d1d5db')}; - background: ${({ $variant }) => - $variant === 'primary' ? '#007acc' : 'white'}; - color: ${({ $variant }) => ($variant === 'primary' ? 'white' : '#374151')}; - - &:hover { - background: ${({ $variant }) => - $variant === 'primary' ? '#005f99' : '#f3f4f6'}; - border-color: ${({ $variant }) => - $variant === 'primary' ? '#005f99' : '#9ca3af'}; - } - - &:active { - transform: scale(0.98); - } + margin-bottom: 4px; &:focus { outline: none; - box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.2); - } - - &:disabled { - opacity: 0.5; - cursor: not-allowed; - transform: none; + border-color: ${({ $isInvalid }) => ($isInvalid ? '#f87171' : '#99ccff')}; + box-shadow: 0 0 0 2px + ${({ $isInvalid }) => + $isInvalid ? 'rgba(248, 113, 113, 0.3)' : 'rgba(153, 204, 255, 0.3)'}; } ` -const CharCount = styled.div<{ $isOverLimit: boolean }>` - font-family: 'Instrument Sans', sans-serif; +const CharCount = styled.div<{ $isInvalid: boolean }>` font-size: 12px; - color: ${({ $isOverLimit }) => ($isOverLimit ? '#ef4444' : '#6b7280')}; text-align: right; - margin-top: 4px; + margin-bottom: 16px; + font-family: 'Instrument Sans', sans-serif; + color: ${({ $isInvalid }) => ($isInvalid ? '#f87171' : '#6b7280')}; ` -const MAX_NAME_LENGTH = 50 +const ModalButtons = styled.div` + display: flex; + justify-content: flex-end; + gap: 12px; +` -const EditTabModal: FC = ({ tab, onSave, onCancel }) => { +export const EditTabModal: FC = ({ + tab, + onSave, + onCancel, +}) => { const [name, setName] = useState(tab.name) const [isValid, setIsValid] = useState(true) @@ -151,64 +65,33 @@ const EditTabModal: FC = ({ tab, onSave, onCancel }) => { const handleSubmit = (e: React.FormEvent) => { e.preventDefault() - if (isValid) { - onSave(name.trim()) - } + if (isValid) onSave(name.trim()) } - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - onCancel() - } - } - - const handleOverlayClick = (e: React.MouseEvent) => { - if (e.target === e.currentTarget) { - onCancel() - } - } - - const isOverLimit = name.length > MAX_NAME_LENGTH + const isInvalid = name.length > MAX_NAME_LENGTH || name.trim().length === 0 return ( - - - - Edit Tab - - Change the name of your tab to better organize your maps. - - - -
- - - setName(e.target.value)} - placeholder="Enter tab name..." - maxLength={MAX_NAME_LENGTH + 10} // Allow typing past limit for visual feedback - autoFocus - /> - - {name.length}/{MAX_NAME_LENGTH} - - - - - - - -
-
-
+ +
+ Edit Tab + setName(e.target.value)} + autoFocus + $isInvalid={isInvalid} + /> + + {name.length}/{MAX_NAME_LENGTH} + + + + Cancel + + + Save + + + +
) } - -export default EditTabModal diff --git a/Frontend/src/_components/Sidebar/Modal.tsx b/Frontend/src/_components/Sidebar/Modal.tsx new file mode 100644 index 00000000..16b63c4c --- /dev/null +++ b/Frontend/src/_components/Sidebar/Modal.tsx @@ -0,0 +1,81 @@ +import { useState, useEffect, type FC, type ReactNode } from 'react' +import { createPortal } from 'react-dom' +import styled, { keyframes } from 'styled-components' + +interface ModalProps { + children: ReactNode + onClose: () => void + isOpen: boolean +} + +const fadeIn = keyframes` + from { opacity: 0; } + to { opacity: 1; } +` + +const fadeOut = keyframes` + from { opacity: 1; } + to { opacity: 0; } +` + +const bounceIn = keyframes` + 0% { transform: scale(0.8); opacity: 0; } + 60% { transform: scale(1.05); opacity: 1; } + 100% { transform: scale(1); opacity: 1; } +` + +const bounceOut = keyframes` + 0% { transform: scale(1); opacity: 1; } + 100% { transform: scale(0.8); opacity: 0; } +` + +const Overlay = styled.div<{ $isClosing: boolean }>` + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 99999; + animation: ${({ $isClosing }) => ($isClosing ? fadeOut : fadeIn)} 0.2s ease + forwards; +` + +const Content = styled.div<{ $isClosing: boolean }>` + background: white; + border-radius: 12px; + padding: 24px; + min-width: 300px; + max-width: 90vw; + font-family: 'Instrument Sans', sans-serif; + animation: ${({ $isClosing }) => ($isClosing ? bounceOut : bounceIn)} 0.25s + ease forwards; +` + +export const Modal: FC = ({ children, onClose, isOpen }) => { + const [isClosing, setIsClosing] = useState(false) + + const handleClose = () => setIsClosing(true) + + useEffect(() => { + if (!isClosing) return + const timer = setTimeout(() => { + setIsClosing(false) + onClose() + }, 250) + return () => clearTimeout(timer) + }, [isClosing, onClose]) + + if (!isOpen && !isClosing) return null + + const handleOverlayClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) handleClose() + } + + return createPortal( + + {children} + , + document.body, + ) +} diff --git a/Frontend/src/_components/Sidebar/ModalButton.tsx b/Frontend/src/_components/Sidebar/ModalButton.tsx new file mode 100644 index 00000000..0904374e --- /dev/null +++ b/Frontend/src/_components/Sidebar/ModalButton.tsx @@ -0,0 +1,36 @@ +import styled from 'styled-components' + +export const ModalButton = styled.button<{ + $variant?: 'primary' | 'secondary' +}>` + height: 36px; + padding: 0 16px; + border-radius: 6px; + font-family: 'Instrument Sans', sans-serif; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + border: 1px solid + ${({ $variant }) => ($variant === 'primary' ? '#a0d1ff' : '#d1d5db')}; + background: ${({ $variant }) => + $variant === 'primary' ? '#b3e0ff' : 'white'}; + color: ${({ $variant }) => ($variant === 'primary' ? '#1d3c66' : '#333')}; + + &:hover { + background: ${({ $variant }) => + $variant === 'primary' ? '#99ccff' : '#f3f4f6'}; + border-color: ${({ $variant }) => + $variant === 'primary' ? '#99ccff' : '#9ca3af'}; + transform: scale(1.05); + } + + &:active { + transform: scale(0.97); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +` diff --git a/Frontend/src/_components/Sidebar/Sidebar.tsx b/Frontend/src/_components/Sidebar/Sidebar.tsx index 67eaace2..57b0a137 100644 --- a/Frontend/src/_components/Sidebar/Sidebar.tsx +++ b/Frontend/src/_components/Sidebar/Sidebar.tsx @@ -3,7 +3,7 @@ import { useState } from 'react' import styled, { keyframes } from 'styled-components' import TabItem from './TabItem.tsx' import AddTabButton from './AddTabButton.tsx' -import EditTabModal from './EditTabModal.tsx' +import { EditTabModal } from './EditTabModal.tsx' import { TabsFileManager } from './TabsFileManager.tsx' import { useTabsContext } from '@/_components/ContextHooks/useTabsContext' diff --git a/Frontend/src/_components/Sidebar/TabItem.tsx b/Frontend/src/_components/Sidebar/TabItem.tsx index 36b534dc..e4ed6b98 100644 --- a/Frontend/src/_components/Sidebar/TabItem.tsx +++ b/Frontend/src/_components/Sidebar/TabItem.tsx @@ -1,6 +1,7 @@ -import { useState, type FC, useEffect } from 'react' -import styled, { keyframes } from 'styled-components' +import { useState, type FC } from 'react' +import styled from 'styled-components' import type { TabData } from '@/_components/ContextHooks/TabsContext' +import DeleteModal from './DeleteModal' interface TabItemProps { tab: TabData @@ -12,38 +13,6 @@ interface TabItemProps { canDelete: boolean } -const formatDate = (date: Date): string => { - const now = new Date() - const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60) - if (diffInHours < 1) return 'Just now' - if (diffInHours < 24) return `${Math.floor(diffInHours)}h ago` - if (diffInHours < 24 * 7) return `${Math.floor(diffInHours / 24)}d ago` - return date.toLocaleDateString() -} - -// --- Animations --- -const fadeIn = keyframes` - from { opacity: 0; } - to { opacity: 1; } -` - -const fadeOut = keyframes` - from { opacity: 1; } - to { opacity: 0; } -` - -const bounceIn = keyframes` - 0% { transform: scale(0.8); opacity: 0; } - 60% { transform: scale(1.05); opacity: 1; } - 100% { transform: scale(1); opacity: 1; } -` - -const bounceOut = keyframes` - 0% { transform: scale(1); opacity: 1; } - 100% { transform: scale(0.8); opacity: 0; } -` - -// --- Styled Components --- const TabItemWrapper = styled.div<{ $isActive: boolean $isCollapsed: boolean @@ -55,18 +24,13 @@ const TabItemWrapper = styled.div<{ transition: all 0.2s ease; background: ${({ $isActive }) => ($isActive ? '#99ccff' : '#b3e0ff')}; color: #1d3c66; - font-family: 'Instrument Sans', sans-serif; font-weight: ${({ $isActive }) => ($isActive ? 'bold' : 'normal')}; + font-family: 'Instrument Sans', sans-serif; &:hover { background: #99ccff; font-weight: bold; } - - &:active { - background: #99ccff; - font-weight: bold; - } ` const TabContent = styled.div<{ $isCollapsed: boolean }>` @@ -75,38 +39,6 @@ const TabContent = styled.div<{ $isCollapsed: boolean }>` justify-content: ${({ $isCollapsed }) => $isCollapsed ? 'center' : 'space-between'}; width: 100%; -` - -const TabInfo = styled.div<{ $isCollapsed: boolean }>` - display: flex; - flex-direction: column; - flex: 1; - min-width: 0; - opacity: ${({ $isCollapsed }) => ($isCollapsed ? 0 : 1)}; - transition: opacity 0.3s ease; -` - -const TabName = styled.span` - font-family: 'Instrument Sans', sans-serif; - font-size: 14px; - font-weight: bold; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -` - -const TabMeta = styled.div` - display: flex; - align-items: center; - gap: 8px; - margin-top: 2px; - opacity: 0.7; - font-family: 'Instrument Sans', sans-serif; -` - -const TabStats = styled.span` - font-size: 11px; - font-weight: 400; font-family: 'Instrument Sans', sans-serif; ` @@ -121,7 +53,6 @@ const TabIcon = styled.div<{ $isActive: boolean }>` justify-content: center; font-size: 12px; color: ${({ $isActive }) => ($isActive ? 'white' : '#6b7280')}; - flex-shrink: 0; margin-right: 8px; font-family: 'Instrument Sans', sans-serif; ` @@ -132,6 +63,7 @@ const TabActions = styled.div<{ $isCollapsed: boolean; $isActive: boolean }>` gap: 4px; opacity: ${({ $isCollapsed }) => ($isCollapsed ? 0 : 1)}; transition: opacity 0.3s ease; + font-family: 'Instrument Sans', sans-serif; ` const ActionButton = styled.button<{ $isActive: boolean }>` @@ -149,11 +81,6 @@ const ActionButton = styled.button<{ $isActive: boolean }>` font-size: 12px; font-family: 'Instrument Sans', sans-serif; transition: all 0.2s ease; - opacity: 0; - - ${TabItemWrapper}:hover & { - opacity: 1; - } &:hover { background: ${({ $isActive }) => @@ -166,73 +93,7 @@ const ActionButton = styled.button<{ $isActive: boolean }>` } ` -const DeleteModalBackdrop = styled.div<{ $isClosing: boolean }>` - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 10000; - animation: ${({ $isClosing }) => ($isClosing ? fadeOut : fadeIn)} 0.2s ease - forwards; -` - -const DeleteModalContent = styled.div<{ $isClosing: boolean }>` - background: white; - padding: 24px; - border-radius: 12px; - text-align: center; - font-family: 'Instrument Sans', sans-serif; - min-width: 300px; - animation: ${({ $isClosing }) => ($isClosing ? bounceOut : bounceIn)} 0.25s - ease forwards; -` - -const ModalButtons = styled.div` - margin-top: 16px; - display: flex; - justify-content: center; - gap: 12px; -` - -const Button = styled.button<{ $variant?: 'primary' | 'secondary' }>` - height: 36px; - padding: 0 16px; - border-radius: 6px; - font-family: 'Instrument Sans', sans-serif; - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - border: 1px solid - ${({ $variant }) => ($variant === 'primary' ? '#a0d1ff' : '#d1d5db')}; - background: ${({ $variant }) => - $variant === 'primary' ? '#b3e0ff' : 'white'}; - color: ${({ $variant }) => ($variant === 'primary' ? '#1d3c66' : '#333')}; - - &:hover { - background: ${({ $variant }) => - $variant === 'primary' ? '#99ccff' : '#f3f4f6'}; - border-color: ${({ $variant }) => - $variant === 'primary' ? '#99ccff' : '#9ca3af'}; - transform: scale(1.05); - } - - &:active { - transform: scale(0.97); - } - - &:disabled { - opacity: 0.5; - cursor: not-allowed; - } -` - -const TabItem: FC = ({ +export const TabItem: FC = ({ tab, isActive, isCollapsed, @@ -242,7 +103,6 @@ const TabItem: FC = ({ canDelete, }) => { const [showDeleteModal, setShowDeleteModal] = useState(false) - const [isClosing, setIsClosing] = useState(false) const handleEdit = (e: React.MouseEvent) => { e.stopPropagation() @@ -254,21 +114,14 @@ const TabItem: FC = ({ setShowDeleteModal(true) } - const confirmDelete = () => { + const handleConfirmDelete = () => { onDelete() - setIsClosing(true) + setShowDeleteModal(false) } - const cancelDelete = () => setIsClosing(true) - - useEffect(() => { - if (!isClosing) return - const timer = setTimeout(() => { - setShowDeleteModal(false) - setIsClosing(false) - }, 250) - return () => clearTimeout(timer) - }, [isClosing]) + const handleCancelDelete = () => { + setShowDeleteModal(false) + } return ( <> @@ -280,31 +133,21 @@ const TabItem: FC = ({ 📍 - - {tab.name} - - {tab.pins.length} pins - - {formatDate(tab.lastModified)} - - + {!isCollapsed && ( +
+
{tab.name}
+
+ {tab.pins.length} pins • {tab.lastModified.toLocaleDateString()} +
+
+ )} - + ✏️ {canDelete && ( - + 🗑️ )} @@ -313,19 +156,12 @@ const TabItem: FC = ({ {showDeleteModal && ( - - -

Are you sure you want to delete "{tab.name}"?

- - - - -
-
+ )} ) diff --git a/Frontend/src/_components/Sidebar/TabsFileManager.tsx b/Frontend/src/_components/Sidebar/TabsFileManager.tsx index 85b48e79..68d43b68 100644 --- a/Frontend/src/_components/Sidebar/TabsFileManager.tsx +++ b/Frontend/src/_components/Sidebar/TabsFileManager.tsx @@ -1,6 +1,7 @@ import React, { useRef, useState } from 'react' import styled from 'styled-components' import { useTabsContext } from '../ContextHooks/useTabsContext' +import ConfirmModal from './ConfirmModal' const FileManagementContainer = styled.div` padding: 1rem; @@ -74,96 +75,6 @@ const StatusMessage = styled.div<{ $type: 'success' | 'error' }>` ${(props) => (props.$type === 'success' ? '#c3e6cb' : '#f5c6cb')}; ` -const ModalBackdrop = styled.div` - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 10000; - animation: fadeIn 0.2s ease; - - @keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } - } -` - -const ModalContent = styled.div` - background: white; - padding: 24px; - border-radius: 12px; - text-align: center; - font-family: 'Instrument Sans', sans-serif; - min-width: 300px; - animation: slideDown 0.2s ease; - - @keyframes slideDown { - from { - transform: translateY(-10px); - opacity: 0; - } - to { - transform: translateY(0); - opacity: 1; - } - } -` - -const ModalButtons = styled.div` - margin-top: 16px; - display: flex; - justify-content: center; - gap: 12px; -` - -const ModalButton = styled.button<{ $variant?: 'primary' | 'secondary' }>` - height: 36px; - padding: 0 16px; - border-radius: 6px; - font-family: 'Instrument Sans', sans-serif; - font-size: 14px; - font-weight: 600; - cursor: pointer; - transition: all 0.2s ease; - border: 1px solid - ${({ $variant }) => ($variant === 'primary' ? '#007acc' : '#d1d5db')}; - background: ${({ $variant }) => - $variant === 'primary' ? '#b3e0ff' : 'white'}; - color: ${({ $variant }) => ($variant === 'primary' ? '#333' : '#333')}; - - &:hover { - background: ${({ $variant }) => - $variant === 'primary' ? '#99ccff' : '#f3f4f6'}; - border-color: ${({ $variant }) => - $variant === 'primary' ? '#99ccff' : '#9ca3af'}; - transform: scale(1.05); - } - - &:active { - transform: scale(0.95); - } - - &:focus { - outline: none; - box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.2); - } - - &:disabled { - opacity: 0.5; - cursor: not-allowed; - transform: none; - } -` - export const TabsFileManager: React.FC = () => { const { tabs, exportTabsToJSON, importTabsFromJSON, clearAllTabs } = useTabsContext() @@ -212,9 +123,7 @@ export const TabsFileManager: React.FC = () => { if (fileInputRef.current) fileInputRef.current.value = '' } - const handleClear = () => { - setShowClearModal(true) - } + const handleClear = () => setShowClearModal(true) const confirmClear = () => { clearAllTabs() @@ -255,23 +164,15 @@ export const TabsFileManager: React.FC = () => { )} - {showClearModal && ( - - -

- Are you sure you want to clear all tabs? This cannot be undone. -

- - - Yes - - - Cancel - - -
-
- )} + {/* Use ConfirmModal for clearing */} + ) } diff --git a/Frontend/src/pages/Home/Home.tsx b/Frontend/src/pages/Home/Home.tsx index d1cea33c..f6e8718c 100644 --- a/Frontend/src/pages/Home/Home.tsx +++ b/Frontend/src/pages/Home/Home.tsx @@ -1,14 +1,12 @@ import type { FC } from 'react' import { FullScreenLayout, MainLayout } from '../../_components' import Dashboard from './_components/Dashboard' -import BottomSheet from './_components/BottomSheet' const Home: FC = () => { return ( - ) diff --git a/Frontend/src/pages/Home/_components/BottomSheet/BottomSheet.tsx b/Frontend/src/pages/Home/_components/BottomSheet/BottomSheet.tsx index 2a33313a..443b410a 100644 --- a/Frontend/src/pages/Home/_components/BottomSheet/BottomSheet.tsx +++ b/Frontend/src/pages/Home/_components/BottomSheet/BottomSheet.tsx @@ -6,7 +6,7 @@ import WeatherStats from './WeatherStats' const BottomSheetWrapper = styled.div` position: absolute; bottom: 0; - left: 50%; + left: 56%; transform: translateX(-50%); z-index: 400; display: flex; @@ -35,7 +35,7 @@ const StyledBottomSheet = styled.div<{ isOpen: boolean }>` box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); padding: ${({ isOpen }) => (isOpen ? '16px 20px' : '8px 20px')}; width: 100%; - max-height: ${({ isOpen }) => (isOpen ? '80vh' : '60px')}; + max-height: ${({ isOpen }) => (isOpen ? '50vh' : '60px')}; overflow: hidden; transition: max-height 0.3s ease, @@ -43,6 +43,23 @@ const StyledBottomSheet = styled.div<{ isOpen: boolean }>` position: relative; ` +const ScrollableContent = styled.div` + overflow-y: auto; + max-height: calc(50vh - 60px); + padding-right: 8px; + + &::-webkit-scrollbar { + width: 6px; + } + &::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.2); + border-radius: 3px; + } + &::-webkit-scrollbar-track { + background: transparent; + } +` + const BottomSheet: FC = () => { const { isOpen, toggle } = useBottomSheet(false) @@ -53,7 +70,9 @@ const BottomSheet: FC = () => { - + + + ) diff --git a/Frontend/src/pages/Home/_components/BottomSheet/WeatherStats.tsx b/Frontend/src/pages/Home/_components/BottomSheet/WeatherStats.tsx index 68d6e90e..b129638b 100644 --- a/Frontend/src/pages/Home/_components/BottomSheet/WeatherStats.tsx +++ b/Frontend/src/pages/Home/_components/BottomSheet/WeatherStats.tsx @@ -7,6 +7,9 @@ import { usePinContext } from '@/_components/ContextHooks/usePinContext' import { useControlPanelContext } from '@/_components/ContextHooks/useControlPanelContext' import type { WeatherData } from '@/_components/ContextHooks/contexts' +const Spacer = styled.div` + height: 40px; +` const HeaderRow = styled.div` display: flex; font-family: 'Instrument Sans', sans-serif; @@ -216,6 +219,8 @@ const WeatherStats: FC = ({ isExpanded = true }) => { renderWeatherBars(locationTwoPin.weatherData)} + + Past Weather Report @@ -311,6 +316,7 @@ const WeatherStats: FC = ({ isExpanded = true }) => { {isExpanded && (
+ diff --git a/Frontend/src/pages/Home/_components/Dashboard/MapView/MapView.tsx b/Frontend/src/pages/Home/_components/Dashboard/MapView/MapView.tsx index f4627987..47616463 100644 --- a/Frontend/src/pages/Home/_components/Dashboard/MapView/MapView.tsx +++ b/Frontend/src/pages/Home/_components/Dashboard/MapView/MapView.tsx @@ -2,6 +2,7 @@ import type { FC } from 'react' import styled from 'styled-components' import Map from './Map' import ControlPanel from './ControlPanel' +import BottomSheet from '../../BottomSheet/BottomSheet' const MapContainer = styled.div` width: 100%; @@ -27,14 +28,23 @@ const MapTitle = styled.div` box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); ` +const BottomSheetWrapper = styled.div` + position: absolute; + bottom: 0; + left: 0; + width: 100%; + z-index: 500; /* above the map but below overlays */ +` + const MapView: FC = () => { return ( - empty - - + \ + + + ) }