additional nav bar items, lyricsearch.jsx changes/cleanup + bugfix for autocomplete scrolling & change to primereact theme (bootstrap4-dark-blue)
This commit is contained in:
		@@ -11,6 +11,7 @@ import Alert from '@mui/joy/Alert';
 | 
			
		||||
import Box from '@mui/joy/Box';
 | 
			
		||||
import Button from "@mui/joy/Button";
 | 
			
		||||
import Checkbox from "@mui/joy/Checkbox";
 | 
			
		||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
 | 
			
		||||
import jQuery from "jquery";
 | 
			
		||||
import { AutoComplete } from 'primereact/autocomplete';
 | 
			
		||||
import { api as API_URL } from '../config';
 | 
			
		||||
@@ -63,171 +64,189 @@ export function LyricSearchInputField(opts = {}) {
 | 
			
		||||
  const [suggestions, setSuggestions] = useState([]);
 | 
			
		||||
  const [showAlert, setShowAlert] = useState(false);
 | 
			
		||||
  const autoCompleteRef = useRef(null);
 | 
			
		||||
  
 | 
			
		||||
  var search_toast = null;
 | 
			
		||||
  var ret_artist = null;
 | 
			
		||||
  var ret_song = null;
 | 
			
		||||
  var ret_lyrics = null;
 | 
			
		||||
  var start_time = null;
 | 
			
		||||
  var end_time = null;
 | 
			
		||||
  
 | 
			
		||||
  useEffect(() => {}, []);
 | 
			
		||||
  
 | 
			
		||||
  async function handleSearch() {
 | 
			
		||||
 | 
			
		||||
  // Ensure the dropdown panel is scrollable after it shows
 | 
			
		||||
  const handlePanelShow = () => {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      const panel = document.querySelector(".p-autocomplete-panel");
 | 
			
		||||
      const items = panel?.querySelector(".p-autocomplete-items");
 | 
			
		||||
 | 
			
		||||
      if (!items) return;
 | 
			
		||||
 | 
			
		||||
      items.style.maxHeight = "200px";
 | 
			
		||||
      items.style.overflowY = "auto";
 | 
			
		||||
      items.style.overscrollBehavior = "contain";
 | 
			
		||||
 | 
			
		||||
      // ✅ Attach wheel scroll manually
 | 
			
		||||
      const wheelHandler = (e) => {
 | 
			
		||||
        const delta = e.deltaY;
 | 
			
		||||
        const atTop = items.scrollTop === 0;
 | 
			
		||||
        const atBottom = items.scrollTop + items.clientHeight >= items.scrollHeight;
 | 
			
		||||
 | 
			
		||||
        if ((delta < 0 && atTop) || (delta > 0 && atBottom)) {
 | 
			
		||||
          e.preventDefault(); // prevent outer scroll
 | 
			
		||||
        } else {
 | 
			
		||||
          e.stopPropagation(); // prevent parent scroll
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      // Clean up first, then re-add
 | 
			
		||||
      items.removeEventListener('wheel', wheelHandler);
 | 
			
		||||
      items.addEventListener('wheel', wheelHandler, { passive: false });
 | 
			
		||||
 | 
			
		||||
      // Cleanup on hide
 | 
			
		||||
      const observer = new MutationObserver(() => {
 | 
			
		||||
        if (!document.body.contains(items)) {
 | 
			
		||||
          items.removeEventListener('wheel', wheelHandler);
 | 
			
		||||
          observer.disconnect();
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      observer.observe(document.body, { childList: true, subtree: true });
 | 
			
		||||
    }, 0);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  const typeahead_search = (event) => {
 | 
			
		||||
    const query = event.query;
 | 
			
		||||
    $.ajax({
 | 
			
		||||
      url: `${API_URL}/typeahead/lyrics`,
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      contentType: 'application/json; charset=utf-8',
 | 
			
		||||
      data: JSON.stringify({ query }),
 | 
			
		||||
      dataType: 'json',
 | 
			
		||||
      success: setSuggestions
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleSearch = () => {
 | 
			
		||||
    if (autoCompleteRef.current) {
 | 
			
		||||
      autoCompleteRef.current.hide();
 | 
			
		||||
    }
 | 
			
		||||
    let validSearch = (value.trim() && (value.split(" - ").length > 1));
 | 
			
		||||
    let box = document.querySelector("[class*='lyrics-card-']")
 | 
			
		||||
    let spinner = $('#spinner');
 | 
			
		||||
    let excluded_sources = [];
 | 
			
		||||
    
 | 
			
		||||
    $("#exclude-checkboxes").find("input:checkbox").each(function () {
 | 
			
		||||
      if (this.checked) {
 | 
			
		||||
        let src = this.id.replace("excl-", "").toLowerCase();
 | 
			
		||||
        excluded_sources.push(src);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    if (validSearch) { 
 | 
			
		||||
      // define artist, song + additional checks
 | 
			
		||||
      let a_s_split = value.split(" - ", 2)
 | 
			
		||||
      var [search_artist, search_song] = a_s_split;
 | 
			
		||||
      search_artist = search_artist.trim();
 | 
			
		||||
      search_song = search_song.trim();
 | 
			
		||||
      if (! search_artist || ! search_song) {
 | 
			
		||||
        validSearch = false; // artist and song could not be derived
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    const validSearch = value.includes(" - ");
 | 
			
		||||
    if (!validSearch) {
 | 
			
		||||
      setShowAlert(true);
 | 
			
		||||
      setTimeout(() => { setShowAlert(false); }, 5000);
 | 
			
		||||
      $("#alert").removeClass("hidden");
 | 
			
		||||
      setTimeout(() => setShowAlert(false), 5000);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!$("#alert").hasClass("hidden")) {
 | 
			
		||||
      setShowAlert(false);
 | 
			
		||||
      $("#alert").addClass("hidden");
 | 
			
		||||
 | 
			
		||||
    const [rawArtist, rawSong] = value.split(" - ", 2);
 | 
			
		||||
    const search_artist = rawArtist?.trim();
 | 
			
		||||
    const search_song = rawSong?.trim();
 | 
			
		||||
 | 
			
		||||
    if (!search_artist || !search_song) {
 | 
			
		||||
      setShowAlert(true);
 | 
			
		||||
      setTimeout(() => setShowAlert(false), 5000);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    $('#spinner').removeClass("hidden");
 | 
			
		||||
    $(box).addClass("hidden");
 | 
			
		||||
    //setTimeout(() => { $("#spinner").addClass("hidden"); alert('Not yet implemented.')}, 1000);
 | 
			
		||||
    search_toast = toast.info("Searching...", {style: { color: '#000000', backgroundColor: 'rgba(217, 242, 255, 0.8)'}});
 | 
			
		||||
    start_time = new Date().getTime()
 | 
			
		||||
 | 
			
		||||
    const box = $("[class*='lyrics-card-']");
 | 
			
		||||
    const lyrics_content = $(".lyrics-content");
 | 
			
		||||
    const spinner = $("#spinner");
 | 
			
		||||
    const excluded_sources = [];
 | 
			
		||||
 | 
			
		||||
    $("#exclude-checkboxes input:checked").each(function () {
 | 
			
		||||
      excluded_sources.push(this.id.replace("excl-", "").toLowerCase());
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    setShowAlert(false);
 | 
			
		||||
    $("#alert").addClass("hidden");
 | 
			
		||||
    spinner.removeClass("hidden");
 | 
			
		||||
    box.addClass("hidden");
 | 
			
		||||
 | 
			
		||||
    const start_time = Date.now();
 | 
			
		||||
    const search_toast = toast.info("Searching...", {
 | 
			
		||||
      style: { color: '#000', backgroundColor: 'rgba(217, 242, 255, 0.8)' }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $.ajax({
 | 
			
		||||
      url: API_URL+'/lyric/search',
 | 
			
		||||
      url: `${API_URL}/lyric/search`,
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      contentType: 'application/json; charset=utf-8',
 | 
			
		||||
      data: JSON.stringify({
 | 
			
		||||
        a: search_artist,
 | 
			
		||||
        s: search_song,
 | 
			
		||||
        excluded_sources: excluded_sources,
 | 
			
		||||
        excluded_sources,
 | 
			
		||||
        src: 'Web',
 | 
			
		||||
        extra: true,
 | 
			
		||||
      })
 | 
			
		||||
    }).done((data, txtStatus, xhr) => {
 | 
			
		||||
    }).done((data) => {
 | 
			
		||||
      spinner.addClass("hidden");
 | 
			
		||||
 | 
			
		||||
      if (data.err || !data.lyrics) {
 | 
			
		||||
        $(spinner).addClass("hidden");
 | 
			
		||||
        return toast.update(search_toast, {
 | 
			
		||||
          type: "",
 | 
			
		||||
          render: `🙁 ${data.errorText}`,
 | 
			
		||||
          style: { backgroundColor: "rgba(255, 0, 0, 0.5)", color: 'inherit' },
 | 
			
		||||
          type: "",
 | 
			
		||||
          style: { backgroundColor: "rgba(255, 0, 0, 0.5)" },
 | 
			
		||||
          hideProgressBar: true,
 | 
			
		||||
          autoClose: 5000,
 | 
			
		||||
        })
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      end_time = new Date().getTime();
 | 
			
		||||
      let duration = (end_time - start_time) / 1000;
 | 
			
		||||
      ret_artist = data.artist;
 | 
			
		||||
      ret_song = data.song;
 | 
			
		||||
      ret_lyrics = data.lyrics;
 | 
			
		||||
      $(box).removeClass("hidden");
 | 
			
		||||
      $(spinner).addClass("hidden");
 | 
			
		||||
      $(box).html(`<span id='lyrics-info'>${ret_artist} - ${ret_song}</span>${ret_lyrics}`);
 | 
			
		||||
 | 
			
		||||
      const duration = ((Date.now() - start_time) / 1000).toFixed(1);
 | 
			
		||||
      lyrics_content.html(`<span id='lyrics-info'>${data.artist} - ${data.song}</span>${data.lyrics}`);
 | 
			
		||||
      box.removeClass("hidden");
 | 
			
		||||
 | 
			
		||||
      toast.update(search_toast, {
 | 
			
		||||
        type: "",
 | 
			
		||||
        style: { backgroundColor: "rgba(46, 186, 106, 1)", color: 'inherit'  },
 | 
			
		||||
        render: `🦄 Found! (Took ${duration}s)`,
 | 
			
		||||
        autoClose: 2000,
 | 
			
		||||
        hideProgressBar: true
 | 
			
		||||
      });
 | 
			
		||||
    }).fail((jqXHR, textStatus, error) => {
 | 
			
		||||
      $(spinner).addClass("hidden");
 | 
			
		||||
      let render_text = `😕 Failed to reach search endpoint (${jqXHR.status})`;
 | 
			
		||||
      if (typeof jqXHR.responseJSON.detail !== "undefined") {
 | 
			
		||||
        render_text += `\n${jqXHR.responseJSON.detail}`;
 | 
			
		||||
      } 
 | 
			
		||||
      return toast.update(search_toast, {
 | 
			
		||||
        type: "",
 | 
			
		||||
        render: render_text,
 | 
			
		||||
        style: { backgroundColor: "rgba(255, 0, 0, 0.5)", color: 'inherit' },
 | 
			
		||||
        style: { backgroundColor: "rgba(46, 186, 106, 1)" },
 | 
			
		||||
        autoClose: 2000,
 | 
			
		||||
        hideProgressBar: true,
 | 
			
		||||
      });
 | 
			
		||||
    }).fail((jqXHR) => {
 | 
			
		||||
      spinner.addClass("hidden");
 | 
			
		||||
      const msg = `😕 Failed to reach search endpoint (${jqXHR.status})` +
 | 
			
		||||
        (jqXHR.responseJSON?.detail ? `\n${jqXHR.responseJSON.detail}` : "");
 | 
			
		||||
      toast.update(search_toast, {
 | 
			
		||||
        render: msg,
 | 
			
		||||
        type: "",
 | 
			
		||||
        style: { backgroundColor: "rgba(255, 0, 0, 0.5)" },
 | 
			
		||||
        hideProgressBar: true,
 | 
			
		||||
        autoClose: 5000,
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleKeyDown = (e) => {
 | 
			
		||||
    if (e.key !== "Enter") return;
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    handleSearch();
 | 
			
		||||
    if (e.key === "Enter") {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      handleSearch();
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  
 | 
			
		||||
  const typeahead_search = (event) => {
 | 
			
		||||
    let query = event.query;
 | 
			
		||||
    $.ajax({
 | 
			
		||||
      url: API_URL+'/typeahead/lyrics',
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      contentType: 'application/json; charset=utf-8',
 | 
			
		||||
      data: JSON.stringify({
 | 
			
		||||
        query: query
 | 
			
		||||
      }),
 | 
			
		||||
      dataType: 'json',
 | 
			
		||||
      success: function (json) {
 | 
			
		||||
        return setSuggestions(json);
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  };
 | 
			
		||||
  
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
    <div id="alert">
 | 
			
		||||
    {showAlert && (
 | 
			
		||||
      <Alert
 | 
			
		||||
      color="danger"
 | 
			
		||||
      variant="solid"
 | 
			
		||||
      onClose={() => setShowAlert(false)}
 | 
			
		||||
      >
 | 
			
		||||
      You must specify both an artist and song to search.
 | 
			
		||||
      <br />
 | 
			
		||||
      Format: Artist - Song
 | 
			
		||||
      </Alert>
 | 
			
		||||
    )}
 | 
			
		||||
    </div>
 | 
			
		||||
    <AutoComplete
 | 
			
		||||
    theme="nano"
 | 
			
		||||
    size="40"
 | 
			
		||||
    scrollHeight="200px"
 | 
			
		||||
    autoFocus={true}
 | 
			
		||||
    virtualScrollerOptions={false}
 | 
			
		||||
    value={value}
 | 
			
		||||
    id={opts.id}
 | 
			
		||||
    ref={autoCompleteRef}
 | 
			
		||||
    suggestions={suggestions}
 | 
			
		||||
    completeMethod={typeahead_search}
 | 
			
		||||
    placeholder={opts.placeholder}
 | 
			
		||||
    onChange={(e) => { setValue(e.target.value)  }}
 | 
			
		||||
    onKeyDown={handleKeyDown}
 | 
			
		||||
    />
 | 
			
		||||
    <Button id="lyric-search-btn" onClick={handleSearch} className={"btn"}>
 | 
			
		||||
    Search
 | 
			
		||||
    </Button>
 | 
			
		||||
      {showAlert && (
 | 
			
		||||
        <Alert
 | 
			
		||||
          color="danger"
 | 
			
		||||
          variant="solid"
 | 
			
		||||
          onClose={() => setShowAlert(false)}
 | 
			
		||||
        >
 | 
			
		||||
          You must specify both an artist and song to search.
 | 
			
		||||
          <br />
 | 
			
		||||
          Format: Artist - Song
 | 
			
		||||
        </Alert>
 | 
			
		||||
      )}
 | 
			
		||||
      <AutoComplete
 | 
			
		||||
        id={opts.id}
 | 
			
		||||
        ref={autoCompleteRef}
 | 
			
		||||
        value={value}
 | 
			
		||||
        size={40}
 | 
			
		||||
        suggestions={suggestions}
 | 
			
		||||
        completeMethod={typeahead_search}
 | 
			
		||||
        onChange={(e) => setValue(e.target.value)}
 | 
			
		||||
        onKeyDown={handleKeyDown}
 | 
			
		||||
        onShow={handlePanelShow}
 | 
			
		||||
        placeholder={opts.placeholder}
 | 
			
		||||
        autoFocus      />
 | 
			
		||||
      <Button onClick={handleSearch} className="btn">Search</Button>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export const UICheckbox = forwardRef(function UICheckbox(opts = {}, ref) {
 | 
			
		||||
  const [checked, setChecked] = useState(false);
 | 
			
		||||
  const [showAlert, setShowAlert] = useState(false);
 | 
			
		||||
@@ -271,6 +290,11 @@ export const UICheckbox = forwardRef(function UICheckbox(opts = {}, ref) {
 | 
			
		||||
 | 
			
		||||
export function LyricResultBox(opts={}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <Box className={`lyrics-card lyrics-card-${theme} hidden`} sx={{ p: 2 }}></Box>
 | 
			
		||||
    <div>
 | 
			
		||||
      <Box className={`lyrics-card lyrics-card-${theme} hidden`} sx={{ p: 2 }}>
 | 
			
		||||
        <div className='lyrics-content'></div>
 | 
			
		||||
        {/* <ContentCopyIcon className='lyrics-card-copyButton' size='lg' /> */}
 | 
			
		||||
      </Box>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -6,10 +6,14 @@ import HorizontalRuleIcon from '@mui/icons-material/HorizontalRule';
 | 
			
		||||
 | 
			
		||||
const navItems = {
 | 
			
		||||
  "/": { name: "Home", className: "", icon: null },
 | 
			
		||||
  "": { name: "", className: "", icon: HorizontalRuleIcon },
 | 
			
		||||
  "divider-1": { name: "", className: "", icon: HorizontalRuleIcon },
 | 
			
		||||
  "/radio": { name: "Radio", className: "", icon: null },
 | 
			
		||||
  "": { name: "", className: "", icon: HorizontalRuleIcon },
 | 
			
		||||
  "https://status.boatson.boats": { name: "Status", className: "", icon: ExitToApp }
 | 
			
		||||
  "divider-2": { name: "", className: "", icon: HorizontalRuleIcon },
 | 
			
		||||
  "https://status.boatson.boats": { name: "Status", className: "", icon: ExitToApp },
 | 
			
		||||
  "divider-3": { name: "", className: "", icon: HorizontalRuleIcon },
 | 
			
		||||
  "https://kode.boatson.boats": { name: "Git", className: "", icon: ExitToApp },
 | 
			
		||||
  "divider-4": { name: "", className: "", icon: HorizontalRuleIcon },
 | 
			
		||||
  "https://old.codey.lol": { name: "Old Site", className: "", icon: ExitToApp },
 | 
			
		||||
};
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user