mirror of
https://github.com/ryanamay/twinkle.git
synced 2024-09-20 04:00:34 +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 SearchBar from './SearchBar'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import ThemeSwitcher from './ThemeSwitcher'
|
import ThemeSwitcher from './ThemeSwitcher'
|
||||||
|
import { IoSparkles } from 'react-icons/io5'
|
||||||
|
import { FaHamburger } from 'react-icons/fa'
|
||||||
|
import { GiHamburgerMenu } from 'react-icons/gi'
|
||||||
|
|
||||||
interface NavigationBarProps {
|
interface NavigationBarProps {
|
||||||
onSelectSymbol: (symbol: string) => void
|
onSelectSymbol: (symbol: string) => void
|
||||||
|
@ -21,9 +24,9 @@ const NavigationBar = ({ onSelectSymbol }: NavigationBarProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="bg-base text-text">
|
<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-4 px-4 lg:px-8">
|
<div className="container mx-auto max-w-8xl flex items-center justify-between py-1">
|
||||||
<Link className="text-lg font-bold" href="/">TWL
|
<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>
|
</Link>
|
||||||
<div className="flex-grow max-w-md mx-auto lg:max-w-lg w-full">
|
<div className="flex-grow max-w-md mx-auto lg:max-w-lg w-full">
|
||||||
<SearchBar onSelectSymbol={onSelectSymbol} />
|
<SearchBar onSelectSymbol={onSelectSymbol} />
|
||||||
|
@ -34,7 +37,7 @@ const NavigationBar = ({ onSelectSymbol }: NavigationBarProps) => {
|
||||||
onClick={() => setDropdownOpen(!dropdownOpen)}
|
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||||
className="bg-mantle px-4 py-2 rounded focus:outline-none focus:bg-overlay0"
|
className="bg-mantle px-4 py-2 rounded focus:outline-none focus:bg-overlay0"
|
||||||
>
|
>
|
||||||
X
|
<GiHamburgerMenu />
|
||||||
</button>
|
</button>
|
||||||
{dropdownOpen && (
|
{dropdownOpen && (
|
||||||
<div className="absolute right-0 mt-2 w-48 bg-surface0 rounded-md shadow-lg z-20">
|
<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 { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from "lodash.debounce";
|
||||||
|
import { FaSearch, FaTimes } from "react-icons/fa";
|
||||||
|
|
||||||
interface SearchBarProps {
|
interface SearchBarProps {
|
||||||
onSelectSymbol: (symbol: string) => void
|
onSelectSymbol: (symbol: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SearchBar = ({ onSelectSymbol }: SearchBarProps) => {
|
const SearchBar = ({ onSelectSymbol }: SearchBarProps) => {
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState("");
|
||||||
const [suggestions, setSuggestions] = useState<{ symbol: string, description: string }[]>([])
|
const [selectedQuery, setSelectedQuery] = useState("");
|
||||||
const [loading, setLoading] = useState(false)
|
const [suggestions, setSuggestions] = useState<{ symbol: string; description: string }[]>([]);
|
||||||
const [notFound, setNotFound] = useState(false)
|
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) => {
|
const fetchSuggestions = async (query: string) => {
|
||||||
setLoading(true)
|
setLoading(true);
|
||||||
setNotFound(false)
|
setNotFound(false);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/search?query=${query}`)
|
const res = await fetch(`/api/search?query=${query}`);
|
||||||
const data = await res.json()
|
const data = await res.json();
|
||||||
if (data.result && data.result.length > 0) {
|
if (data.result && data.result.length > 0) {
|
||||||
setSuggestions(data.result)
|
setSuggestions(data.result.slice(0, 5));
|
||||||
} else {
|
} else {
|
||||||
setNotFound(true)
|
setNotFound(true);
|
||||||
setSuggestions([])
|
setSuggestions([]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching suggestions:', error)
|
console.error("Error fetching suggestions:", error);
|
||||||
setNotFound(true)
|
setNotFound(true);
|
||||||
setSuggestions([])
|
setSuggestions([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const debouncedFetchSuggestions = useCallback(debounce((query: string) => {
|
const debouncedFetchSuggestions = useCallback(debounce((query: string) => {
|
||||||
fetchSuggestions(query)
|
fetchSuggestions(query);
|
||||||
}, 300), [])
|
}, 300), []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (query.length > 1) {
|
if (query.length > 1) {
|
||||||
debouncedFetchSuggestions(query)
|
debouncedFetchSuggestions(query);
|
||||||
|
setIsPickerVisible(true);
|
||||||
} else {
|
} else {
|
||||||
setSuggestions([])
|
setSuggestions([]);
|
||||||
setNotFound(false)
|
setNotFound(false);
|
||||||
|
setIsPickerVisible(false);
|
||||||
}
|
}
|
||||||
}, [query, debouncedFetchSuggestions])
|
}, [query, debouncedFetchSuggestions]);
|
||||||
|
|
||||||
const handleSelect = (symbol: string) => {
|
const handleSelect = (symbol: string) => {
|
||||||
setQuery('')
|
setQuery("");
|
||||||
setSuggestions([])
|
setSelectedQuery(symbol);
|
||||||
onSelectSymbol(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 (
|
return (
|
||||||
<div className="relative">
|
<div className="relative w-full" ref={pickerRef}>
|
||||||
<input
|
<div className="relative flex items-center border p-1 w-full border-none md:bg-mantle md:border-surface0 md:rounded-md">
|
||||||
type="text"
|
<input
|
||||||
placeholder="Search stocks..."
|
type="text"
|
||||||
value={query}
|
placeholder={selectedQuery || "Search stocks..."}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
value={query}
|
||||||
className="border p-2 w-full"
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
/>
|
className="pl-8 w-full bg-transparent outline-none text-text"
|
||||||
{loading && (
|
onFocus={() => setIsPickerVisible(true)}
|
||||||
<div className="absolute border bg-white p-2 w-full mt-1 text-center">
|
/>
|
||||||
Loading...
|
<span className="absolute left-2 text-surface0">
|
||||||
</div>
|
<FaSearch className="text-text" />
|
||||||
)}
|
</span>
|
||||||
{!loading && notFound && (
|
{query && (
|
||||||
<div className="absolute border bg-white p-2 w-full mt-1 text-center text-red-500">
|
<span className="absolute right-2 cursor-pointer" onClick={() => setQuery('')}>
|
||||||
No results found
|
<FaTimes className="text-text" />
|
||||||
</div>
|
</span>
|
||||||
)}
|
)}
|
||||||
{suggestions.length > 0 && (
|
</div>
|
||||||
<ul className="absolute border bg-white w-full mt-1">
|
{isPickerVisible && (
|
||||||
{suggestions.map((item) => (
|
<>
|
||||||
<li
|
{loading && (
|
||||||
key={item.symbol}
|
<div className="absolute border bg-mantle p-2 w-full mt-1 text-center">
|
||||||
onClick={() => handleSelect(item.symbol)}
|
Loading...
|
||||||
className="p-2 hover:bg-gray-200 cursor-pointer"
|
</div>
|
||||||
>
|
)}
|
||||||
{item.description} ({item.symbol})
|
{!loading && notFound && (
|
||||||
</li>
|
<div className="absolute border bg-mantle p-2 w-full mt-1 text-center text-red-500">
|
||||||
))}
|
No results found
|
||||||
</ul>
|
</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>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default SearchBar
|
export default SearchBar;
|
|
@ -1,4 +1,5 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { FaMoon, FaSun } from 'react-icons/fa';
|
||||||
|
|
||||||
const ThemeSwitcher = () => {
|
const ThemeSwitcher = () => {
|
||||||
const [theme, setTheme] = useState('light');
|
const [theme, setTheme] = useState('light');
|
||||||
|
@ -18,9 +19,11 @@ const ThemeSwitcher = () => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={handleThemeToggle}
|
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>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -39,4 +39,7 @@
|
||||||
body {
|
body {
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
color: var(--foreground);
|
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