Theming added
@@ -6,6 +6,7 @@
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.0.0",
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
"@windmill/react-ui": "^0.6.0",
|
||||
"autoprefixer": "^10.4.4",
|
||||
"hls.js": "^1.1.5",
|
||||
"postcss": "^8.4.12",
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
import React from "react";
|
||||
import { Sidebar } from "./components";
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import { HomePage, ChannelPage, PlayerPage } from "./pages";
|
||||
import { Layout } from "./containers";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="flex flex-col h-screen font-Rampart">
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<Sidebar />
|
||||
<div className="flex flex-col flex-1">
|
||||
<div className="flex h-16 p-4 bg-gray-100">Header</div>
|
||||
<main className="flex flex-1 overflow-y-auto">
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="live/channel/:channelId" element={<ChannelPage />} />
|
||||
<Route path="live/play/:streamId" element={<PlayerPage />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/*" element={<Layout />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
157
frontend/src/components/header.component.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Input,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
WindmillContext,
|
||||
} from "@windmill/react-ui";
|
||||
import { SidebarContext } from "../context";
|
||||
import {
|
||||
BellIcon,
|
||||
MenuIcon,
|
||||
MoonIcon,
|
||||
OutlineCogIcon,
|
||||
OutlineLogoutIcon,
|
||||
OutlinePersonIcon,
|
||||
SearchIcon,
|
||||
SunIcon,
|
||||
} from "../icons";
|
||||
|
||||
const Header = () => {
|
||||
const { mode, toggleMode } = React.useContext(WindmillContext);
|
||||
const { toggleSidebar } = React.useContext(SidebarContext);
|
||||
|
||||
const [isNotificationsMenuOpen, setIsNotificationsMenuOpen] =
|
||||
React.useState(false);
|
||||
const [isProfileMenuOpen, setIsProfileMenuOpen] = React.useState(false);
|
||||
|
||||
const handleNotificationsClick = () => {
|
||||
setIsNotificationsMenuOpen(!isNotificationsMenuOpen);
|
||||
};
|
||||
|
||||
const handleProfileClick = () => {
|
||||
setIsProfileMenuOpen(!isProfileMenuOpen);
|
||||
};
|
||||
return (
|
||||
<header className="z-40 py-4 bg-white shadow-bottom dark:bg-gray-800">
|
||||
<div className="container flex items-center justify-between h-full px-6 mx-auto text-purple-600 dark:text-purple-300">
|
||||
{/* <!-- Mobile hamburger --> */}
|
||||
<button
|
||||
className="p-1 mr-5 -ml-1 rounded-md lg:hidden focus:outline-none focus:shadow-outline-purple"
|
||||
onClick={toggleSidebar}
|
||||
aria-label="Menu"
|
||||
>
|
||||
<MenuIcon className="w-6 h-6" aria-hidden="true" />
|
||||
</button>
|
||||
{/* <!-- Search input --> */}
|
||||
<div className="flex justify-center flex-1 lg:mr-32">
|
||||
<div className="relative w-full max-w-xl mr-6 focus-within:text-purple-500">
|
||||
<div className="absolute inset-y-0 flex items-center pl-2">
|
||||
<SearchIcon className="w-4 h-4" aria-hidden="true" />
|
||||
</div>
|
||||
<Input
|
||||
css=""
|
||||
className="pl-8 text-gray-700"
|
||||
placeholder="Search for projects"
|
||||
aria-label="Search"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="flex items-center flex-shrink-0 space-x-6">
|
||||
{/* <!-- Theme toggler --> */}
|
||||
<li className="flex">
|
||||
<button
|
||||
className="rounded-md focus:outline-none focus:shadow-outline-purple"
|
||||
onClick={toggleMode}
|
||||
aria-label="Toggle color mode"
|
||||
>
|
||||
{mode === "dark" ? (
|
||||
<SunIcon className="w-5 h-5" aria-hidden="true" />
|
||||
) : (
|
||||
<MoonIcon className="w-5 h-5" aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
{/* <!-- Notifications menu --> */}
|
||||
<li className="relative">
|
||||
<button
|
||||
className="relative align-middle rounded-md focus:outline-none focus:shadow-outline-purple"
|
||||
onClick={handleNotificationsClick}
|
||||
aria-label="Notifications"
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<BellIcon className="w-5 h-5" aria-hidden="true" />
|
||||
{/* <!-- Notification badge --> */}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="absolute top-0 right-0 inline-block w-3 h-3 transform translate-x-1 -translate-y-1 bg-red-600 border-2 border-white rounded-full dark:border-gray-800"
|
||||
></span>
|
||||
</button>
|
||||
|
||||
<Dropdown
|
||||
align="right"
|
||||
isOpen={isNotificationsMenuOpen}
|
||||
onClose={() => setIsNotificationsMenuOpen(false)}
|
||||
>
|
||||
<DropdownItem tag="a" href="#" className="justify-between">
|
||||
<span>Messages</span>
|
||||
<Badge type="danger">13</Badge>
|
||||
</DropdownItem>
|
||||
<DropdownItem tag="a" href="#" className="justify-between">
|
||||
<span>Sales</span>
|
||||
<Badge type="danger">2</Badge>
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={() => alert("Alerts!")}>
|
||||
<span>Alerts</span>
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</li>
|
||||
{/* <!-- Profile menu --> */}
|
||||
<li className="relative">
|
||||
<button
|
||||
className="rounded-full focus:shadow-outline-purple focus:outline-none"
|
||||
onClick={handleProfileClick}
|
||||
aria-label="Account"
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<Avatar
|
||||
className="align-middle"
|
||||
src="https://images.unsplash.com/photo-1502378735452-bc7d86632805?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&s=aa3a807e1bbdfd4364d1f449eaa96d82"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
<Dropdown
|
||||
align="right"
|
||||
isOpen={isProfileMenuOpen}
|
||||
onClose={() => setIsProfileMenuOpen(false)}
|
||||
>
|
||||
<DropdownItem tag="a" href="#">
|
||||
<OutlinePersonIcon
|
||||
className="w-4 h-4 mr-3"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>Profile</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem tag="a" href="#">
|
||||
<OutlineCogIcon className="w-4 h-4 mr-3" aria-hidden="true" />
|
||||
<span>Settings</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={() => alert("Log out!")}>
|
||||
<OutlineLogoutIcon
|
||||
className="w-4 h-4 mr-3"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>Log out</span>
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -1,6 +1,7 @@
|
||||
import HLSPlayer from "./hls-player.component";
|
||||
import Navbar from "./navbar.component";
|
||||
import Sidebar from "./sidebar.component";
|
||||
import Sidebar from "./sidebar/sidebar-content.component";
|
||||
import EPGComponent from "./epg.component";
|
||||
import ThemedSuspence from "./themed-suspence.component";
|
||||
|
||||
export { Navbar, Sidebar, HLSPlayer, EPGComponent };
|
||||
export { Navbar, Sidebar, HLSPlayer, EPGComponent, ThemedSuspence };
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import React from "react";
|
||||
import { Link, NavLink } from "react-router-dom";
|
||||
import { Channel } from "../models/channel";
|
||||
|
||||
const Sidebar = () => {
|
||||
const [channels, setChannels] = React.useState<Channel[]>([]);
|
||||
const [filteredChannels, setFilteredChannels] = React.useState<Channel[]>([]);
|
||||
React.useEffect(() => {
|
||||
const fetchChannels = async () => {
|
||||
const res = await fetch(`${process.env.REACT_APP_API_URL}/channels`);
|
||||
const data = await res.json();
|
||||
setChannels(data);
|
||||
setFilteredChannels(data);
|
||||
};
|
||||
|
||||
fetchChannels().catch(console.error);
|
||||
}, []);
|
||||
const _searchChannels = ($event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const searchString = $event.target.value;
|
||||
if (searchString) {
|
||||
const filteredChannels = channels.filter((c) => {
|
||||
const result = c.category_name
|
||||
.toLowerCase()
|
||||
.includes(searchString.toLowerCase());
|
||||
console.log(
|
||||
"sidebar.component",
|
||||
`Category Name: ${c.category_name}`,
|
||||
`Search String: ${searchString}`
|
||||
);
|
||||
console.log("sidebar.component", "Result", result);
|
||||
return result;
|
||||
});
|
||||
setFilteredChannels(filteredChannels);
|
||||
} else {
|
||||
setFilteredChannels(channels);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<aside className="flex flex-col w-1/5 pl-2 bg-gray-100">
|
||||
<div className="flex items-center justify-center h-16">
|
||||
<div className="flex p-8">
|
||||
<input
|
||||
type="text"
|
||||
className="px-4 py-2"
|
||||
placeholder="Search..."
|
||||
onChange={_searchChannels}
|
||||
/>
|
||||
<button className="flex items-center justify-center px-4 border-l">
|
||||
<svg
|
||||
className="w-6 h-6 text-gray-600"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M16.32 14.9l5.39 5.4a1 1 0 0 1-1.42 1.4l-5.38-5.38a8 8 0 1 1 1.41-1.41zM10 16a6 6 0 1 0 0-12 6 6 0 0 0 0 12z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="overflow-y-auto">
|
||||
<li>
|
||||
{filteredChannels.map((channel: Channel) => (
|
||||
<NavLink
|
||||
key={channel.category_id}
|
||||
to={`/live/channel/${channel.category_id}`}
|
||||
className={({ isActive }) => (isActive ? "bg-gray-500" : "bg-red-300")}
|
||||
>
|
||||
<div className="intro-x ">
|
||||
<div className="flex items-center px-5 py-3 mb-3 box zoom-in">
|
||||
<div className="flex-none w-10 h-10 overflow-hidden rounded-full image-fit">
|
||||
<svg
|
||||
className="w-12 h-12 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z" />
|
||||
<path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-4 mr-auto">
|
||||
<div className="font-semibold text-gray-800">{channel.category_name}</div>
|
||||
<div className="text-slate-500 text-xs mt-0.5">
|
||||
9 September 2022
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-success">+$24</div>
|
||||
</div>
|
||||
</div>
|
||||
</NavLink>
|
||||
// <Link
|
||||
// key={channel.category_id}
|
||||
// to={`/live/channel/${channel.category_id}`}
|
||||
// className="flex items-center p-2 text-base font-normal text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
// >
|
||||
// <svg
|
||||
// className="w-6 h-6 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white"
|
||||
// fill="currentColor"
|
||||
// viewBox="0 0 20 20"
|
||||
// xmlns="http://www.w3.org/2000/svg"
|
||||
// >
|
||||
// <path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z" />
|
||||
// <path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z" />
|
||||
// </svg>
|
||||
// <span className="ml-3 text-sm font-semibold">
|
||||
// {channel.category_name}
|
||||
// </span>
|
||||
// </Link>
|
||||
))}
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
import SidebarContent from "./sidebar-content.component";
|
||||
|
||||
const DesktopSidebar = () => {
|
||||
return (
|
||||
<aside className="z-30 flex-shrink-0 hidden w-64 overflow-y-auto bg-white dark:bg-gray-800 lg:block">
|
||||
<SidebarContent />
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
export default DesktopSidebar;
|
||||
13
frontend/src/components/sidebar/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import DesktopSidebar from "./desktop-sidebar.component";
|
||||
import MobileSidebar from "./mobile-sidebar.component";
|
||||
|
||||
const Sidebar = () => {
|
||||
return (
|
||||
<>
|
||||
<DesktopSidebar />
|
||||
<MobileSidebar />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
39
frontend/src/components/sidebar/mobile-sidebar.component.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import { Transition, Backdrop } from "@windmill/react-ui";
|
||||
import { SidebarContext } from "../../context";
|
||||
import SidebarContent from "./sidebar-content.component";
|
||||
|
||||
const MobileSidebar = () => {
|
||||
const { isSidebarOpen, closeSidebar } = React.useContext(SidebarContext);
|
||||
return (
|
||||
<Transition show={isSidebarOpen}>
|
||||
<>
|
||||
<Transition
|
||||
enter="transition ease-in-out duration-150"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition ease-in-out duration-150"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Backdrop onClick={closeSidebar} />
|
||||
</Transition>
|
||||
|
||||
<Transition
|
||||
enter="transition ease-in-out duration-150"
|
||||
enterFrom="opacity-0 transform -translate-x-20"
|
||||
enterTo="opacity-100"
|
||||
leave="transition ease-in-out duration-150"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0 transform -translate-x-20"
|
||||
>
|
||||
<aside className="fixed inset-y-0 z-50 flex-shrink-0 w-64 mt-16 overflow-y-auto bg-white dark:bg-gray-800 lg:hidden">
|
||||
<SidebarContent />
|
||||
</aside>
|
||||
</Transition>
|
||||
</>
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileSidebar;
|
||||
@@ -0,0 +1,78 @@
|
||||
import React from "react";
|
||||
import { NavLink, Route } from "react-router-dom";
|
||||
import { Channel } from "../../models/channel";
|
||||
|
||||
const SidebarContent = () => {
|
||||
const [channels, setChannels] = React.useState<Channel[]>([]);
|
||||
const [filteredChannels, setFilteredChannels] = React.useState<Channel[]>([]);
|
||||
React.useEffect(() => {
|
||||
const fetchChannels = async () => {
|
||||
const res = await fetch(`${process.env.REACT_APP_API_URL}/channels`);
|
||||
const data = await res.json();
|
||||
setChannels(data);
|
||||
setFilteredChannels(data);
|
||||
};
|
||||
|
||||
fetchChannels().catch(console.error);
|
||||
}, []);
|
||||
const _searchChannels = ($event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const searchString = $event.target.value;
|
||||
if (searchString) {
|
||||
const filteredChannels = channels.filter((c) => {
|
||||
const result = c.category_name
|
||||
.toLowerCase()
|
||||
.includes(searchString.toLowerCase());
|
||||
console.log(
|
||||
"sidebar.component",
|
||||
`Category Name: ${c.category_name}`,
|
||||
`Search String: ${searchString}`
|
||||
);
|
||||
console.log("sidebar.component", "Result", result);
|
||||
return result;
|
||||
});
|
||||
setFilteredChannels(filteredChannels);
|
||||
} else {
|
||||
setFilteredChannels(channels);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="py-4 text-gray-500 dark:text-gray-400">
|
||||
<a
|
||||
className="ml-6 text-lg font-bold text-gray-800 dark:text-gray-200"
|
||||
href="/"
|
||||
>
|
||||
Xtreamium
|
||||
</a>
|
||||
<ul className="mt-6">
|
||||
{filteredChannels.map((channel: Channel) => (
|
||||
<li className="relative px-6 py-3" key={channel.category_id}>
|
||||
<NavLink
|
||||
to={`/live/channel/${channel.category_id}`}
|
||||
className={({ isActive }) =>
|
||||
`inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200 ${
|
||||
isActive && "text-gray-800 dark:text-gray-100"
|
||||
}`
|
||||
}
|
||||
children={({ isActive }) => {
|
||||
return (
|
||||
<>
|
||||
{isActive && (
|
||||
<span
|
||||
className="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
)}
|
||||
{/* <Icon className="w-5 h-5" aria-hidden="true" icon={route.icon} /> */}
|
||||
<span className="ml-4">{channel.category_name}</span>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarContent;
|
||||
11
frontend/src/components/themed-suspence.component.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
|
||||
const ThemedSuspence = () => {
|
||||
return (
|
||||
<div className="w-full h-screen p-6 text-lg font-medium text-gray-600 dark:text-gray-400 dark:bg-gray-900">
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemedSuspence;
|
||||
4
frontend/src/containers/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import Layout from "./layout.container";
|
||||
import Main from "./main.container";
|
||||
|
||||
export { Main, Layout };
|
||||
40
frontend/src/containers/layout.container.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React, { Suspense } from "react";
|
||||
import { useLocation, Routes, Route } from "react-router-dom";
|
||||
import { Sidebar } from "../components";
|
||||
import Header from "../components/header.component";
|
||||
import Main from "./main.container";
|
||||
import { ChannelPage, PlayerPage } from "../pages";
|
||||
import ThemedSuspence from "../components/themed-suspence.component";
|
||||
import { SidebarContext } from "../context";
|
||||
|
||||
const Layout = () => {
|
||||
const { isSidebarOpen, closeSidebar } = React.useContext(SidebarContext);
|
||||
let location = useLocation();
|
||||
|
||||
React.useEffect(() => {
|
||||
closeSidebar();
|
||||
}, [location]);
|
||||
return (
|
||||
<div
|
||||
className={`flex h-screen bg-gray-50 dark:bg-gray-900 ${
|
||||
isSidebarOpen && "overflow-hidden"
|
||||
}`}
|
||||
>
|
||||
<Sidebar />
|
||||
|
||||
<div className="flex flex-col flex-1 w-full">
|
||||
<Header />
|
||||
<Main>
|
||||
<Suspense fallback={<ThemedSuspence />}>
|
||||
<Routes>
|
||||
<Route path="live/channel/:channelId" element={<ChannelPage />} />
|
||||
<Route path="live/play/:streamId" element={<PlayerPage />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</Main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
14
frontend/src/containers/main.container.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
|
||||
interface IMainProps {
|
||||
children: JSX.Element;
|
||||
}
|
||||
const Main = ({ children }: IMainProps) => {
|
||||
return (
|
||||
<main className="h-full overflow-y-auto">
|
||||
<div className="container grid px-6 mx-auto">{children}</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default Main;
|
||||
4
frontend/src/context/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { SidebarContext } from "./sidebar.context";
|
||||
import { ThemeContext } from "./theme.context";
|
||||
|
||||
export { SidebarContext, ThemeContext };
|
||||
39
frontend/src/context/sidebar.context.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
|
||||
interface ISidebarProvider {
|
||||
children: React.ReactChild;
|
||||
}
|
||||
interface ISidebarProviderContext {
|
||||
isSidebarOpen: boolean;
|
||||
toggleSidebar: () => void;
|
||||
closeSidebar: () => void;
|
||||
}
|
||||
export const SidebarContext = React.createContext<ISidebarProviderContext>({
|
||||
isSidebarOpen: true,
|
||||
toggleSidebar: () => {},
|
||||
closeSidebar: () => {},
|
||||
});
|
||||
|
||||
export const SidebarProvider = ({ children }: ISidebarProvider) => {
|
||||
const [isSidebarOpen, setIsSidebarOpen] = React.useState(false);
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setIsSidebarOpen(!isSidebarOpen);
|
||||
};
|
||||
const closeSidebar = () => {
|
||||
setIsSidebarOpen(false);
|
||||
};
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
isSidebarOpen,
|
||||
toggleSidebar,
|
||||
closeSidebar,
|
||||
}),
|
||||
[isSidebarOpen]
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={value}>{children}</SidebarContext.Provider>
|
||||
);
|
||||
};
|
||||
81
frontend/src/context/theme.context.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
} from "react";
|
||||
|
||||
/**
|
||||
* Saves the old theme for future use
|
||||
* @param {string} theme - Name of curent theme
|
||||
* @return {string} previousTheme
|
||||
*/
|
||||
function usePrevious(theme: any) {
|
||||
const ref = useRef();
|
||||
useEffect(() => {
|
||||
ref.current = theme;
|
||||
});
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets user preferences from local storage
|
||||
* @param {string} key - localStorage key
|
||||
* @return {array} getter and setter for user preferred theme
|
||||
*/
|
||||
function useStorageTheme(
|
||||
key: any
|
||||
): [string, React.Dispatch<React.SetStateAction<string>>] {
|
||||
const userPreference =
|
||||
!!window.matchMedia &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "1"
|
||||
: "0";
|
||||
|
||||
const [theme, setTheme] = useState<string>(
|
||||
// use stored theme; fallback to user preference
|
||||
localStorage.getItem(key) || userPreference
|
||||
);
|
||||
|
||||
// update stored theme
|
||||
useEffect(() => {
|
||||
localStorage.setItem(key, theme);
|
||||
}, [theme, key]);
|
||||
|
||||
return [theme, setTheme];
|
||||
}
|
||||
|
||||
// create context
|
||||
export const ThemeContext = React.createContext({});
|
||||
interface IThemeProvider {
|
||||
children: React.ReactChild;
|
||||
}
|
||||
// create context provider
|
||||
export const ThemeProvider = ({ children }: IThemeProvider) => {
|
||||
const [theme, setTheme] = useStorageTheme("theme");
|
||||
|
||||
// update root element class on theme change
|
||||
const oldTheme = usePrevious(theme);
|
||||
useLayoutEffect(() => {
|
||||
document.documentElement.classList.remove(`theme-${oldTheme}`);
|
||||
document.documentElement.classList.add(`theme-${theme}`);
|
||||
}, [theme, oldTheme]);
|
||||
|
||||
function toggleTheme() {
|
||||
if (theme === "light") setTheme("dark");
|
||||
else setTheme("light");
|
||||
}
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
theme,
|
||||
toggleTheme,
|
||||
}),
|
||||
[theme]
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
3
frontend/src/icons/bell.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 203 B |
10
frontend/src/icons/buttons.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 293 B |
10
frontend/src/icons/cards.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 314 B |
3
frontend/src/icons/cart.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M3 1a1 1 0 000 2h1.22l.305 1.222a.997.997 0 00.01.042l1.358 5.43-.893.892C3.74 11.846 4.632 14 6.414 14H15a1 1 0 000-2H6.414l1-1H14a1 1 0 00.894-.553l3-6A1 1 0 0017 3H6.28l-.31-1.243A1 1 0 005 1H3zM16 16.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM6.5 18a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 352 B |
11
frontend/src/icons/charts.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z"></path>
|
||||
<path d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 274 B |
7
frontend/src/icons/chat.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M18 5v8a2 2 0 01-2 2h-5l-5 4v-4H4a2 2 0 01-2-2V5a2 2 0 012-2h12a2 2 0 012 2zM7 8H5v2h2V8zm2 0h2v2H9V8zm6 0h-2v2h2V8z"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 244 B |
7
frontend/src/icons/dropdown.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 242 B |
3
frontend/src/icons/edit.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 192 B |
7
frontend/src/icons/forbidden.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M13.477 14.89A6 6 0 015.11 6.524l8.367 8.368zm1.414-1.414L6.524 5.11a6 6 0 018.367 8.367zM18 10a8 8 0 11-16 0 8 8 0 0116 0z"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 251 B |
10
frontend/src/icons/forms.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 323 B |
1
frontend/src/icons/github.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
|
||||
|
After Width: | Height: | Size: 775 B |
11
frontend/src/icons/heart.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z"
|
||||
clip-rule="evenodd"
|
||||
fill-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 263 B |
10
frontend/src/icons/home.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 316 B |
59
frontend/src/icons/index.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { ReactComponent as ButtonsIcon } from './buttons.svg'
|
||||
import { ReactComponent as CardsIcon } from './cards.svg'
|
||||
import { ReactComponent as ChartsIcon } from './charts.svg'
|
||||
import { ReactComponent as FormsIcon } from './forms.svg'
|
||||
import { ReactComponent as HomeIcon } from './home.svg'
|
||||
import { ReactComponent as ModalsIcon } from './modals.svg'
|
||||
import { ReactComponent as PagesIcon } from './pages.svg'
|
||||
import { ReactComponent as TablesIcon } from './tables.svg'
|
||||
import { ReactComponent as HeartIcon } from './heart.svg'
|
||||
import { ReactComponent as EditIcon } from './edit.svg'
|
||||
import { ReactComponent as TrashIcon } from './trash.svg'
|
||||
import { ReactComponent as ForbiddenIcon } from './forbidden.svg'
|
||||
import { ReactComponent as GithubIcon } from './github.svg'
|
||||
import { ReactComponent as TwitterIcon } from './twitter.svg'
|
||||
import { ReactComponent as MailIcon } from './mail.svg'
|
||||
import { ReactComponent as CartIcon } from './cart.svg'
|
||||
import { ReactComponent as ChatIcon } from './chat.svg'
|
||||
import { ReactComponent as MoneyIcon } from './money.svg'
|
||||
import { ReactComponent as PeopleIcon } from './people.svg'
|
||||
import { ReactComponent as SearchIcon } from './search.svg'
|
||||
import { ReactComponent as MoonIcon } from './moon.svg'
|
||||
import { ReactComponent as SunIcon } from './sun.svg'
|
||||
import { ReactComponent as BellIcon } from './bell.svg'
|
||||
import { ReactComponent as MenuIcon } from './menu.svg'
|
||||
import { ReactComponent as DropdownIcon } from './dropdown.svg'
|
||||
import { ReactComponent as OutlinePersonIcon } from './outlinePerson.svg'
|
||||
import { ReactComponent as OutlineCogIcon } from './outlineCog.svg'
|
||||
import { ReactComponent as OutlineLogoutIcon } from './outlineLogout.svg'
|
||||
|
||||
export {
|
||||
ButtonsIcon,
|
||||
CardsIcon,
|
||||
ChartsIcon,
|
||||
FormsIcon,
|
||||
HomeIcon,
|
||||
ModalsIcon,
|
||||
PagesIcon,
|
||||
TablesIcon,
|
||||
HeartIcon,
|
||||
EditIcon,
|
||||
TrashIcon,
|
||||
ForbiddenIcon,
|
||||
GithubIcon,
|
||||
TwitterIcon,
|
||||
MailIcon,
|
||||
CartIcon,
|
||||
ChatIcon,
|
||||
MoneyIcon,
|
||||
PeopleIcon,
|
||||
SearchIcon,
|
||||
MoonIcon,
|
||||
SunIcon,
|
||||
BellIcon,
|
||||
MenuIcon,
|
||||
DropdownIcon,
|
||||
OutlinePersonIcon,
|
||||
OutlineCogIcon,
|
||||
OutlineLogoutIcon,
|
||||
}
|
||||
10
frontend/src/icons/mail.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 264 B |
7
frontend/src/icons/menu.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 271 B |
10
frontend/src/icons/modals.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 281 B |
7
frontend/src/icons/money.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4 4a2 2 0 00-2 2v4a2 2 0 002 2V6h10a2 2 0 00-2-2H4zm2 6a2 2 0 012-2h8a2 2 0 012 2v4a2 2 0 01-2 2H8a2 2 0 01-2-2v-4zm6 4a2 2 0 100-4 2 2 0 000 4z"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 273 B |
3
frontend/src/icons/moon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 138 B |
11
frontend/src/icons/outlineCog.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 700 B |
10
frontend/src/icons/outlineLogout.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 256 B |
10
frontend/src/icons/outlinePerson.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 231 B |
10
frontend/src/icons/pages.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 360 B |
3
frontend/src/icons/people.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 316 B |
7
frontend/src/icons/search.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg aria-hidden="true" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 259 B |
7
frontend/src/icons/sun.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 628 B |
10
frontend/src/icons/tables.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path d="M4 6h16M4 10h16M4 14h16M4 18h16"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 195 B |
10
frontend/src/icons/trash.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 336 B |
1
frontend/src/icons/twitter.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.954 4.569c-.885.389-1.83.654-2.825.775 1.014-.611 1.794-1.574 2.163-2.723-.951.555-2.005.959-3.127 1.184-.896-.959-2.173-1.559-3.591-1.559-2.717 0-4.92 2.203-4.92 4.917 0 .39.045.765.127 1.124C7.691 8.094 4.066 6.13 1.64 3.161c-.427.722-.666 1.561-.666 2.475 0 1.71.87 3.213 2.188 4.096-.807-.026-1.566-.248-2.228-.616v.061c0 2.385 1.693 4.374 3.946 4.827-.413.111-.849.171-1.296.171-.314 0-.615-.03-.916-.086.631 1.953 2.445 3.377 4.604 3.417-1.68 1.319-3.809 2.105-6.102 2.105-.39 0-.779-.023-1.17-.067 2.189 1.394 4.768 2.209 7.557 2.209 9.054 0 13.999-7.496 13.999-13.986 0-.209 0-.42-.015-.63.961-.689 1.8-1.56 2.46-2.548l-.047-.02z"/></svg>
|
||||
|
After Width: | Height: | Size: 704 B |
@@ -7,9 +7,7 @@ import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
|
||||
@@ -1024,7 +1024,7 @@
|
||||
core-js-pure "^3.20.2"
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
||||
version "7.17.8"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.8.tgz#3e56e4aff81befa55ac3ac6a0967349fd1c5bca2"
|
||||
integrity sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==
|
||||
@@ -1570,6 +1570,13 @@
|
||||
"@svgr/plugin-svgo" "^5.5.0"
|
||||
loader-utils "^2.0.0"
|
||||
|
||||
"@tailwindcss/forms@^0.3.2":
|
||||
version "0.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@tailwindcss/forms/-/forms-0.3.4.tgz#e4939dc16450eccf4fd2029770096f38cbb556d4"
|
||||
integrity sha512-vlAoBifNJUkagB+PAdW4aHMe4pKmSLroH398UPgIogBFc91D2VlHUxe4pjxQhiJl0Nfw53sHSJSQBSTQBZP3vA==
|
||||
dependencies:
|
||||
mini-svg-data-uri "^1.2.3"
|
||||
|
||||
"@testing-library/dom@^8.0.0":
|
||||
version "8.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.12.0.tgz#fef5e545533fb084175dda6509ee71d7d2f72e23"
|
||||
@@ -2153,6 +2160,18 @@
|
||||
"@webassemblyjs/ast" "1.11.1"
|
||||
"@xtuc/long" "4.2.2"
|
||||
|
||||
"@windmill/react-ui@^0.6.0":
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@windmill/react-ui/-/react-ui-0.6.0.tgz#259ee5d8b08088d4b0258fdc7595760dda6b09fe"
|
||||
integrity sha512-VjvRC0YI8V/uUMWU70XL0jHzBYmRGPMlvauLjdHJ0h60cSFm2ZrcvwIVktPi9eWw9au2cXov13rkwvVmPM/Yww==
|
||||
dependencies:
|
||||
"@tailwindcss/forms" "^0.3.2"
|
||||
classnames "2.2.6"
|
||||
deepmerge "4.2.2"
|
||||
postcss "^8.2.15"
|
||||
react-focus-lock "2.4.1"
|
||||
react-transition-group "4.4.1"
|
||||
|
||||
"@xtuc/ieee754@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
|
||||
@@ -2872,6 +2891,11 @@ cjs-module-lexer@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40"
|
||||
integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==
|
||||
|
||||
classnames@2.2.6:
|
||||
version "2.2.6"
|
||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
|
||||
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
|
||||
|
||||
clean-css@^5.2.2:
|
||||
version "5.2.4"
|
||||
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.2.4.tgz#982b058f8581adb2ae062520808fb2429bd487a4"
|
||||
@@ -3377,7 +3401,7 @@ deep-is@^0.1.3, deep-is@~0.1.3:
|
||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
|
||||
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
|
||||
|
||||
deepmerge@^4.2.2:
|
||||
deepmerge@4.2.2, deepmerge@^4.2.2:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
|
||||
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
|
||||
@@ -3440,6 +3464,11 @@ detect-newline@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
|
||||
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
|
||||
|
||||
detect-node-es@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493"
|
||||
integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==
|
||||
|
||||
detect-node@^2.0.4:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
|
||||
@@ -3530,6 +3559,14 @@ dom-converter@^0.2.0:
|
||||
dependencies:
|
||||
utila "~0.4"
|
||||
|
||||
dom-helpers@^5.0.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
|
||||
integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.8.7"
|
||||
csstype "^3.0.2"
|
||||
|
||||
dom-serializer@0:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
|
||||
@@ -4237,6 +4274,11 @@ flatted@^3.1.0:
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3"
|
||||
integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==
|
||||
|
||||
focus-lock@^0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.7.0.tgz#b2bfb0ca7beacc8710a1ff74275fe0dc60a1d88a"
|
||||
integrity sha512-LI7v2mH02R55SekHYdv9pRHR9RajVNyIJ2N5IEkWbg7FT5ZmJ9Hw4mWxHeEUcd+dJo0QmzztHvDvWcc7prVFsw==
|
||||
|
||||
follow-redirects@^1.0.0:
|
||||
version "1.14.9"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7"
|
||||
@@ -5841,6 +5883,11 @@ mini-css-extract-plugin@^2.4.5:
|
||||
dependencies:
|
||||
schema-utils "^4.0.0"
|
||||
|
||||
mini-svg-data-uri@^1.2.3:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz#8ab0aabcdf8c29ad5693ca595af19dd2ead09939"
|
||||
integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==
|
||||
|
||||
minimalistic-assert@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
|
||||
@@ -6852,7 +6899,7 @@ postcss@^7.0.35:
|
||||
picocolors "^0.2.1"
|
||||
source-map "^0.6.1"
|
||||
|
||||
postcss@^8.3.5, postcss@^8.4.12, postcss@^8.4.4, postcss@^8.4.6, postcss@^8.4.7:
|
||||
postcss@^8.2.15, postcss@^8.3.5, postcss@^8.4.12, postcss@^8.4.4, postcss@^8.4.6, postcss@^8.4.7:
|
||||
version "8.4.12"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.12.tgz#1e7de78733b28970fa4743f7da6f3763648b1905"
|
||||
integrity sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg==
|
||||
@@ -6913,7 +6960,7 @@ prompts@^2.0.1, prompts@^2.4.2:
|
||||
kleur "^3.0.3"
|
||||
sisteransi "^1.0.5"
|
||||
|
||||
prop-types@^15.8.1:
|
||||
prop-types@^15.6.2, prop-types@^15.8.1:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
@@ -7001,6 +7048,13 @@ react-app-polyfill@^3.0.0:
|
||||
regenerator-runtime "^0.13.9"
|
||||
whatwg-fetch "^3.6.2"
|
||||
|
||||
react-clientside-effect@^1.2.2:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.5.tgz#e2c4dc3c9ee109f642fac4f5b6e9bf5bcd2219a3"
|
||||
integrity sha512-2bL8qFW1TGBHozGGbVeyvnggRpMjibeZM2536AKNENLECutp2yfs44IL8Hmpn8qjFQ2K7A9PnYf3vc7aQq/cPA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.13"
|
||||
|
||||
react-dev-utils@^12.0.0:
|
||||
version "12.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.0.tgz#4eab12cdb95692a077616770b5988f0adf806526"
|
||||
@@ -7045,6 +7099,18 @@ react-error-overlay@^6.0.10:
|
||||
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6"
|
||||
integrity sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA==
|
||||
|
||||
react-focus-lock@2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.4.1.tgz#e842cc93da736b5c5d331799012544295cbcee4f"
|
||||
integrity sha512-c5ZP56KSpj9EAxzScTqQO7bQQNPltf/W1ZEBDqNDOV1XOIwvAyHX0O7db9ekiAtxyKgnqZjQlLppVg94fUeL9w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.0.0"
|
||||
focus-lock "^0.7.0"
|
||||
prop-types "^15.6.2"
|
||||
react-clientside-effect "^1.2.2"
|
||||
use-callback-ref "^1.2.1"
|
||||
use-sidecar "^1.0.1"
|
||||
|
||||
react-is@^16.13.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
@@ -7130,6 +7196,16 @@ react-scripts@5.0.0:
|
||||
optionalDependencies:
|
||||
fsevents "^2.3.2"
|
||||
|
||||
react-transition-group@4.4.1:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
|
||||
integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
dom-helpers "^5.0.1"
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react@^17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
||||
@@ -8100,7 +8176,7 @@ tsconfig-paths@^3.12.0:
|
||||
minimist "^1.2.6"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
tslib@^1.8.1:
|
||||
tslib@^1.8.1, tslib@^1.9.3:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
@@ -8243,6 +8319,19 @@ uri-js@^4.2.2:
|
||||
dependencies:
|
||||
punycode "^2.1.0"
|
||||
|
||||
use-callback-ref@^1.2.1:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.5.tgz#6115ed242cfbaed5915499c0a9842ca2912f38a5"
|
||||
integrity sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==
|
||||
|
||||
use-sidecar@^1.0.1:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.5.tgz#ffff2a17c1df42e348624b699ba6e5c220527f2b"
|
||||
integrity sha512-k9jnrjYNwN6xYLj1iaGhonDghfvmeTmYjAiGvOr7clwKfPjMXJf4/HOr7oT5tJwYafgp2tG2l3eZEOfoELiMcA==
|
||||
dependencies:
|
||||
detect-node-es "^1.1.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
|
||||