mirror of
https://github.com/ryanamay/twinkle.git
synced 2024-09-20 02:40:35 +00:00
improve: theming
This commit is contained in:
parent
43a99125c3
commit
e0b5c6f1d5
4 changed files with 113 additions and 68 deletions
|
@ -2,6 +2,9 @@ import { useState, useEffect } from 'react'
|
|||
import SearchBar from './SearchBar'
|
||||
import Link from 'next/link'
|
||||
import ThemeSwitcher from './ThemeSwitcher'
|
||||
import { IoSparkles } from 'react-icons/io5'
|
||||
import { FaHamburger } from 'react-icons/fa'
|
||||
import { GiHamburgerMenu } from 'react-icons/gi'
|
||||
|
||||
interface NavigationBarProps {
|
||||
onSelectSymbol: (symbol: string) => void
|
||||
|
@ -21,9 +24,9 @@ const NavigationBar = ({ onSelectSymbol }: NavigationBarProps) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<nav className="bg-base text-text">
|
||||
<div className="container mx-auto max-w-8xl flex items-center justify-between py-4 px-4 lg:px-8">
|
||||
<Link className="text-lg font-bold" href="/">TWL
|
||||
<nav className="bg-crust transition-all text-text border-b-2 border-surface0 px-4">
|
||||
<div className="container mx-auto max-w-8xl flex items-center justify-between py-1">
|
||||
<Link className="text-lg font-bold flex flex-row align-middle items-center gap-2" href="/"><IoSparkles /> <span className="md:block hidden">twinkle</span>
|
||||
</Link>
|
||||
<div className="flex-grow max-w-md mx-auto lg:max-w-lg w-full">
|
||||
<SearchBar onSelectSymbol={onSelectSymbol} />
|
||||
|
@ -34,7 +37,7 @@ const NavigationBar = ({ onSelectSymbol }: NavigationBarProps) => {
|
|||
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||
className="bg-mantle px-4 py-2 rounded focus:outline-none focus:bg-overlay0"
|
||||
>
|
||||
X
|
||||
<GiHamburgerMenu />
|
||||
</button>
|
||||
{dropdownOpen && (
|
||||
<div className="absolute right-0 mt-2 w-48 bg-surface0 rounded-md shadow-lg z-20">
|
||||
|
|
|
@ -1,90 +1,126 @@
|
|||
import { useState, useEffect, useCallback } from 'react'
|
||||
import debounce from 'lodash.debounce'
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import debounce from "lodash.debounce";
|
||||
import { FaSearch, FaTimes } from "react-icons/fa";
|
||||
|
||||
interface SearchBarProps {
|
||||
onSelectSymbol: (symbol: string) => void
|
||||
onSelectSymbol: (symbol: string) => void;
|
||||
}
|
||||
|
||||
const SearchBar = ({ onSelectSymbol }: SearchBarProps) => {
|
||||
const [query, setQuery] = useState('')
|
||||
const [suggestions, setSuggestions] = useState<{ symbol: string, description: string }[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [notFound, setNotFound] = useState(false)
|
||||
const [query, setQuery] = useState("");
|
||||
const [selectedQuery, setSelectedQuery] = useState("");
|
||||
const [suggestions, setSuggestions] = useState<{ symbol: string; description: string }[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [notFound, setNotFound] = useState(false);
|
||||
const [isPickerVisible, setIsPickerVisible] = useState(false);
|
||||
const pickerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const fetchSuggestions = async (query: string) => {
|
||||
setLoading(true)
|
||||
setNotFound(false)
|
||||
setLoading(true);
|
||||
setNotFound(false);
|
||||
try {
|
||||
const res = await fetch(`/api/search?query=${query}`)
|
||||
const data = await res.json()
|
||||
const res = await fetch(`/api/search?query=${query}`);
|
||||
const data = await res.json();
|
||||
if (data.result && data.result.length > 0) {
|
||||
setSuggestions(data.result)
|
||||
setSuggestions(data.result.slice(0, 5));
|
||||
} else {
|
||||
setNotFound(true)
|
||||
setSuggestions([])
|
||||
setNotFound(true);
|
||||
setSuggestions([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching suggestions:', error)
|
||||
setNotFound(true)
|
||||
setSuggestions([])
|
||||
console.error("Error fetching suggestions:", error);
|
||||
setNotFound(true);
|
||||
setSuggestions([]);
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedFetchSuggestions = useCallback(debounce((query: string) => {
|
||||
fetchSuggestions(query)
|
||||
}, 300), [])
|
||||
fetchSuggestions(query);
|
||||
}, 300), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (query.length > 1) {
|
||||
debouncedFetchSuggestions(query)
|
||||
debouncedFetchSuggestions(query);
|
||||
setIsPickerVisible(true);
|
||||
} else {
|
||||
setSuggestions([])
|
||||
setNotFound(false)
|
||||
setSuggestions([]);
|
||||
setNotFound(false);
|
||||
setIsPickerVisible(false);
|
||||
}
|
||||
}, [query, debouncedFetchSuggestions])
|
||||
}, [query, debouncedFetchSuggestions]);
|
||||
|
||||
const handleSelect = (symbol: string) => {
|
||||
setQuery('')
|
||||
setSuggestions([])
|
||||
onSelectSymbol(symbol)
|
||||
}
|
||||
setQuery("");
|
||||
setSelectedQuery(symbol);
|
||||
setSuggestions([]);
|
||||
setIsPickerVisible(false);
|
||||
onSelectSymbol(symbol);
|
||||
};
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (pickerRef.current && !pickerRef.current.contains(event.target as Node)) {
|
||||
setIsPickerVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search stocks..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="border p-2 w-full"
|
||||
/>
|
||||
{loading && (
|
||||
<div className="absolute border bg-white p-2 w-full mt-1 text-center">
|
||||
Loading...
|
||||
</div>
|
||||
)}
|
||||
{!loading && notFound && (
|
||||
<div className="absolute border bg-white p-2 w-full mt-1 text-center text-red-500">
|
||||
No results found
|
||||
</div>
|
||||
)}
|
||||
{suggestions.length > 0 && (
|
||||
<ul className="absolute border bg-white w-full mt-1">
|
||||
{suggestions.map((item) => (
|
||||
<li
|
||||
key={item.symbol}
|
||||
onClick={() => handleSelect(item.symbol)}
|
||||
className="p-2 hover:bg-gray-200 cursor-pointer"
|
||||
>
|
||||
{item.description} ({item.symbol})
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="relative w-full" ref={pickerRef}>
|
||||
<div className="relative flex items-center border p-1 w-full border-none md:bg-mantle md:border-surface0 md:rounded-md">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={selectedQuery || "Search stocks..."}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="pl-8 w-full bg-transparent outline-none text-text"
|
||||
onFocus={() => setIsPickerVisible(true)}
|
||||
/>
|
||||
<span className="absolute left-2 text-surface0">
|
||||
<FaSearch className="text-text" />
|
||||
</span>
|
||||
{query && (
|
||||
<span className="absolute right-2 cursor-pointer" onClick={() => setQuery('')}>
|
||||
<FaTimes className="text-text" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{isPickerVisible && (
|
||||
<>
|
||||
{loading && (
|
||||
<div className="absolute border bg-mantle p-2 w-full mt-1 text-center">
|
||||
Loading...
|
||||
</div>
|
||||
)}
|
||||
{!loading && notFound && (
|
||||
<div className="absolute border bg-mantle p-2 w-full mt-1 text-center text-red-500">
|
||||
No results found
|
||||
</div>
|
||||
)}
|
||||
{suggestions.length > 0 && (
|
||||
<ul className="absolute border bg-mantle w-full mt-1">
|
||||
{suggestions.map((item) => (
|
||||
<li
|
||||
key={item.symbol}
|
||||
onClick={() => handleSelect(item.symbol)}
|
||||
className="p-2 hover:bg-crust cursor-pointer"
|
||||
>
|
||||
{item.description} ({item.symbol})
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchBar
|
||||
export default SearchBar;
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { FaMoon, FaSun } from 'react-icons/fa';
|
||||
|
||||
const ThemeSwitcher = () => {
|
||||
const [theme, setTheme] = useState('light');
|
||||
|
@ -18,9 +19,11 @@ const ThemeSwitcher = () => {
|
|||
return (
|
||||
<button
|
||||
onClick={handleThemeToggle}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md"
|
||||
className="px-4 py-2 bg-text text-crust rounded-md transition-all hover:bg-overlay1 focus:outline-none"
|
||||
>
|
||||
O
|
||||
{
|
||||
theme === 'light' ? <FaSun /> : <FaMoon />
|
||||
}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -39,4 +39,7 @@
|
|||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
Loading…
Reference in a new issue