From 53fa0f38adba967aaec7217ae98372553b6c24e9 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Fri, 28 Aug 2026 22:25:59 +0200 Subject: [PATCH] feat: suggestions --- src/App.css | 12 - src/App.tsx | 69 +-- src/components/NavSearch.css | 571 ++++++++++++++++++ src/components/NavSearch.tsx | 816 ++++++++++++++++++++++++++ src/locales/de.json | 13 + src/locales/en.json | 13 + src/locales/es.json | 13 + src/locales/fr.json | 13 + src/locales/it.json | 13 + src/locales/ja.json | 13 + src/locales/ko.json | 13 + src/locales/nl.json | 13 + src/locales/pl.json | 13 + src/locales/pt.json | 13 + src/locales/ru.json | 13 + src/locales/tr.json | 13 + src/locales/zh.json | 13 + src/pages/PluginDetail.tsx | 4 +- src/pages/auth/CheckEmailPage.css | 26 +- src/pages/auth/ForgotPasswordPage.tsx | 100 ++-- src/pages/auth/LoginPage.tsx | 102 ++-- src/pages/auth/RegisterPage.tsx | 94 ++- src/pages/auth/ResetPasswordPage.tsx | 63 +- 23 files changed, 1764 insertions(+), 262 deletions(-) create mode 100644 src/components/NavSearch.css create mode 100644 src/components/NavSearch.tsx diff --git a/src/App.css b/src/App.css index 89b758e..15636bc 100644 --- a/src/App.css +++ b/src/App.css @@ -53,7 +53,6 @@ h3 { .page-title, .auth-title { font-size: clamp(2.5rem, 5vw, 4rem); - text-transform: uppercase; } .page-title span { @@ -1100,8 +1099,6 @@ h3 { .filter-title { font-size: 1rem; - text-transform: uppercase; - letter-spacing: 1px; margin: 0; } @@ -1650,8 +1647,6 @@ h3 { color: var(--primary, #FF7518); margin-bottom: 0.25rem; font-size: 1rem; - text-transform: uppercase; - letter-spacing: 0.5px; } .early-access-text { @@ -1713,7 +1708,6 @@ h3 { .plugin-title { font-size: 3.5rem; - text-transform: uppercase; overflow-wrap: break-word; word-break: break-word; } @@ -2007,8 +2001,6 @@ h3 { display: flex; align-items: center; gap: 0.45rem; - text-transform: uppercase; - letter-spacing: 0.5px; } .plugin-recent-update-badge { @@ -2529,8 +2521,6 @@ h3 { .review-form-title { font-size: 1rem; font-weight: 800; - text-transform: uppercase; - letter-spacing: 0.05em; margin: 0 0 1.5rem 0; color: var(--text); } @@ -3052,8 +3042,6 @@ h3 { font-weight: 800; color: var(--text); margin: 0 0 0.4rem; - text-transform: uppercase; - letter-spacing: 0.03em; } .library-empty-sub { diff --git a/src/App.tsx b/src/App.tsx index 002609c..7928cc3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; -import { BrowserRouter as Router, Routes, Route, Link, NavLink, useNavigate, Outlet, Navigate, useLocation } from 'react-router-dom'; +import { BrowserRouter as Router, Routes, Route, Link, NavLink, Outlet, Navigate, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Search, Store, X } from 'lucide-react'; +import { Store } from 'lucide-react'; import Home from './pages/Home'; import PluginDetail from './pages/PluginDetail'; import LoginPage from './pages/auth/LoginPage'; @@ -26,6 +26,7 @@ import LegalNoticePage from './pages/legal/LegalNoticePage'; import AdminPanel from './pages/admin/AdminPanel'; import { CookieBanner } from './components/CookieBanner'; import { openCookiePreferencesModal } from './utils/consent'; +import { NavSearch } from './components/NavSearch'; import './App.css'; import api from './api'; @@ -272,44 +273,7 @@ const DashboardLayout = () => { // --- Components --- const Navbar = ({ user }: any) => { const { t } = useTranslation(); - const location = useLocation(); - const [searchQuery, setSearchQuery] = useState(() => { - if (typeof window !== 'undefined' && window.location.pathname === '/search') { - return new URLSearchParams(window.location.search).get('q') || ''; - } - return ''; - }); const [isMenuOpen, setIsMenuOpen] = useState(false); - const navigate = useNavigate(); - - // Sync searchQuery with URL 'q' parameter when navigating - useEffect(() => { - if (location.pathname === '/search') { - const urlQuery = new URLSearchParams(location.search).get('q') || ''; - setSearchQuery(urlQuery); - } else { - setSearchQuery(''); - } - }, [location.pathname, location.search]); - - const handleSearch = (event: React.KeyboardEvent) => { - if (event.key === 'Enter') { - const query = searchQuery.trim(); - if (query) { - navigate(`/search?q=${encodeURIComponent(query)}`); - } else { - navigate('/search'); - } - setIsMenuOpen(false); - } - }; - - const handleClear = () => { - setSearchQuery(''); - if (location.pathname === '/search') { - navigate('/search'); - } - }; const toggleMenu = () => setIsMenuOpen(!isMenuOpen); @@ -341,32 +305,9 @@ const Navbar = ({ user }: any) => { {user && (user.role === 'admin' || user.role === 'moderator') && setIsMenuOpen(false)}>Staff} - {/* ── Center Search Bar ── */} + {/* ── Center Search Bar with Suggestions ── */}
-
- - setSearchQuery(e.target.value)} - onKeyDown={handleSearch} - autoComplete="off" - autoCorrect="off" - spellCheck={false} - /> - {searchQuery && ( - - )} -
+ setIsMenuOpen(false)} />
diff --git a/src/components/NavSearch.css b/src/components/NavSearch.css new file mode 100644 index 0000000..f9d88f4 --- /dev/null +++ b/src/components/NavSearch.css @@ -0,0 +1,571 @@ +.nav-search-container { + position: relative; + width: 100%; + max-width: 440px; +} + +.nav-search-input-wrap { + position: relative; + width: 100%; + display: flex; + align-items: center; +} + +.nav-search-icon { + position: absolute; + left: 14px; + color: var(--text-muted, #94a3b8); + pointer-events: none; + z-index: 2; + transition: color 0.2s ease; +} + +.nav-search-input-wrap input { + width: 100%; + padding: 8px 60px 8px 38px; + border-radius: 20px; + border: 1px solid var(--border, rgba(255, 255, 255, 0.12)); + background: rgba(255, 255, 255, 0.05); + color: var(--text, #f8fafc); + font-size: 0.875rem; + outline: none; + transition: all 0.2s ease; +} + +.nav-search-input-wrap input::placeholder { + color: var(--text-muted, #94a3b8); +} + +.nav-search-input-wrap input:focus, +.nav-search-input-wrap.dropdown-active input { + border-color: var(--primary, #f97316); + background: rgba(0, 0, 0, 0.55); + box-shadow: 0 0 12px rgba(249, 115, 22, 0.25); +} + +.nav-search-input-wrap:focus-within .nav-search-icon { + color: var(--primary, #f97316); +} + +.nav-search-end-adornments { + position: absolute; + right: 10px; + display: flex; + align-items: center; + gap: 6px; + z-index: 2; +} + +.nav-search-spinner { + animation: nav-search-spin 0.8s linear infinite; + color: var(--primary, #f97316); +} + +@keyframes nav-search-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.nav-search-clear { + background: none; + border: none; + color: var(--text-muted, #94a3b8); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + border-radius: 50%; + transition: color 0.15s, background-color 0.15s; +} + +.nav-search-clear:hover { + color: var(--text, #f8fafc); + background: rgba(255, 255, 255, 0.1); +} + +/* ── Suggestions Dropdown ── */ +.nav-search-dropdown { + position: absolute; + top: calc(100% + 8px); + left: 0; + right: 0; + width: 100%; + min-width: 380px; + background: #121316; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 14px; + box-shadow: 0 18px 40px rgba(0, 0, 0, 0.65), 0 0 0 1px rgba(249, 115, 22, 0.1); + z-index: 1000; + max-height: 480px; + display: flex; + flex-direction: column; + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + overflow: hidden; + animation: navSearchFadeIn 0.15s ease-out; +} + +@keyframes navSearchFadeIn { + from { + opacity: 0; + transform: translateY(-6px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.nav-search-results-list { + overflow-y: auto; + flex: 1; + padding: 6px; + max-height: 380px; +} + +.nav-search-results-list::-webkit-scrollbar { + width: 6px; +} + +.nav-search-results-list::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.15); + border-radius: 3px; +} + +.nav-search-results-list::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.25); +} + +/* ── Sections ── */ +.nav-search-section { + margin-bottom: 8px; +} + +.nav-search-section:last-child { + margin-bottom: 0; +} + +.nav-search-section-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 10px 4px; +} + +.nav-search-section-title { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.78rem; + font-weight: 600; + color: var(--text-muted, #94a3b8); +} + +.nav-search-clear-recent-btn { + background: none; + border: none; + color: var(--text-muted, #94a3b8); + font-size: 0.72rem; + font-weight: 500; + cursor: pointer; + padding: 2px 6px; + border-radius: 4px; + transition: all 0.15s; +} + +.nav-search-clear-recent-btn:hover { + color: var(--primary, #f97316); + background: rgba(249, 115, 22, 0.1); +} + +/* ── Generic Item ── */ +.nav-search-item { + display: flex; + align-items: center; + gap: 10px; + padding: 7px 10px; + border-radius: 8px; + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease; + user-select: none; +} + +.nav-search-item:hover, +.nav-search-item.selected { + background: rgba(255, 255, 255, 0.07); +} + +.nav-search-item.selected { + background: rgba(249, 115, 22, 0.12); + outline: 1px solid rgba(249, 115, 22, 0.35); +} + +.nav-search-item-icon.muted { + color: var(--text-muted, #94a3b8); +} + +.nav-search-item-text { + flex: 1; + font-size: 0.875rem; + color: var(--text, #f8fafc); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.nav-search-item-title { + flex: 1; + font-size: 0.875rem; + font-weight: 500; + color: var(--text, #f8fafc); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.nav-search-item-badge { + font-size: 0.72rem; + color: var(--text-muted, #94a3b8); + background: rgba(255, 255, 255, 0.06); + padding: 2px 7px; + border-radius: 12px; + white-space: nowrap; +} + +.nav-search-item-remove-btn { + background: none; + border: none; + color: var(--text-muted, #94a3b8); + cursor: pointer; + padding: 3px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + opacity: 0.6; + transition: opacity 0.15s, color 0.15s, background-color 0.15s; +} + +.nav-search-item-remove-btn:hover { + opacity: 1; + color: #ef4444; + background: rgba(239, 68, 68, 0.12); +} + +/* ── Plugin Item ── */ +.nav-search-plugin-item { + gap: 12px; +} + +.nav-search-plugin-thumb { + width: 34px; + height: 34px; + border-radius: 7px; + overflow: hidden; + background: rgba(255, 255, 255, 0.05); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + border: 1px solid rgba(255, 255, 255, 0.08); +} + +.nav-search-plugin-thumb img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.nav-search-thumb-fallback { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 0.875rem; + color: var(--primary, #f97316); + background: rgba(249, 115, 22, 0.12); +} + +.nav-search-plugin-info { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.nav-search-plugin-title-row { + display: flex; + align-items: center; + gap: 6px; +} + +.nav-search-plugin-name { + font-size: 0.875rem; + font-weight: 600; + color: var(--text, #f8fafc); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.nav-search-plugin-meta { + display: flex; + align-items: center; + gap: 5px; + font-size: 0.75rem; + color: var(--text-muted, #94a3b8); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.nav-search-meta-dot { + opacity: 0.4; +} + +.nav-search-plugin-cat { + opacity: 0.85; +} + +.nav-search-pill.preorder { + font-size: 0.65rem; + font-weight: 600; + color: #38bdf8; + background: rgba(56, 189, 248, 0.12); + border: 1px solid rgba(56, 189, 248, 0.25); + padding: 1px 5px; + border-radius: 4px; + white-space: nowrap; +} + +.nav-search-plugin-pricing { + flex-shrink: 0; + display: flex; + align-items: center; +} + +.nav-search-price-badge { + font-size: 0.75rem; + font-weight: 700; + padding: 2px 7px; + border-radius: 6px; + white-space: nowrap; +} + +.nav-search-price-badge.free { + color: #22c55e; + background: rgba(34, 197, 94, 0.12); + border: 1px solid rgba(34, 197, 94, 0.25); +} + +.nav-search-price-badge.paid { + color: var(--text, #f8fafc); + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.12); +} + +.nav-search-sale-wrap { + display: flex; + align-items: center; + gap: 5px; +} + +.nav-search-sale-discount { + font-size: 0.68rem; + font-weight: 700; + color: #ef4444; + background: rgba(239, 68, 68, 0.15); + padding: 2px 5px; + border-radius: 4px; +} + +/* ── Category Item ── */ +.nav-search-icon-circle { + width: 28px; + height: 28px; + border-radius: 7px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.nav-search-icon-circle.category { + background: rgba(249, 115, 22, 0.12); + color: var(--primary, #f97316); +} + +/* ── Author Item ── */ +.nav-search-author-avatar { + width: 28px; + height: 28px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: 700; + color: #fff; + flex-shrink: 0; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +} + +.nav-search-author-info { + flex: 1; + min-width: 0; +} + +/* ── Tags Grid ── */ +.nav-search-tags-grid { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 4px 6px; +} + +.nav-search-tag-chip { + display: flex; + align-items: center; + gap: 4px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + color: var(--text, #f8fafc); + padding: 4px 10px; + border-radius: 14px; + font-size: 0.78rem; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; +} + +.nav-search-tag-chip:hover, +.nav-search-tag-chip.selected { + background: rgba(249, 115, 22, 0.15); + border-color: rgba(249, 115, 22, 0.4); + color: var(--primary, #f97316); +} + +.nav-search-tag-hash { + color: var(--primary, #f97316); + font-weight: 700; +} + +/* ── Matching Text Highlight ── */ +.nav-search-highlight { + background: transparent; + color: var(--primary, #f97316); + font-weight: 700; + text-decoration: underline; + text-underline-offset: 2px; +} + +/* ── States ── */ +.nav-search-loading-state, +.nav-search-empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 24px 16px; + text-align: center; + color: var(--text-muted, #94a3b8); + gap: 8px; +} + +.nav-search-empty-title { + font-size: 0.875rem; + font-weight: 500; + color: var(--text, #f8fafc); + margin: 0; +} + +.nav-search-empty-hint { + font-size: 0.75rem; + color: var(--text-muted, #94a3b8); + margin: 0; +} + +/* ── View All Results Footer ── */ +.nav-search-view-all-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + background: rgba(0, 0, 0, 0.35); + border-top: 1px solid rgba(255, 255, 255, 0.08); + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease; +} + +.nav-search-view-all-footer:hover, +.nav-search-view-all-footer.selected { + background: rgba(249, 115, 22, 0.15); + border-top-color: rgba(249, 115, 22, 0.3); +} + +.nav-search-view-all-left { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.825rem; + font-weight: 600; + color: var(--text, #f8fafc); +} + +.nav-search-view-all-icon { + color: var(--primary, #f97316); +} + +.nav-search-view-all-right { + display: flex; + align-items: center; + gap: 6px; +} + +.nav-search-key-hint { + display: flex; + align-items: center; + gap: 3px; + font-size: 0.7rem; + color: var(--text-muted, #94a3b8); + background: rgba(255, 255, 255, 0.08); + padding: 2px 6px; + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.nav-search-view-all-arrow { + color: var(--text-muted, #94a3b8); + transition: transform 0.15s ease, color 0.15s ease; +} + +.nav-search-view-all-footer:hover .nav-search-view-all-arrow, +.nav-search-view-all-footer.selected .nav-search-view-all-arrow { + transform: translateX(2px); + color: var(--primary, #f97316); +} + +/* ── Mobile Responsiveness ── */ +@media (max-width: 768px) { + .nav-search-container { + max-width: 100%; + } + + .nav-search-dropdown { + position: fixed; + top: auto; + left: 12px; + right: 12px; + width: auto; + min-width: 0; + max-height: 60vh; + } +} diff --git a/src/components/NavSearch.tsx b/src/components/NavSearch.tsx new file mode 100644 index 0000000..d819279 --- /dev/null +++ b/src/components/NavSearch.tsx @@ -0,0 +1,816 @@ +import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { + Search, + X, + Clock, + User, + Folder, + Tag, + Package, + ArrowRight, + CornerDownLeft, + Loader2, +} from 'lucide-react'; +import api from '../api'; +import { getPluginUrl } from '../utils/url'; +import './NavSearch.css'; + +export interface SearchSuggestionPlugin { + id: number; + name: string; + dev_name: string; + category: string | null; + preview_path: string | null; + price_cents: number; + price: number; + type: 'free' | 'paid' | 'adwall'; + sale_active: boolean; + sale_discount_percent: number; + downloads: number; + is_early_access: boolean; + is_preorder: boolean; +} + +export interface SearchSuggestionAuthor { + id: number; + username: string; + plugin_count: number; +} + +export interface SearchSuggestionCategory { + name: string; + plugin_count: number; +} + +export interface SearchSuggestionsResponse { + plugins: SearchSuggestionPlugin[]; + authors: SearchSuggestionAuthor[]; + categories: SearchSuggestionCategory[]; + tags: string[]; +} + +const RECENT_SEARCHES_KEY = 'pumpkin_market_recent_searches'; +const MAX_RECENT_SEARCHES = 6; + +const getRecentSearches = (): string[] => { + try { + const raw = localStorage.getItem(RECENT_SEARCHES_KEY); + if (!raw) return []; + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed.slice(0, MAX_RECENT_SEARCHES) : []; + } catch { + return []; + } +}; + +const saveRecentSearch = (term: string) => { + const clean = term.trim(); + if (!clean) return; + try { + const existing = getRecentSearches(); + const updated = [clean, ...existing.filter((s) => s.toLowerCase() !== clean.toLowerCase())].slice( + 0, + MAX_RECENT_SEARCHES + ); + localStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(updated)); + } catch { + // Ignore localStorage errors + } +}; + +const removeRecentSearch = (term: string) => { + try { + const existing = getRecentSearches(); + const updated = existing.filter((s) => s.toLowerCase() !== term.toLowerCase()); + localStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(updated)); + } catch { + // Ignore localStorage errors + } +}; + +const clearRecentSearches = () => { + try { + localStorage.removeItem(RECENT_SEARCHES_KEY); + } catch { + // Ignore localStorage errors + } +}; + +const clientSuggestionCache = new Map(); +const MAX_CLIENT_CACHE = 100; + +const getCachedSuggestions = (term: string): SearchSuggestionsResponse | undefined => { + return clientSuggestionCache.get(term.toLowerCase()); +}; + +const setCachedSuggestions = (term: string, data: SearchSuggestionsResponse) => { + if (clientSuggestionCache.size >= MAX_CLIENT_CACHE) { + const firstKey = clientSuggestionCache.keys().next().value; + if (firstKey) clientSuggestionCache.delete(firstKey); + } + clientSuggestionCache.set(term.toLowerCase(), data); +}; + +const HighlightMatch: React.FC<{ text: string; query: string }> = ({ text, query }) => { + if (!query.trim()) return <>{text}; + const trimmed = query.trim(); + const escaped = trimmed.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const parts = text.split(new RegExp(`(${escaped})`, 'gi')); + return ( + <> + {parts.map((part, i) => + part.toLowerCase() === trimmed.toLowerCase() ? ( + + {part} + + ) : ( + part + ) + )} + + ); +}; + +const getAuthorAccentColor = (name: string) => { + const colors = [ + '#f97316', '#3b82f6', '#10b981', '#8b5cf6', + '#ec4899', '#06b6d4', '#eab308', '#14b8a6', + ]; + let hash = 0; + for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash); + return colors[Math.abs(hash) % colors.length]; +}; + +type FlattenedItem = + | { kind: 'recent'; query: string } + | { kind: 'plugin'; plugin: SearchSuggestionPlugin } + | { kind: 'author'; author: SearchSuggestionAuthor } + | { kind: 'category'; category: SearchSuggestionCategory } + | { kind: 'tag'; tag: string } + | { kind: 'view_all'; query: string }; + +interface NavSearchProps { + onNavigate?: () => void; + className?: string; + placeholder?: string; + autoFocus?: boolean; +} + +export const NavSearch: React.FC = ({ + onNavigate, + className = '', + placeholder, + autoFocus = false, +}) => { + const { t } = useTranslation(); + const location = useLocation(); + const navigate = useNavigate(); + + const [query, setQuery] = useState(() => { + if (typeof window !== 'undefined' && window.location.pathname === '/search') { + return new URLSearchParams(window.location.search).get('q') || ''; + } + return ''; + }); + + const [isOpen, setIsOpen] = useState(false); + const [loading, setLoading] = useState(false); + const [suggestions, setSuggestions] = useState(null); + const [recentSearches, setRecentSearches] = useState([]); + const [selectedIndex, setSelectedIndex] = useState(-1); + + const containerRef = useRef(null); + const inputRef = useRef(null); + const debounceTimerRef = useRef | null>(null); + const abortControllerRef = useRef(null); + + // Sync query with URL on /search page + useEffect(() => { + if (location.pathname === '/search') { + const urlQ = new URLSearchParams(location.search).get('q') || ''; + setQuery(urlQ); + } + }, [location.pathname, location.search]); + + // Load recent searches on mount / open + const refreshRecentSearches = useCallback(() => { + setRecentSearches(getRecentSearches()); + }, []); + + useEffect(() => { + refreshRecentSearches(); + }, [refreshRecentSearches]); + + // Close dropdown when clicking outside + useEffect(() => { + const handleOutsideClick = (e: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) { + setIsOpen(false); + setSelectedIndex(-1); + } + }; + document.addEventListener('mousedown', handleOutsideClick); + return () => { + document.removeEventListener('mousedown', handleOutsideClick); + }; + }, []); + + // Fetch suggestions with instant in-memory client cache & debounced fallback + useEffect(() => { + const trimmed = query.trim(); + + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + if (!trimmed) { + setSuggestions(null); + setLoading(false); + setSelectedIndex(-1); + return; + } + + // 1. Check client memory cache (0ms instant response on backspace / repeat) + const cached = getCachedSuggestions(trimmed); + if (cached) { + setSuggestions(cached); + setLoading(false); + setSelectedIndex(-1); + return; + } + + setLoading(true); + const controller = new AbortController(); + abortControllerRef.current = controller; + + debounceTimerRef.current = setTimeout(async () => { + try { + const res = await api.get('/plugins/suggestions', { + params: { q: trimmed }, + signal: controller.signal, + }); + setCachedSuggestions(trimmed, res.data); + setSuggestions(res.data); + setSelectedIndex(-1); + } catch (err: any) { + if (err?.code !== 'ERR_CANCELED' && err?.name !== 'CanceledError') { + console.error('Failed to fetch search suggestions:', err); + } + } finally { + setLoading(false); + } + }, 160); + + return () => { + if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current); + if (abortControllerRef.current) abortControllerRef.current.abort(); + }; + }, [query]); + + // Compute flattened navigable items + const flattenedItems = useMemo(() => { + const items: FlattenedItem[] = []; + const trimmed = query.trim(); + + if (!trimmed) { + // Show recent searches + recentSearches.forEach((s) => { + items.push({ kind: 'recent', query: s }); + }); + return items; + } + + if (suggestions) { + // Plugins + suggestions.plugins.forEach((plugin) => { + items.push({ kind: 'plugin', plugin }); + }); + // Categories + suggestions.categories.forEach((category) => { + items.push({ kind: 'category', category }); + }); + // Authors + suggestions.authors.forEach((author) => { + items.push({ kind: 'author', author }); + }); + // Tags + suggestions.tags.forEach((tag) => { + items.push({ kind: 'tag', tag }); + }); + // View all results + items.push({ kind: 'view_all', query: trimmed }); + } + + return items; + }, [query, recentSearches, suggestions]); + + const executeSearch = useCallback( + (searchTerm: string) => { + const clean = searchTerm.trim(); + if (clean) { + saveRecentSearch(clean); + refreshRecentSearches(); + navigate(`/search?q=${encodeURIComponent(clean)}`); + } else { + navigate('/search'); + } + setIsOpen(false); + setSelectedIndex(-1); + inputRef.current?.blur(); + onNavigate?.(); + }, + [navigate, onNavigate, refreshRecentSearches] + ); + + const handleSelectItem = useCallback( + (item: FlattenedItem) => { + switch (item.kind) { + case 'recent': { + setQuery(item.query); + executeSearch(item.query); + break; + } + case 'plugin': { + saveRecentSearch(item.plugin.name); + refreshRecentSearches(); + navigate(getPluginUrl(item.plugin)); + setIsOpen(false); + setSelectedIndex(-1); + inputRef.current?.blur(); + onNavigate?.(); + break; + } + case 'author': { + saveRecentSearch(item.author.username); + refreshRecentSearches(); + navigate(`/profile/${encodeURIComponent(item.author.username)}`); + setIsOpen(false); + setSelectedIndex(-1); + inputRef.current?.blur(); + onNavigate?.(); + break; + } + case 'category': { + saveRecentSearch(item.category.name); + refreshRecentSearches(); + navigate(`/search?category=${encodeURIComponent(item.category.name)}`); + setIsOpen(false); + setSelectedIndex(-1); + inputRef.current?.blur(); + onNavigate?.(); + break; + } + case 'tag': { + saveRecentSearch(item.tag); + refreshRecentSearches(); + navigate(`/search?q=${encodeURIComponent(item.tag)}`); + setIsOpen(false); + setSelectedIndex(-1); + inputRef.current?.blur(); + onNavigate?.(); + break; + } + case 'view_all': { + executeSearch(item.query); + break; + } + } + }, + [executeSearch, navigate, onNavigate, refreshRecentSearches] + ); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'ArrowDown') { + e.preventDefault(); + if (!isOpen) { + setIsOpen(true); + refreshRecentSearches(); + return; + } + if (flattenedItems.length === 0) return; + setSelectedIndex((prev) => (prev + 1 >= flattenedItems.length ? 0 : prev + 1)); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + if (!isOpen) return; + if (flattenedItems.length === 0) return; + setSelectedIndex((prev) => (prev <= 0 ? flattenedItems.length - 1 : prev - 1)); + } else if (e.key === 'Enter') { + e.preventDefault(); + if (selectedIndex >= 0 && selectedIndex < flattenedItems.length) { + handleSelectItem(flattenedItems[selectedIndex]); + } else { + executeSearch(query); + } + } else if (e.key === 'Escape') { + setIsOpen(false); + setSelectedIndex(-1); + inputRef.current?.blur(); + } + }; + + const handleClear = () => { + setQuery(''); + setSuggestions(null); + setSelectedIndex(-1); + inputRef.current?.focus(); + if (location.pathname === '/search') { + navigate('/search'); + } + }; + + const handleRemoveRecent = (e: React.MouseEvent, term: string) => { + e.stopPropagation(); + removeRecentSearch(term); + refreshRecentSearches(); + }; + + const handleClearAllRecent = (e: React.MouseEvent) => { + e.stopPropagation(); + clearRecentSearches(); + refreshRecentSearches(); + }; + + const hasSuggestions = + suggestions && + (suggestions.plugins.length > 0 || + suggestions.authors.length > 0 || + suggestions.categories.length > 0 || + suggestions.tags.length > 0); + + const showRecentDropdown = isOpen && !query.trim() && recentSearches.length > 0; + const showSuggestionsDropdown = isOpen && query.trim().length > 0; + + return ( +
+
+
+ + {/* Suggestions & Recent Searches Dropdown */} + {(showRecentDropdown || showSuggestionsDropdown) && ( + + )} +
+ ); +}; + +export default NavSearch; diff --git a/src/locales/de.json b/src/locales/de.json index 8d1cbd4..5aa9af6 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -221,6 +221,19 @@ "customize": "Einstellungen anpassen", "save": "Einstellungen speichern" }, + "search": { + "recent_title": "Letzte Suchen", + "clear_recent": "Löschen", + "no_suggestions": "Keine Vorschläge gefunden", + "press_enter": "Eingabetaste drücken zum Suchen", + "categories_title": "Kategorien", + "plugins_title": "Plugins", + "authors_title": "Entwickler", + "tags_title": "Tags", + "plugin_count": "1 Plugin", + "plugins_count": "{{count}} Plugins", + "view_all_results": "Alle Ergebnisse für „{{query}}“ anzeigen" + }, "common": { "save": "Speichern", "cancel": "Abbrechen", diff --git a/src/locales/en.json b/src/locales/en.json index 286d765..ac5c1a9 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -221,6 +221,19 @@ "customize": "Customize Preferences", "save": "Save Preferences" }, + "search": { + "recent_title": "Recent Searches", + "clear_recent": "Clear", + "no_suggestions": "No suggestions found", + "press_enter": "Press Enter to search", + "categories_title": "Categories", + "plugins_title": "Plugins", + "authors_title": "Developers", + "tags_title": "Tags", + "plugin_count": "1 plugin", + "plugins_count": "{{count}} plugins", + "view_all_results": "View all results for \"{{query}}\"" + }, "common": { "save": "Save", "cancel": "Cancel", diff --git a/src/locales/es.json b/src/locales/es.json index bde3f47..09ddb1f 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -221,6 +221,19 @@ "customize": "Personalizar", "save": "Guardar preferencias" }, + "search": { + "recent_title": "Búsquedas recientes", + "clear_recent": "Borrar", + "no_suggestions": "No se encontraron sugerencias", + "press_enter": "Presiona Enter para buscar", + "categories_title": "Categorías", + "plugins_title": "Plugins", + "authors_title": "Desarrolladores", + "tags_title": "Etiquetas", + "plugin_count": "1 plugin", + "plugins_count": "{{count}} plugins", + "view_all_results": "Ver todos los resultados de «{{query}}»" + }, "common": { "save": "Guardar", "cancel": "Cancelar", diff --git a/src/locales/fr.json b/src/locales/fr.json index af613a9..090270e 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -221,6 +221,19 @@ "customize": "Personnaliser", "save": "Enregistrer les préférences" }, + "search": { + "recent_title": "Recherches récentes", + "clear_recent": "Effacer", + "no_suggestions": "Aucune suggestion trouvée", + "press_enter": "Appuyez sur Entrée pour rechercher", + "categories_title": "Catégories", + "plugins_title": "Plugins", + "authors_title": "Développeurs", + "tags_title": "Tags", + "plugin_count": "1 plugin", + "plugins_count": "{{count}} plugins", + "view_all_results": "Voir tous les résultats pour « {{query}} »" + }, "common": { "save": "Enregistrer", "cancel": "Annuler", diff --git a/src/locales/it.json b/src/locales/it.json index 096e3de..98cb378 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -221,6 +221,19 @@ "customize": "Personalizza", "save": "Salva Preferenze" }, + "search": { + "recent_title": "Ricerche recenti", + "clear_recent": "Cancella", + "no_suggestions": "Nessun suggerimento trovato", + "press_enter": "Premi Invio per cercare", + "categories_title": "Categorie", + "plugins_title": "Plugin", + "authors_title": "Sviluppatori", + "tags_title": "Tag", + "plugin_count": "1 plugin", + "plugins_count": "{{count}} plugin", + "view_all_results": "Mostra tutti i risultati per \"{{query}}\"" + }, "common": { "save": "Salva", "cancel": "Annulla", diff --git a/src/locales/ja.json b/src/locales/ja.json index 9b478cf..84516c0 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -221,6 +221,19 @@ "customize": "設定を変更", "save": "設定を保存" }, + "search": { + "recent_title": "最近の検索", + "clear_recent": "クリア", + "no_suggestions": "候補が見つかりません", + "press_enter": "Enterを押して検索", + "categories_title": "カテゴリー", + "plugins_title": "プラグイン", + "authors_title": "開発者", + "tags_title": "タグ", + "plugin_count": "1個のプラグイン", + "plugins_count": "{{count}}個のプラグイン", + "view_all_results": "「{{query}}」の全検索結果を表示" + }, "common": { "save": "保存", "cancel": "キャンセル", diff --git a/src/locales/ko.json b/src/locales/ko.json index 56c021e..ddaba66 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -221,6 +221,19 @@ "customize": "설정 변경", "save": "설정 저장" }, + "search": { + "recent_title": "최근 검색", + "clear_recent": "지우기", + "no_suggestions": "추천 검색어가 없습니다", + "press_enter": "Enter를 눌러 검색", + "categories_title": "카테고리", + "plugins_title": "플러그인", + "authors_title": "개발자", + "tags_title": "태그", + "plugin_count": "플러그인 1개", + "plugins_count": "플러그인 {{count}}개", + "view_all_results": "\"{{query}}\" 전체 결과 보기" + }, "common": { "save": "저장", "cancel": "취소", diff --git a/src/locales/nl.json b/src/locales/nl.json index b67553b..5e02029 100644 --- a/src/locales/nl.json +++ b/src/locales/nl.json @@ -221,6 +221,19 @@ "customize": "Aanpassen", "save": "Voorkeuren Opslaan" }, + "search": { + "recent_title": "Recente zoekopdrachten", + "clear_recent": "Wissen", + "no_suggestions": "Geen suggesties gevonden", + "press_enter": "Druk op Enter om te zoeken", + "categories_title": "Categorieën", + "plugins_title": "Plugins", + "authors_title": "Ontwikkelaars", + "tags_title": "Tags", + "plugin_count": "1 plugin", + "plugins_count": "{{count}} plugins", + "view_all_results": "Bekijk alle resultaten voor \"{{query}}\"" + }, "common": { "save": "Opslaan", "cancel": "Annuleren", diff --git a/src/locales/pl.json b/src/locales/pl.json index 12962a9..6374b49 100644 --- a/src/locales/pl.json +++ b/src/locales/pl.json @@ -221,6 +221,19 @@ "customize": "Dostosuj", "save": "Zapisz Ustawienia" }, + "search": { + "recent_title": "Ostatnie wyszukiwania", + "clear_recent": "Wyczyść", + "no_suggestions": "Brak sugestii", + "press_enter": "Naciśnij Enter, aby wyszukać", + "categories_title": "Kategorie", + "plugins_title": "Wtyczki", + "authors_title": "Twórcy", + "tags_title": "Tagi", + "plugin_count": "1 wtyczka", + "plugins_count": "{{count}} wtyczek", + "view_all_results": "Zobacz wszystkie wyniki dla \"{{query}}\"" + }, "common": { "save": "Zapisz", "cancel": "Anuluj", diff --git a/src/locales/pt.json b/src/locales/pt.json index 46aa8f7..9c5941b 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -221,6 +221,19 @@ "customize": "Personalizar", "save": "Salvar Preferências" }, + "search": { + "recent_title": "Pesquisas recentes", + "clear_recent": "Limpar", + "no_suggestions": "Nenhuma sugestão encontrada", + "press_enter": "Pressione Enter para pesquisar", + "categories_title": "Categorias", + "plugins_title": "Plugins", + "authors_title": "Desenvolvedores", + "tags_title": "Tags", + "plugin_count": "1 plugin", + "plugins_count": "{{count}} plugins", + "view_all_results": "Ver todos os resultados de \"{{query}}\"" + }, "common": { "save": "Salvar", "cancel": "Cancelar", diff --git a/src/locales/ru.json b/src/locales/ru.json index 2e48016..f695144 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -221,6 +221,19 @@ "customize": "Настроить", "save": "Сохранить настройки" }, + "search": { + "recent_title": "Недавние поиски", + "clear_recent": "Очистить", + "no_suggestions": "Подсказок не найдено", + "press_enter": "Нажмите Enter для поиска", + "categories_title": "Категории", + "plugins_title": "Плагины", + "authors_title": "Разработчики", + "tags_title": "Теги", + "plugin_count": "1 плагин", + "plugins_count": "Плагинов: {{count}}", + "view_all_results": "Все результаты для «{{query}}»" + }, "common": { "save": "Сохранить", "cancel": "Отмена", diff --git a/src/locales/tr.json b/src/locales/tr.json index 0294352..e9eefff 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -221,6 +221,19 @@ "customize": "Tercihleri Özelleştir", "save": "Tercihleri Kaydet" }, + "search": { + "recent_title": "Son Aramalar", + "clear_recent": "Temizle", + "no_suggestions": "Öneri bulunamadı", + "press_enter": "Aramak için Enter'a basın", + "categories_title": "Kategoriler", + "plugins_title": "Eklentiler", + "authors_title": "Geliştiriciler", + "tags_title": "Etiketler", + "plugin_count": "1 eklenti", + "plugins_count": "{{count}} eklenti", + "view_all_results": "\"{{query}}\" için tüm sonuçları gör" + }, "common": { "save": "Kaydet", "cancel": "İptal", diff --git a/src/locales/zh.json b/src/locales/zh.json index 5419b9b..b5f1af4 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -221,6 +221,19 @@ "customize": "自定义设置", "save": "保存偏好" }, + "search": { + "recent_title": "最近搜索", + "clear_recent": "清除", + "no_suggestions": "未找到搜索建议", + "press_enter": "按 Enter 搜索", + "categories_title": "分类", + "plugins_title": "插件", + "authors_title": "开发者", + "tags_title": "标签", + "plugin_count": "1 个插件", + "plugins_count": "{{count}} 个插件", + "view_all_results": "查看“{{query}}”的所有结果" + }, "common": { "save": "保存", "cancel": "取消", diff --git a/src/pages/PluginDetail.tsx b/src/pages/PluginDetail.tsx index 012bdf2..077cf86 100644 --- a/src/pages/PluginDetail.tsx +++ b/src/pages/PluginDetail.tsx @@ -919,7 +919,7 @@ const PluginDetail = () => { wordBreak: 'break-word' }}>
- Developer Response + Developer Response {formatDate(r.replied_at)}
{ alignItems: 'center', background: 'linear-gradient(to right, rgba(255, 117, 24, 0.05), transparent)' }}> -

Update History

+

Update History

diff --git a/src/pages/auth/CheckEmailPage.css b/src/pages/auth/CheckEmailPage.css index 4984ab8..b674ae8 100644 --- a/src/pages/auth/CheckEmailPage.css +++ b/src/pages/auth/CheckEmailPage.css @@ -3,15 +3,16 @@ justify-content: center; align-items: center; min-height: 70vh; - background-color: #f9f9f9; - font-family: sans-serif; + background-color: transparent; + padding: 40px 20px; } .check-email-box { - background: #fff; + background: var(--surface, #141414); + border: 1px solid var(--border, #2A2A2A); padding: 40px; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0,0,0,0.08); + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0,0,0,0.5); text-align: center; max-width: 450px; width: 100%; @@ -19,34 +20,35 @@ .check-email-icon { margin-bottom: 20px; - color: #4CAF50; /* A nice green */ + color: var(--success, #4ADE80); } .check-email-box h1 { font-size: 24px; margin-bottom: 15px; - color: #333; + color: var(--text, #EAEAEA); + font-weight: 700; } .check-email-box p { - font-size: 16px; + font-size: 15px; line-height: 1.6; - color: #666; + color: var(--text-muted, #888888); margin-bottom: 25px; } .check-email-box strong { - color: #333; + color: var(--text, #EAEAEA); font-weight: 600; } .check-email-box hr { border: none; - border-top: 1px solid #eee; + border-top: 1px solid var(--border, #2A2A2A); margin: 25px 0; } .check-email-footer { font-size: 14px; - color: #999; + color: var(--text-muted, #888888); } diff --git a/src/pages/auth/ForgotPasswordPage.tsx b/src/pages/auth/ForgotPasswordPage.tsx index 8efc195..80620b8 100644 --- a/src/pages/auth/ForgotPasswordPage.tsx +++ b/src/pages/auth/ForgotPasswordPage.tsx @@ -100,12 +100,10 @@ const ForgotPasswordPage = () => { .auth-card { width: 100%; max-width: 440px; - background: #ffffff; + background: var(--surface, #141414); + border: 1px solid var(--border, #2A2A2A); border-radius: 12px; - box-shadow: - 0 0 0 1px rgba(60, 66, 87, 0.08), - 0 2px 4px rgba(60, 66, 87, 0.04), - 0 8px 24px rgba(60, 66, 87, 0.08); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); padding: 40px; animation: cardIn 0.35s cubic-bezier(0.22, 1, 0.36, 1) both; } @@ -119,8 +117,8 @@ const ForgotPasswordPage = () => { .card-title { font-size: 22px; - font-weight: 600; - color: #0a2540; + font-weight: 700; + color: var(--text, #EAEAEA); letter-spacing: -0.4px; line-height: 1.3; } @@ -128,7 +126,7 @@ const ForgotPasswordPage = () => { .card-subtitle { margin-top: 6px; font-size: 14px; - color: #697386; + color: var(--text-muted, #888888); line-height: 1.5; } @@ -137,8 +135,8 @@ const ForgotPasswordPage = () => { .field-label { display: block; font-size: 13px; - font-weight: 500; - color: #3c4257; + font-weight: 600; + color: var(--text, #EAEAEA); margin-bottom: 6px; letter-spacing: 0.01em; } @@ -151,28 +149,38 @@ const ForgotPasswordPage = () => { padding: 0 40px 0 14px; font-size: 15px; font-family: inherit; - color: #0a2540; - background: #ffffff; - border: 1.5px solid #e0e5ec; + color: var(--text, #EAEAEA); + background: rgba(255, 255, 255, 0.05); + border: 1.5px solid var(--border, #2A2A2A); border-radius: 7px; outline: none; - transition: border-color 0.15s ease, box-shadow 0.15s ease; + transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease; -webkit-appearance: none; } + .auth-input::placeholder { color: var(--text-muted, #888888); font-size: 14px; } + .auth-input:hover:not(:disabled) { border-color: rgba(255, 255, 255, 0.25); } + .auth-input:focus { - border-color: #ff6b00; - box-shadow: 0 0 0 3px rgba(255, 107, 0, 0.16); + border-color: var(--primary, #FF7518); + background: rgba(0, 0, 0, 0.4); + box-shadow: 0 0 0 3px rgba(255, 117, 24, 0.2); + } + + .auth-input:disabled { + background: rgba(255, 255, 255, 0.02); + color: var(--text-muted, #666666); + cursor: not-allowed; } .auth-input.email-invalid { - border-color: #df1b41 !important; - box-shadow: 0 0 0 3px rgba(223, 27, 65, 0.12) !important; + border-color: #ef4444 !important; + box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2) !important; } .auth-input.email-valid { - border-color: #1a9e5f !important; - box-shadow: 0 0 0 3px rgba(26, 158, 95, 0.12) !important; + border-color: #22c55e !important; + box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.2) !important; } .field-status-icon { @@ -194,47 +202,54 @@ const ForgotPasswordPage = () => { font-weight: 500; } - .field-message.invalid { color: #df1b41; } + .field-message.invalid { color: #ef4444; } .error-banner { - background: #fff8f8; - border: 1.5px solid #fecdd3; + background: rgba(239, 68, 68, 0.12); + border: 1.5px solid rgba(239, 68, 68, 0.35); border-radius: 7px; padding: 12px 14px; margin-bottom: 20px; - color: #df1b41; + color: #ef4444; font-size: 13px; } .success-banner { - background: #f0fff4; - border: 1.5px solid #c6f6d5; + background: rgba(34, 197, 94, 0.12); + border: 1.5px solid rgba(34, 197, 94, 0.35); border-radius: 7px; padding: 12px 14px; margin-bottom: 20px; - color: #2f855a; + color: #22c55e; font-size: 13px; } .submit-btn { width: 100%; height: 46px; - background: #ff6b00; + background: var(--primary, #FF7518); color: white; border: none; border-radius: 7px; font-size: 15px; - font-weight: 500; + font-weight: 600; cursor: pointer; - transition: background 0.15s ease; + transition: background 0.15s ease, box-shadow 0.15s ease; display: flex; align-items: center; justify-content: center; gap: 8px; } - .submit-btn:hover:not(:disabled) { background: #e85d00; } - .submit-btn:disabled { background: #ffb380; cursor: not-allowed; } + .submit-btn:hover:not(:disabled) { + background: #e85d00; + box-shadow: 0 4px 14px rgba(255, 117, 24, 0.35); + } + + .submit-btn:disabled { + background: rgba(255, 117, 24, 0.5); + cursor: not-allowed; + } .spinner { width: 16px; height: 16px; @@ -246,15 +261,18 @@ const ForgotPasswordPage = () => { @keyframes spin { to { transform: rotate(360deg); } } - .divider { height: 1px; background: #e8ecf1; margin: 24px 0; } + .divider { height: 1px; background: var(--border, #2A2A2A); margin: 24px 0; } - .card-footer { text-align: center; font-size: 13.5px; color: #697386; } + .card-footer { text-align: center; font-size: 13.5px; color: var(--text-muted, #888888); } .card-footer a { - color: #ff6b00; + color: var(--primary, #FF7518); text-decoration: none; - font-weight: 500; + font-weight: 600; + transition: color 0.15s; } + + .card-footer a:hover { color: #ff944d; text-decoration: underline; } `}
@@ -286,14 +304,14 @@ const ForgotPasswordPage = () => { /> {emailIsInvalid && ( - - + + )} {emailIsValid && ( - - + + )}
@@ -309,7 +327,7 @@ const ForgotPasswordPage = () => { onSuccess={(token) => setCaptchaToken(token)} onExpire={() => setCaptchaToken(null)} onError={() => setCaptchaToken(null)} - options={{ theme: 'light', appearance: 'interaction-only' } as any} + options={{ theme: 'dark', appearance: 'interaction-only' } as any} />
diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx index c8a1711..6dde190 100644 --- a/src/pages/auth/LoginPage.tsx +++ b/src/pages/auth/LoginPage.tsx @@ -120,17 +120,6 @@ const LoginPage = () => { return ( <>
@@ -369,7 +357,7 @@ const LoginPage = () => { {requires2fa ? (
-

Enter the 6-digit code from your authenticator app.

+

Enter the 6-digit code from your authenticator app.

{ /> {emailIsInvalid && ( - - + + )} {emailIsValid && ( - - + + )}
@@ -434,7 +422,7 @@ const LoginPage = () => {
- + Forgot password?
@@ -463,7 +451,7 @@ const LoginPage = () => { onSuccess={(token) => setCaptchaToken(token)} onExpire={() => setCaptchaToken(null)} onError={() => setCaptchaToken(null)} - options={{ theme: 'light', appearance: 'interaction-only' } as any} + options={{ theme: 'dark', appearance: 'interaction-only' } as any} />
)} diff --git a/src/pages/auth/RegisterPage.tsx b/src/pages/auth/RegisterPage.tsx index 4a946c9..b5c5655 100644 --- a/src/pages/auth/RegisterPage.tsx +++ b/src/pages/auth/RegisterPage.tsx @@ -114,9 +114,6 @@ const RegisterPage = () => { return ( <>
@@ -441,7 +435,7 @@ const RegisterPage = () => { onSuccess={(token) => setCaptchaToken(token)} onExpire={() => setCaptchaToken(null)} onError={() => setCaptchaToken(null)} - options={{ theme: 'light', appearance: 'interaction-only' } as any} + options={{ theme: 'dark', appearance: 'interaction-only' } as any} />
diff --git a/src/pages/auth/ResetPasswordPage.tsx b/src/pages/auth/ResetPasswordPage.tsx index e2ad4e1..2c4e829 100644 --- a/src/pages/auth/ResetPasswordPage.tsx +++ b/src/pages/auth/ResetPasswordPage.tsx @@ -73,12 +73,10 @@ const ResetPasswordPage = () => { .auth-card { width: 100%; max-width: 440px; - background: #ffffff; + background: var(--surface, #141414); + border: 1px solid var(--border, #2A2A2A); border-radius: 12px; - box-shadow: - 0 0 0 1px rgba(60, 66, 87, 0.08), - 0 2px 4px rgba(60, 66, 87, 0.04), - 0 8px 24px rgba(60, 66, 87, 0.08); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); padding: 40px; animation: cardIn 0.35s cubic-bezier(0.22, 1, 0.36, 1) both; } @@ -92,8 +90,8 @@ const ResetPasswordPage = () => { .card-title { font-size: 22px; - font-weight: 600; - color: #0a2540; + font-weight: 700; + color: var(--text, #EAEAEA); letter-spacing: -0.4px; line-height: 1.3; } @@ -101,7 +99,7 @@ const ResetPasswordPage = () => { .card-subtitle { margin-top: 6px; font-size: 14px; - color: #697386; + color: var(--text-muted, #888888); line-height: 1.5; } @@ -110,8 +108,8 @@ const ResetPasswordPage = () => { .field-label { display: block; font-size: 13px; - font-weight: 500; - color: #3c4257; + font-weight: 600; + color: var(--text, #EAEAEA); margin-bottom: 6px; letter-spacing: 0.01em; } @@ -122,48 +120,65 @@ const ResetPasswordPage = () => { padding: 0 14px; font-size: 15px; font-family: inherit; - color: #0a2540; - background: #ffffff; - border: 1.5px solid #e0e5ec; + color: var(--text, #EAEAEA); + background: rgba(255, 255, 255, 0.05); + border: 1.5px solid var(--border, #2A2A2A); border-radius: 7px; outline: none; - transition: border-color 0.15s ease, box-shadow 0.15s ease; + transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease; } + .auth-input::placeholder { color: var(--text-muted, #888888); } + .auth-input:hover:not(:disabled) { border-color: rgba(255, 255, 255, 0.25); } + .auth-input:focus { - border-color: #ff6b00; - box-shadow: 0 0 0 3px rgba(255, 107, 0, 0.16); + border-color: var(--primary, #FF7518); + background: rgba(0, 0, 0, 0.4); + box-shadow: 0 0 0 3px rgba(255, 117, 24, 0.2); + } + + .auth-input:disabled { + background: rgba(255, 255, 255, 0.02); + color: var(--text-muted, #666666); + cursor: not-allowed; } .error-banner { - background: #fff8f8; - border: 1.5px solid #fecdd3; + background: rgba(239, 68, 68, 0.12); + border: 1.5px solid rgba(239, 68, 68, 0.35); border-radius: 7px; padding: 12px 14px; margin-bottom: 20px; - color: #df1b41; + color: #ef4444; font-size: 13px; } .submit-btn { width: 100%; height: 46px; - background: #ff6b00; + background: var(--primary, #FF7518); color: white; border: none; border-radius: 7px; font-size: 15px; - font-weight: 500; + font-weight: 600; cursor: pointer; - transition: background 0.15s ease; + transition: background 0.15s ease, box-shadow 0.15s ease; display: flex; align-items: center; justify-content: center; gap: 8px; } - .submit-btn:hover:not(:disabled) { background: #e85d00; } - .submit-btn:disabled { background: #ffb380; cursor: not-allowed; } + .submit-btn:hover:not(:disabled) { + background: #e85d00; + box-shadow: 0 4px 14px rgba(255, 117, 24, 0.35); + } + + .submit-btn:disabled { + background: rgba(255, 117, 24, 0.5); + cursor: not-allowed; + } .spinner { width: 16px; height: 16px;