Skip to content

Commit

Permalink
filter_options&optimized_youtube_parser
Browse files Browse the repository at this point in the history
  • Loading branch information
MetinVn committed Aug 12, 2024
1 parent df442e5 commit 053f61c
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 79 deletions.
2 changes: 2 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
<script data-collect-dnt="true" async defer src="https://scripts.simpleanalyticscdn.com/latest.js"></script>
<noscript><img src="https://queue.simpleanalyticscdn.com/noscript.gif?collect-dnt=true" alt="" referrerpolicy="no-referrer-when-downgrade" /></noscript>
</body>
</html>
36 changes: 6 additions & 30 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
},
"dependencies": {
"axios": "^1.5.0",
"framer-motion": "^11.3.24",
"idb": "^8.0.0",
"lodash": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^5.2.1",
Expand Down
46 changes: 29 additions & 17 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { useEffect, useRef, useState } from "react";
import axios from "axios";
import { toast, ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";

import { youtube_parser } from "./utils/YoutubeParser";
import { getAllMP3s, saveMP3 } from "./utils/IndexedDB";

import Header from "./components/Header";
import LoadingAnimation from "./components/LoadingAnimation";
import Button from "./components/Button";
import Input from "./components/Inputfield";
import { toast, ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import Dashboard from "./components/Dashboard";
import ResultLink from "./components/ResultLink";
import { getAllMP3s, saveMP3 } from "./utils/IndexedDB";

function App() {
const inputUrl = useRef();
Expand Down Expand Up @@ -71,23 +73,33 @@ function App() {

try {
const response = await axios(options);
setTimeout(async () => {
const mp3Data = {
title: response.data.title,
url: response.data.link,
};

if (!mp3Data.url) {
toast.error(
"The conversion was successful, but no download link was provided. Please try again."
);
setLoading(false);
const mp3Data = {
title: response.data.title,
url: response.data.link,
};
setResult(mp3Data.url);
setTitle(mp3Data.title);
setMp3List((prevList) => ({
...prevList,
[mp3Data.url]: mp3Data,
}));
await saveMP3(mp3Data);
resolve();
}, 300);
reject(new Error("No download link provided"));
return;
}

setResult(mp3Data.url);
setTitle(mp3Data.title);
setMp3List((prevList) => ({
...prevList,
[mp3Data.url]: mp3Data,
}));

await saveMP3(mp3Data);
setLoading(false);
resolve();
} catch (error) {
setLoading(false);
toast.error("Error occurred during conversion. Please try again.");
reject(error);
console.warn(error);
}
Expand Down
1 change: 0 additions & 1 deletion src/components/Button.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const Button = ({
className = "",
ariaLabel = "",
}) => {
// Use a fallback for aria-label if none is provided and handle children being null
const accessibleLabel =
ariaLabel || (typeof children === "string" ? children : "Button");

Expand Down
72 changes: 58 additions & 14 deletions src/components/Dashboard.jsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,57 @@
import { useState } from "react";
import { useEffect, useRef, useState } from "react";
import "react-toastify/dist/ReactToastify.css";
import { motion } from "framer-motion";
import Button from "./Button";
import { FiMenu } from "react-icons/fi";
import { debounce } from "lodash";

import { clearMP3Store } from "../utils/IndexedDB";
import Button from "./Button";
import Input from "./Inputfield";

const Dashboard = ({ mp3List, setMp3List, toast, ToastContainer }) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [filteredMP3s, setFilteredMP3s] = useState({});
const dashRef = useRef();
const menuRef = useRef();

const handleClickOutsideMenu = (e) => {
if (menuRef.current && !menuRef.current.contains(e.target)) {
setIsMenuOpen(false);
}
};

useEffect(() => {
document.addEventListener("touchstart", handleClickOutsideMenu);
return () => {
document.removeEventListener("touchstart", handleClickOutsideMenu);
};
}, []);

const handleSearch = debounce((term) => {
if (term.trim() === "") {
setFilteredMP3s(mp3List);
} else {
const filtered = Object.keys(mp3List).reduce((acc, key) => {
if (mp3List[key].title.toLowerCase().includes(term.toLowerCase())) {
acc[key] = mp3List[key];
}
return acc;
}, {});
setFilteredMP3s(filtered);
}
}, 300);

useEffect(() => {
handleSearch(searchTerm);
}, [searchTerm, mp3List]);

const handleClearList = async () => {
if (Object.keys(mp3List).length === 0) {
toast.info("The list is already empty.");
} else {
await clearMP3Store();
setMp3List({});
setFilteredMP3s({});
toast.info("List cleared.");
}
};
Expand All @@ -28,41 +67,46 @@ const Dashboard = ({ mp3List, setMp3List, toast, ToastContainer }) => {

return (
<div
ref={menuRef}
className={`fixed top-2 left-2 h-screen overflow-hidden ${
isMenuOpen ? "w-72" : "w-0"
} transition-all duration-300 bg-white dark:bg-[#1E1E1E]`}>
<motion.div
initial={{ x: -80 }}
animate={{ x: isMenuOpen ? 64 : 0 }}
transition={{ duration: 0.5 }}
className="z-50 fixed top-2 left-2">
<div className="z-50 fixed top-2 left-2">
<Button
children={null}
onClick={handleMenuToggle}
ariaLabel="Burger Menu"
className="px-3 py-1 rounded bg-[#4CAF50] text-white hover:bg-[#388E3C] dark:bg-[#333] dark:text-[#4CAF50] dark:hover:bg-[#555] transition-colors duration-300">
<FiMenu size={24} />
</Button>
</motion.div>
</div>
<div
className={`w-full h-full overflow-y-scroll overflow-x-hidden py-10 max-w-[1000px] mx-auto mt-4 p-4 ${
isMenuOpen ? "opacity-100" : "opacity-0"
} transition-opacity duration-300`}>
<h1 className="text-2xl font-bold text-[#333] dark:text-white mb-4">
MP3 Dashboard
</h1>
{Object.keys(mp3List).length === 0 ? (
<Input
ref={dashRef}
placeholder="Search converted music"
className="text-[#333] my-3 dark:text-white dark:bg-[#333] dark:border-[#444]"
onChange={(e) => setSearchTerm(e.target.value)}
/>
{Object.keys(filteredMP3s).length === 0 ? (
<p className="text-lg text-[#666] dark:text-[#ccc]">
The list is empty. Convert a URL to add it here automatically.
{searchTerm.trim() === ""
? "The list is empty. Convert a URL to add it here automatically."
: "No results found. Try a different search term."}
</p>
) : (
<ul className="space-y-2 text-sm">
{Object.keys(mp3List).map((key) => (
{Object.keys(filteredMP3s).map((key) => (
<li
key={key}
className="flex justify-between items-center p-2 border rounded bg-[#f9f9f9] dark:bg-[#333] dark:border-[#444]">
<span className="text-[#333] dark:text-white">
{mp3List[key].title}
{filteredMP3s[key].title}
</span>
<div>
<Button
Expand All @@ -82,7 +126,7 @@ const Dashboard = ({ mp3List, setMp3List, toast, ToastContainer }) => {
ariaLabel="Clear List"
children={null}
onClick={handleClearList}
className=" py-[4px] bg-[#FF5252] text-white hover:bg-[#E63946] dark:bg-[#555] dark:text-[#FF5252] dark:hover:bg-[#777]">
className="py-[4px] bg-[#FF5252] text-white hover:bg-[#E63946] dark:bg-[#555] dark:text-[#FF5252] dark:hover:bg-[#777]">
Clear List
</Button>
</div>
Expand Down
25 changes: 15 additions & 10 deletions src/components/Header.jsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { setDarkMode, setLightMode } from "./Themes";
import { getTheme, saveTheme } from "../utils/IndexedDB";

const Header = () => {
const [systemTheme, setSystemTheme] = useState("light");

useEffect(() => {
const applySavedTheme = async () => {
const savedTheme = await getTheme();
if (savedTheme) {
if (savedTheme === "dark") {
setDarkMode();
setSystemTheme("dark");
} else {
setLightMode();
setSystemTheme("light");
}
} else {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
setSystemTheme(mediaQuery.matches ? "dark" : "light");
const currentSystemTheme = mediaQuery.matches ? "dark" : "light";
if (currentSystemTheme === "dark") {
setDarkMode();
} else {
setLightMode();
}

const handleChange = (e) => {
setSystemTheme(e.matches ? "dark" : "light");
if (e.matches) {
setDarkMode();
} else {
setLightMode();
}
};

mediaQuery.addEventListener("change", handleChange);
Expand Down Expand Up @@ -56,16 +61,16 @@ const Header = () => {
};

return (
<div className="fixed top-2 w-full flex justify-center gap-4 mb-4">
<div className="fixed text-xs sm:text-sm md:text-lg top-2 w-full flex justify-center gap-4 mb-4">
<button
onClick={handleLightMode}
className="px-3 py-1 rounded dark:text-[#4CAF50] hover:bg-[#999] dark:hover:bg-[#333] transition-colors duration-300">
Light Mode
Light mode
</button>
<button
onClick={handleDarkMode}
className="px-3 py-1 rounded dark:text-[#4CAF50] hover:bg-[#999] dark:hover:bg-[#333] transition-colors duration-300">
Dark Mode
Dark mode
</button>
<button
onClick={handleSystemTheme}
Expand Down
12 changes: 6 additions & 6 deletions src/utils/YoutubeParser.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export function youtube_parser(url) {
const regExp =
/(?:youtu\.be\/|(?:v|e(?:mbed)?)\/|watch\?v=|\/u\/\w\/|embed\/|watch\?.*v=|\/)([a-zA-Z0-9_-]{11})/;
const match = url.match(regExp);
return match ? match[1] : false;
}
export const youtube_parser = (url) => {
const regex =
/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?/\s]{11})/;
const match = url.match(regex);
return match ? match[1] : null;
};

0 comments on commit 053f61c

Please sign in to comment.