diff --git a/Frontend/src/_components/Form/FormField.tsx b/Frontend/src/_components/Form/FormField.tsx index 3629ea62..7e365386 100644 --- a/Frontend/src/_components/Form/FormField.tsx +++ b/Frontend/src/_components/Form/FormField.tsx @@ -15,7 +15,7 @@ const Label = styled.label` color: #3c3939; font-weight: 400; text-align: left; - margin-left: 1px; + margin-left: 15px; ` interface FormFieldProps { diff --git a/Frontend/src/_components/Header/Menu/LocSearchBar/LocSearchBar.tsx b/Frontend/src/_components/Header/Menu/LocSearchBar/LocSearchBar.tsx index 2adb133b..a3197aca 100644 --- a/Frontend/src/_components/Header/Menu/LocSearchBar/LocSearchBar.tsx +++ b/Frontend/src/_components/Header/Menu/LocSearchBar/LocSearchBar.tsx @@ -1,6 +1,13 @@ -import type { FC } from 'react' +import type { FC, ChangeEvent } from 'react' import styled from 'styled-components' +interface LocSearchBarProps { + placeholder?: string + value: string + onChange: (e: ChangeEvent) => void + onClick?: () => void +} + const LocSearchInput = styled.input` width: 100%; padding: 8px; @@ -14,7 +21,22 @@ const LocSearchInput = styled.input` color: #333; } ` -const LocSearchBar: FC = () => { - return + +const LocSearchBar: FC = ({ + placeholder, + value, + onChange, + onClick, +}) => { + return ( + + ) } + export default LocSearchBar diff --git a/Frontend/src/_components/Header/Menu/compare/CompareMenu.tsx b/Frontend/src/_components/Header/Menu/compare/CompareMenu.tsx index f5eef71a..41bb64e2 100644 --- a/Frontend/src/_components/Header/Menu/compare/CompareMenu.tsx +++ b/Frontend/src/_components/Header/Menu/compare/CompareMenu.tsx @@ -1,7 +1,6 @@ import type { FC } from 'react' -//import { Link } from 'react-router-dom' +import { useState } from 'react' import styled from 'styled-components' -import LocSearchBar from '../LocSearchBar' const AccordionItem = styled.div<{ isExpanded?: boolean }>` background-color: white; @@ -43,20 +42,73 @@ const AccordionIcon = styled.span<{ isOpen?: boolean }>` const AccordionContent = styled.div<{ isOpen?: boolean }>` max-height: ${({ isOpen }) => (isOpen ? '500px' : '0')}; - overflow: hidden; + overflow-y: auto; transition: max-height 0.3s ease; padding: ${({ isOpen }) => (isOpen ? '16px' : '0 16px')}; background: rgba(255, 255, 255, 0.8); ` const LocTitle = styled.h4` - border: none; - text-align: middle; margin: 4px 0; font-size: 14px; font-family: 'Instrument Sans', sans-serif; ` +const LocSearchInput = styled.input` + width: 100%; + padding: 8px; + margin-bottom: 8px; + border: 1px solid #c2e9ff; + background: #c2e9ff; + color: #333; + border-radius: 4px; + font-size: 14px; + &::placeholder { + color: #333; + } +` + +const ToggleButton = styled.button<{ disabled?: boolean }>` + width: 100%; + padding: 0.5rem 1rem; + background-color: #007acc; + color: white; + border: none; + border-radius: 6px; + font-family: 'Instrument Sans', sans-serif; + cursor: pointer; + font-size: 0.85rem; + margin-top: 8px; + opacity: ${({ disabled }) => (disabled ? 0.5 : 1)}; + pointer-events: ${({ disabled }) => (disabled ? 'none' : 'auto')}; + + &:hover { + background-color: #005f99; + } +` + +const SearchResultsColumn = styled.div` + display: flex; + flex-direction: column; + background: #e5f3ff; + border-radius: 6px; + max-height: 150px; + overflow-y: auto; + padding: 8px; + font-size: 0.85rem; +` + +const ResultItem = styled.div<{ selected?: boolean }>` + padding: 6px 8px; + border-radius: 4px; + cursor: pointer; + background-color: ${({ selected }) => (selected ? '#c2e9ff' : 'transparent')}; + + &:hover { + background-color: #d0eaff; + } +` + interface CompareMenuProps { isLoc1Open: boolean isLoc2Open: boolean @@ -66,6 +118,17 @@ interface CompareMenuProps { closeLoc2: () => void } +const australianStates = [ + 'New South Wales', + 'Victoria', + 'Queensland', + 'South Australia', + 'Western Australia', + 'Tasmania', + 'Northern Territory', + 'Australian Capital Territory', +] + const CompareMenu: FC = ({ isLoc1Open, isLoc2Open, @@ -74,8 +137,69 @@ const CompareMenu: FC = ({ closeLoc1, closeLoc2, }) => { + const [searchInput1, setSearchInput1] = useState('') + const [searchInput2, setSearchInput2] = useState('') + const [results1, setResults1] = useState([]) + const [results2, setResults2] = useState([]) + const [pendingSelection1, setPendingSelection1] = useState( + null, + ) + const [pendingSelection2, setPendingSelection2] = useState( + null, + ) + const [selected1, setSelected1] = useState(null) + const [selected2, setSelected2] = useState(null) + + const handleSearch1 = (value: string) => { + setSearchInput1(value) + setResults1( + australianStates.filter((state) => + state.toLowerCase().includes(value.toLowerCase()), + ), + ) + setPendingSelection1(null) + } + + const handleSearch2 = (value: string) => { + setSearchInput2(value) + setResults2( + australianStates.filter((state) => + state.toLowerCase().includes(value.toLowerCase()), + ), + ) + setPendingSelection2(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 toggleSelection2 = () => { + if (pendingSelection2) { + setSelected2(pendingSelection2) + setSearchInput2(pendingSelection2) + setResults2([]) + setPendingSelection2(null) + } else { + setSelected2(null) + setSearchInput2('') + } + } + return ( <> + {/* Location 1 */} { @@ -87,11 +211,34 @@ const CompareMenu: FC = ({ Set Location 1 - - + handleSearch1(e.target.value)} + /> + {results1.length > 0 && ( + + {results1.map((item, idx) => ( + handleClickResult1(item)} + selected={pendingSelection1 === item} + > + {item} + + ))} + + )} + + {selected1 ? 'Deselect' : 'Select'} + + {/* Location 2 */} { @@ -103,8 +250,30 @@ const CompareMenu: FC = ({ Set Location 2 - - + handleSearch2(e.target.value)} + /> + {results2.length > 0 && ( + + {results2.map((item, idx) => ( + handleClickResult2(item)} + selected={pendingSelection2 === item} + > + {item} + + ))} + + )} + + {selected2 ? 'Deselect' : 'Select'} + diff --git a/Frontend/src/_components/Header/SearchBar/AIdropdown.tsx b/Frontend/src/_components/Header/SearchBar/AIdropdown.tsx index 270ed7ff..66207c1f 100644 --- a/Frontend/src/_components/Header/SearchBar/AIdropdown.tsx +++ b/Frontend/src/_components/Header/SearchBar/AIdropdown.tsx @@ -2,29 +2,93 @@ import type { FC } from 'react' import styled from 'styled-components' import { renderAIContent } from '@/_components/ContextHooks/aiRender' -const Container = styled.div` +const Container = styled.div<{ width: number }>` position: absolute; - top: 45px; + top: 100%; left: 0; - width: 100%; - min-width: 660px; - max-width: 720px; + width: ${({ width }) => width}px; background: #ffffff; border-radius: 12px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); border: 1px solid #eaeaea; - padding: 12px 0; + padding: 12px 16px; max-height: 520px; overflow-y: auto; - z-index: 100000; + font-family: 'Instrument Sans', sans-serif; + line-height: 1.6; + white-space: pre-wrap; + z-index: 1000; +` + +const ButtonRow = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 12px; +` + +const AIButton = styled.button` + padding: 6px 12px; + border-radius: 6px; + border: none; + background-color: #007acc; + color: white; + cursor: pointer; + font-size: 14px; + + &:hover { + background-color: #005f99; + } +` + +const TextBlock = styled.div` + color: #333; + line-height: 1.6; + white-space: pre-wrap; ` interface AIDropdownProps { prompt: string + aiState: 'idle' | 'loading' | 'done' + loadingDots: number + onAskAI: () => void + onClear: () => void + inputWidth: number + aiDescription?: string } -const AIdropdown: FC = ({ prompt }) => { - return {renderAIContent(prompt)} +const AIdropdown: FC = ({ + prompt, + aiState, + loadingDots, + onAskAI, + onClear, + inputWidth, + aiDescription = 'Ask the AI to generate a response based on your input.', +}) => { + return ( + e.preventDefault()}> + {aiState === 'idle' && ( + <> + {aiDescription} + + Ask AI + + + )} + {aiState === 'loading' && ( + AI Response: Loading{'.'.repeat(loadingDots)} + )} + {aiState === 'done' && ( + <> + AI Response: {renderAIContent(prompt)} + + Clear + + + )} + + ) } export default AIdropdown diff --git a/Frontend/src/_components/Header/SearchBar/SearchBar.tsx b/Frontend/src/_components/Header/SearchBar/SearchBar.tsx index 49b61397..59532c78 100644 --- a/Frontend/src/_components/Header/SearchBar/SearchBar.tsx +++ b/Frontend/src/_components/Header/SearchBar/SearchBar.tsx @@ -1,5 +1,5 @@ import type { FC } from 'react' -import { useState } from 'react' +import { useState, useEffect, useRef } from 'react' import styled from 'styled-components' import SearchDropdown from './SearchDropdown' import AIdropdown from './AIdropdown' @@ -18,21 +18,19 @@ const ToggleSlot = styled.div` pointer-events: auto; ` -const SearchInput = styled.input` - width: 550px; +const SearchInput = styled.input<{ inputWidth: number }>` + width: ${({ inputWidth }) => inputWidth}px; height: 55px; + border-radius: 20px; background-color: #def8ffff; border: none; - border-radius: 20px; outline: none; font-size: 16px; color: #333; text-align: left; padding-left: 20px; - z-index: 1000; transition: width 0.3s ease, - background-color 0.3s ease, box-shadow 0.3s ease; &::placeholder { @@ -40,8 +38,6 @@ const SearchInput = styled.input` } &:focus { - width: 750px; - background-color: #def8ffff; box-shadow: 0 0 0 2px #e3f2fd; } ` @@ -50,10 +46,71 @@ const SearchBar: FC = () => { const [query, setQuery] = useState('') const [focused, setFocused] = useState(false) const [mode, setMode] = useState<'search' | 'ai'>('search') + const [aiState, setAiState] = useState<'idle' | 'loading' | 'done'>('idle') + const [loadingDots, setLoadingDots] = useState(1) + const [inputWidth, setInputWidth] = useState(550) + const [showDropdown, setShowDropdown] = useState(false) + + const inputRef = useRef(null) + + //Update width dynamically based on focus + useEffect(() => { + setInputWidth(focused ? 750 : 550) + if (focused && mode === 'search') setShowDropdown(true) + else setShowDropdown(false) + }, [focused, mode]) + + //AI loading animation + useEffect(() => { + if (aiState === 'loading') { + const interval = setInterval(() => { + setLoadingDots((prev) => (prev % 5) + 1) + }, 500) + + const timeout = setTimeout(() => { + setAiState('done') + clearInterval(interval) + }, 3000) + + return () => { + clearInterval(interval) + clearTimeout(timeout) + } + } + }, [aiState]) + + const handleAskAI = () => { + if (query.trim() === '') return + setAiState('loading') + } + + const handleClearAI = () => { + setQuery('') + setAiState('idle') + } + + const handleModeChange = (nextMode: 'search' | 'ai') => { + setMode(nextMode) + setQuery('') + setAiState('idle') + setShowDropdown(false) + } + + const handleSelect = (loc: { + id: number + title: string + subtitle: string + }) => { + setQuery(loc.title) + setShowDropdown(false) + console.log('Selected location:', loc) + } return ( { onFocus={() => setFocused(true)} onBlur={() => setFocused(false)} /> + - setMode(next)} /> + - {query && - focused && - (mode === 'search' ? ( - - ) : ( - - ))} + + {/* Search Dropdown */} + {mode === 'search' && showDropdown && query && ( + + )} + + {/* AI Dropdown */} + {mode === 'ai' && query.trim() !== '' && ( + + )} ) } diff --git a/Frontend/src/_components/Header/SearchBar/SearchDropdown.tsx b/Frontend/src/_components/Header/SearchBar/SearchDropdown.tsx index 0f38d917..44ca0f7e 100644 --- a/Frontend/src/_components/Header/SearchBar/SearchDropdown.tsx +++ b/Frontend/src/_components/Header/SearchBar/SearchDropdown.tsx @@ -1,47 +1,40 @@ import type { FC } from 'react' import styled from 'styled-components' -const Container = styled.div` +const Container = styled.div<{ width: number }>` position: absolute; - top: 45px; + top: 100%; left: 0; - width: 100%; - min-width: 660px; - max-width: 720px; + width: ${({ width }) => width}px; background: #ffffff; border-radius: 12px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); border: 1px solid #eaeaea; - padding: 12px 0; + padding: 12px 16px; max-height: 520px; overflow-y: auto; - z-index: 100000; + font-family: 'Instrument Sans', sans-serif; + white-space: pre-wrap; + line-height: 1.6; + z-index: 1000; ` const ResultItem = styled.div` - padding: 14px 18px; + padding: 12px 0; cursor: pointer; transition: background-color 0.2s ease; - font-family: 'Instrument Sans', sans-serif; &:hover { background-color: #f5f5f5; } ` -const ItemTitle = styled.div` - font-size: 14px; - font-weight: 500; - color: #333; - margin-bottom: 4px; -` - -const ItemSubtitle = styled.div` - font-size: 12px; - color: #666; -` +interface SearchDropdownProps { + query: string + onSelect: (loc: { id: number; title: string; subtitle: string }) => void + inputWidth: number +} -// Dummy search results const dummyResults = [ { id: 1, title: 'Sydney, Australia', subtitle: 'New South Wales' }, { id: 2, title: 'Melbourne, Australia', subtitle: 'Victoria' }, @@ -50,17 +43,32 @@ const dummyResults = [ { id: 5, title: 'Adelaide, Australia', subtitle: 'South Australia' }, ] -const renderResults = () => { - return dummyResults.map((result) => ( - - {result.title} - {result.subtitle} - - )) -} +const SearchDropdown: FC = ({ + query, + onSelect, + inputWidth, +}) => { + const results = dummyResults.filter((r) => + r.title.toLowerCase().includes(query.toLowerCase()), + ) + + if (results.length === 0) { + return No search results + } -const SearchDropdown: FC = () => { - return {renderResults()} + return ( + + {results.map((r) => ( + e.preventDefault()} + onClick={() => onSelect(r)} + > + {r.title} - {r.subtitle} + + ))} + + ) } export default SearchDropdown diff --git a/Frontend/src/_components/Sidebar/Sidebar.tsx b/Frontend/src/_components/Sidebar/Sidebar.tsx index 68c42020..67eaace2 100644 --- a/Frontend/src/_components/Sidebar/Sidebar.tsx +++ b/Frontend/src/_components/Sidebar/Sidebar.tsx @@ -1,6 +1,6 @@ import type { FC } from 'react' import { useState } from 'react' -import styled from 'styled-components' +import styled, { keyframes } from 'styled-components' import TabItem from './TabItem.tsx' import AddTabButton from './AddTabButton.tsx' import EditTabModal from './EditTabModal.tsx' @@ -12,21 +12,57 @@ interface SidebarProps { onToggle: () => void } +const slideIn = keyframes` + from { transform: translateX(-20px); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +` + const SidebarWrapper = styled.div<{ $isOpen: boolean }>` position: fixed; top: 0; left: 0; height: 100vh; - width: ${({ $isOpen }) => ($isOpen ? '280px' : '60px')}; + width: ${({ $isOpen }) => ($isOpen ? '280px' : '0px')}; background: linear-gradient(180deg, #ffffff 0%, #f8fafb 100%); - box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1); - transition: width 0.3s ease; - z-index: 1000; + box-shadow: ${({ $isOpen }) => + $isOpen ? '2px 0 8px rgba(0, 0, 0, 0.1)' : 'none'}; + transition: + width 0.3s ease, + box-shadow 0.3s ease; + z-index: 999; display: flex; flex-direction: column; overflow: hidden; ` +const ToggleButtonWrapper = styled.div<{ $isOpen: boolean }>` + position: fixed; + top: 16px; + left: ${({ $isOpen }) => ($isOpen ? '280px' : '16px')}; + width: 40px; + height: 40px; + border-radius: 50%; + background: #ffffff; + border: 1px solid #d1d5db; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 1000; + transition: + all 0.3s ease, + left 0.3s ease; + + &:hover { + background: #f3f4f6; + border-color: #9ca3af; + } + + &:active { + transform: scale(0.95); + } +` + const SidebarHeader = styled.div<{ $isOpen: boolean }>` padding: 16px; border-bottom: 1px solid #e5e7eb; @@ -49,58 +85,35 @@ const SidebarTitle = styled.h2<{ $isOpen: boolean }>` overflow: hidden; ` -const ToggleButton = styled.button` - width: 32px; - height: 32px; - border-radius: 6px; - border: 1px solid #d1d5db; - background: #ffffff; - color: #6b7280; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 14px; - transition: all 0.2s ease; - - &:hover { - background: #f3f4f6; - border-color: #9ca3af; - } - - &:active { - transform: scale(0.95); - } -` - const TabsContainer = styled.div` flex: 1; overflow-y: auto; padding: 8px; - /* Custom scrollbar */ &::-webkit-scrollbar { width: 6px; } - &::-webkit-scrollbar-track { background: transparent; } - &::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 3px; } - &::-webkit-scrollbar-thumb:hover { background: #9ca3af; } ` -const TabsList = styled.div` +const TabsList = styled.div<{ $isOpen: boolean }>` display: flex; flex-direction: column; gap: 4px; + + /* Animate tab items sliding in */ + & > * { + animation: ${slideIn} 0.3s ease forwards; + } ` const SidebarFooter = styled.div` @@ -114,52 +127,31 @@ const Sidebar: FC = ({ isOpen, onToggle }) => { useTabsContext() const [editingTabId, setEditingTabId] = useState(null) - const handleAddTab = () => { - addTab() - } - + const handleAddTab = () => addTab() const handleTabClick = (tabId: string) => { - if (tabId !== activeTabId) { - switchTab(tabId) - } + if (tabId !== activeTabId) switchTab(tabId) } - - const handleTabEdit = (tabId: string) => { - setEditingTabId(tabId) - } - + const handleTabEdit = (tabId: string) => setEditingTabId(tabId) const handleTabDelete = (tabId: string) => { - if (tabs.length > 1) { - removeTab(tabId) - } + if (tabs.length > 1) removeTab(tabId) } - const handleTabRename = (tabId: string, newName: string) => { renameTab(tabId, newName) setEditingTabId(null) } - - const handleModalClose = () => { - setEditingTabId(null) - } - + const handleModalClose = () => setEditingTabId(null) const editingTab = tabs.find((tab) => tab.id === editingTabId) return ( <> + {/* Sidebar panel */} {isOpen && Map Tabs} - - {isOpen ? '‹' : '›'} - - + {tabs.map((tab) => ( = ({ isOpen, onToggle }) => { + {/* Circular toggle button */} + + {isOpen ? '‹' : '›'} + + {editingTab && ( { + 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 @@ -21,18 +53,19 @@ const TabItemWrapper = styled.div<{ border-radius: 8px; cursor: pointer; transition: all 0.2s ease; - background: ${({ $isActive }) => ($isActive ? '#007acc' : 'transparent')}; - color: ${({ $isActive }) => ($isActive ? 'white' : '#374151')}; - border: ${({ $isActive }) => - $isActive ? '1px solid #007acc' : '1px solid transparent'}; + background: ${({ $isActive }) => ($isActive ? '#99ccff' : '#b3e0ff')}; + color: #1d3c66; + font-family: 'Instrument Sans', sans-serif; + font-weight: ${({ $isActive }) => ($isActive ? 'bold' : 'normal')}; &:hover { - background: ${({ $isActive }) => ($isActive ? '#005f99' : '#f3f4f6')}; - ${({ $isActive }) => !$isActive && 'border-color: #d1d5db;'} + background: #99ccff; + font-weight: bold; } &:active { - transform: scale(0.98); + background: #99ccff; + font-weight: bold; } ` @@ -56,7 +89,7 @@ const TabInfo = styled.div<{ $isCollapsed: boolean }>` const TabName = styled.span` font-family: 'Instrument Sans', sans-serif; font-size: 14px; - font-weight: 500; + font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -68,11 +101,13 @@ const TabMeta = styled.div` 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; ` const TabIcon = styled.div<{ $isActive: boolean }>` @@ -87,6 +122,8 @@ const TabIcon = styled.div<{ $isActive: boolean }>` font-size: 12px; color: ${({ $isActive }) => ($isActive ? 'white' : '#6b7280')}; flex-shrink: 0; + margin-right: 8px; + font-family: 'Instrument Sans', sans-serif; ` const TabActions = styled.div<{ $isCollapsed: boolean; $isActive: boolean }>` @@ -110,6 +147,7 @@ const ActionButton = styled.button<{ $isActive: boolean }>` align-items: center; justify-content: center; font-size: 12px; + font-family: 'Instrument Sans', sans-serif; transition: all 0.2s ease; opacity: 0; @@ -128,20 +166,71 @@ const ActionButton = styled.button<{ $isActive: boolean }>` } ` -const formatDate = (date: Date): string => { - const now = new Date() - const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60) +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; +` - if (diffInHours < 1) { - return 'Just now' - } else if (diffInHours < 24) { - return `${Math.floor(diffInHours)}h ago` - } else if (diffInHours < 24 * 7) { - return `${Math.floor(diffInHours / 24)}d ago` - } else { - return date.toLocaleDateString() +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 = ({ tab, @@ -152,6 +241,9 @@ const TabItem: FC = ({ onDelete, canDelete, }) => { + const [showDeleteModal, setShowDeleteModal] = useState(false) + const [isClosing, setIsClosing] = useState(false) + const handleEdit = (e: React.MouseEvent) => { e.stopPropagation() onEdit() @@ -159,64 +251,83 @@ const TabItem: FC = ({ const handleDelete = (e: React.MouseEvent) => { e.stopPropagation() - if ( - canDelete && - window.confirm(`Are you sure you want to delete "${tab.name}"?`) - ) { - onDelete() - } + setShowDeleteModal(true) } - const handleMouseEnter = () => { - // Actions will show on hover via CSS + const confirmDelete = () => { + onDelete() + setIsClosing(true) } - const handleMouseLeave = () => { - // Actions will hide on hover via CSS - } + const cancelDelete = () => setIsClosing(true) + + useEffect(() => { + if (!isClosing) return + const timer = setTimeout(() => { + setShowDeleteModal(false) + setIsClosing(false) + }, 250) + return () => clearTimeout(timer) + }, [isClosing]) return ( - - - 📍 - - - {tab.name} - - {tab.pins.length} pins - - {formatDate(tab.lastModified)} - - - - - - ✏️ - - {canDelete && ( + <> + + + 📍 + + + {tab.name} + + {tab.pins.length} pins + + {formatDate(tab.lastModified)} + + + + - 🗑️ + ✏️ - )} - - - + {canDelete && ( + + 🗑️ + + )} + + + + + {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 c7627593..85b48e79 100644 --- a/Frontend/src/_components/Sidebar/TabsFileManager.tsx +++ b/Frontend/src/_components/Sidebar/TabsFileManager.tsx @@ -6,13 +6,14 @@ const FileManagementContainer = styled.div` padding: 1rem; border-bottom: 1px solid #e0e0e0; background-color: #f8f9fa; + font-family: 'Instrument Sans', sans-serif; + color: #333; ` const FileManagementTitle = styled.h3` margin: 0 0 1rem 0; font-size: 0.9rem; - font-weight: 600; - color: #495057; + color: #333; ` const ButtonGroup = styled.div` @@ -24,16 +25,18 @@ const ButtonGroup = styled.div` const FileButton = styled.button` padding: 0.5rem 0.75rem; font-size: 0.8rem; - border: 1px solid #007bff; + border: 1px solid #a0d1ff; border-radius: 4px; - background-color: #007bff; - color: white; + background-color: #b3e0ff; + color: #333; cursor: pointer; + font-family: 'Instrument Sans', sans-serif; transition: all 0.2s ease; + font-weight: 200; &:hover { - background-color: #0056b3; - border-color: #0056b3; + background-color: #99ccff; + border-color: #99ccff; } &:disabled { @@ -44,12 +47,13 @@ const FileButton = styled.button` ` const ClearButton = styled(FileButton)` - background-color: #dc3545; - border-color: #dc3545; + background-color: #ffc9c9; + border-color: #ffc9c9; + color: #333; &:hover { - background-color: #c82333; - border-color: #bd2130; + background-color: #ffb3b3; + border-color: #ff9999; } ` @@ -62,13 +66,104 @@ const StatusMessage = styled.div<{ $type: 'success' | 'error' }>` padding: 0.5rem; font-size: 0.8rem; border-radius: 4px; + font-family: 'Instrument Sans', sans-serif; + color: #333; background-color: ${(props) => props.$type === 'success' ? '#d4edda' : '#f8d7da'}; - color: ${(props) => (props.$type === 'success' ? '#155724' : '#721c24')}; border: 1px solid ${(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() @@ -77,6 +172,7 @@ export const TabsFileManager: React.FC = () => { text: string type: 'success' | 'error' } | null>(null) + const [showClearModal, setShowClearModal] = useState(false) const handleExport = () => { try { @@ -113,27 +209,25 @@ export const TabsFileManager: React.FC = () => { setTimeout(() => setStatusMessage(null), 3000) } - // Reset file input - if (fileInputRef.current) { - fileInputRef.current.value = '' - } + if (fileInputRef.current) fileInputRef.current.value = '' } const handleClear = () => { - if ( - window.confirm( - 'Are you sure you want to clear all tabs? This action cannot be undone.', - ) - ) { - clearAllTabs() - setStatusMessage({ - text: 'All tabs cleared and reset to default', - type: 'success', - }) - setTimeout(() => setStatusMessage(null), 3000) - } + setShowClearModal(true) } + const confirmClear = () => { + clearAllTabs() + setStatusMessage({ + text: 'All tabs cleared and reset to default', + type: 'success', + }) + setShowClearModal(false) + setTimeout(() => setStatusMessage(null), 3000) + } + + const cancelClear = () => setShowClearModal(false) + return ( Tab Management @@ -160,6 +254,24 @@ export const TabsFileManager: React.FC = () => { {statusMessage.text} )} + + {showClearModal && ( + + +

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

+ + + Yes + + + Cancel + + +
+
+ )}
) } diff --git a/Frontend/src/pages/Login/_components/LoginForm.tsx b/Frontend/src/pages/Login/_components/LoginForm.tsx index 312962b9..11b8ce4f 100644 --- a/Frontend/src/pages/Login/_components/LoginForm.tsx +++ b/Frontend/src/pages/Login/_components/LoginForm.tsx @@ -83,7 +83,7 @@ const LoginForm: FC = () => { value={formData.email} onChange={(e) => updateFormData('email', e.target.value)} disabled={isLoading} - width="80%" + width="90%" margin="12px 0 14px 0" /> @@ -93,7 +93,7 @@ const LoginForm: FC = () => { value={formData.password} onChange={(e) => updateFormData('password', e.target.value)} disabled={isLoading} - width="80%" + width="90%" margin="12px 0 6px 0" showPasswordToggle={true} passwordVisible={passwordVisible} diff --git a/Frontend/src/pages/Profile/_components/ProfileSettings.tsx b/Frontend/src/pages/Profile/_components/ProfileSettings.tsx index bd7bd86e..13c4d3ee 100644 --- a/Frontend/src/pages/Profile/_components/ProfileSettings.tsx +++ b/Frontend/src/pages/Profile/_components/ProfileSettings.tsx @@ -70,9 +70,14 @@ const FileName = styled.p` font-family: 'Instrument Sans', sans-serif; ` +const ButtonContainer = styled.div` + display: flex; + justify-content: flex-end; + margin-top: 1rem; +` + const SubmitButton = styled.button` - width: 100%; - padding: 0.75rem; + padding: 0.5rem 1rem; background-color: #007acc; color: white; border: none; @@ -80,7 +85,7 @@ const SubmitButton = styled.button` font-weight: 500; font-family: 'Instrument Sans', sans-serif; cursor: pointer; - margin-top: 1rem; + font-size: 0.85rem; &:hover { background-color: #005f99; @@ -179,8 +184,9 @@ const ProfileSettings: React.FC = () => { - - Save Changes + + Save Changes + )