diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7a4ce58..f29f529 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,6 +16,7 @@ "axios": "^1.9.0", "bootstrap": "^5.3.3", "chart.js": "^4.4.8", + "framer-motion": "^12.14.0", "leaflet": "^1.9.4", "leaflet.heat": "^0.2.0", "react": "^19.0.0", @@ -3706,6 +3707,33 @@ "node": ">=0.4.x" } }, + "node_modules/framer-motion": { + "version": "12.14.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.14.0.tgz", + "integrity": "sha512-P/CLiA+YLc0GL1nB2bgtRBFAbRh0aawU5KPLqnXi6QEZQE2BEwqgIH8qUIT7cMN+4a4nj0iZ2uWTKOrzcYsyGQ==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.14.0", + "motion-utils": "^12.12.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4802,6 +4830,21 @@ "node": "*" } }, + "node_modules/motion-dom": { + "version": "12.14.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.14.0.tgz", + "integrity": "sha512-t5v1QNXrOsLmIzTdmB9OlZ1cA8zkHlpPhn85123/B+8Xe6tiENWKELAZm0yvAoFjcbFmq4u80uUXortZTTvDbg==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.12.1" + } + }, + "node_modules/motion-utils": { + "version": "12.12.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.12.1.tgz", + "integrity": "sha512-f9qiqUHm7hWSLlNW8gS9pisnsN7CRFRD58vNjptKdsqFLpkVnX00TNeD6Q0d27V9KzT7ySFyK1TZ/DShfVOv6w==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 061b6c6..9aa9a03 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ "axios": "^1.9.0", "bootstrap": "^5.3.3", "chart.js": "^4.4.8", + "framer-motion": "^12.14.0", "leaflet": "^1.9.4", "leaflet.heat": "^0.2.0", "react": "^19.0.0", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 4dca96a..8b0f4f7 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -5,19 +5,19 @@ import 'bootstrap/dist/js/bootstrap.bundle.min.js' import Dashboard from '@/pages/Dashboard.jsx' import Groups from '@/pages/Groups.jsx' -import ThemeButton from '@/components/layout/ThemeButton.jsx' import Header from '@/components/layout/Header.jsx' import GroupView from '@/pages/GroupView.jsx' import { Routes, Route } from 'react-router-dom' -import ContentWrapper from './components/layout/ContentWrapper' -import Docs from './pages/Docs' +import ContentWrapper from '@/components/layout/ContentWrapper' +import Docs from '@/pages/Docs' +import FloatingMenu from '@/components/layout/FloatingMenu' const App = () => { return ( <> - +
diff --git a/frontend/src/components/layout/DocsButton.jsx b/frontend/src/components/layout/DocsButton.jsx new file mode 100644 index 0000000..d342789 --- /dev/null +++ b/frontend/src/components/layout/DocsButton.jsx @@ -0,0 +1,12 @@ +import "@/css/DocsButton.css"; +import { Link } from "react-router-dom"; + +const DocsButton = () => { + return ( + + + + ); +} + +export default DocsButton; \ No newline at end of file diff --git a/frontend/src/components/layout/FloatingMenu.jsx b/frontend/src/components/layout/FloatingMenu.jsx new file mode 100644 index 0000000..6712c34 --- /dev/null +++ b/frontend/src/components/layout/FloatingMenu.jsx @@ -0,0 +1,61 @@ +import { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import DocsButton from "./DocsButton"; +import ThemeButton from "./ThemeButton"; +import "@/css/FloatingMenu.css"; +import { faEllipsisVertical } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +const FloatingMenu = () => { + const [open, setOpen] = useState(false); + + const buttonVariants = { + hidden: { opacity: 0, y: 10 }, + visible: (i) => ({ + opacity: 1, + y: 0, + transition: { delay: i * 0.05, type: "spring", stiffness: 300 } + }), + exit: { opacity: 0, y: 10, transition: { duration: 0.1 } } + }; + + const buttons = [ + { component: , key: "docs", onClick: () => setOpen(false) }, + { component: , key: "theme", onClick: () => setOpen(false) } + ]; + + return ( +
+ + {open && ( + + {buttons.map((btn, i) => ( + + {btn.component} + + ))} + + )} + + + +
+ ); +}; + +export default FloatingMenu; \ No newline at end of file diff --git a/frontend/src/css/DocsButton.css b/frontend/src/css/DocsButton.css new file mode 100644 index 0000000..b7a32a2 --- /dev/null +++ b/frontend/src/css/DocsButton.css @@ -0,0 +1,19 @@ +.docs-button { + z-index: 1000; + border: none; + border-radius: 50%; + width: 50px; + height: 50px; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--primary-color); + color: white; + cursor: pointer; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: background-color 0.3s, transform 0.3s; +} + +.docs-button:hover { + background-color: var(--secondary-color); +} diff --git a/frontend/src/css/FloatingMenu.css b/frontend/src/css/FloatingMenu.css new file mode 100644 index 0000000..c7570b8 --- /dev/null +++ b/frontend/src/css/FloatingMenu.css @@ -0,0 +1,55 @@ +.floating-menu { + position: fixed; + bottom: 20px; + right: 20px; + z-index: 1000; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 10px; +} + +.menu-buttons { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 10px; +} + +.menu-toggle { + border: none; + border-radius: 50%; + width: 50px; + height: 50px; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--primary-color); + color: white; + cursor: pointer; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: background-color 0.3s, transform 0.3s; +} + +.menu-toggle:hover { + background-color: var(--secondary-color); +} + +.menu-buttons button { + border: none; + border-radius: 50%; + width: 50px; + height: 50px; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--primary-color); + color: white; + cursor: pointer; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: background-color 0.3s, transform 0.3s; +} + +.menu-buttons button:hover { + background-color: var(--secondary-color); +} diff --git a/frontend/src/css/ThemeButton.css b/frontend/src/css/ThemeButton.css index e0f0f0d..805a93f 100644 --- a/frontend/src/css/ThemeButton.css +++ b/frontend/src/css/ThemeButton.css @@ -1,7 +1,4 @@ .theme-toggle { - position: fixed; - bottom: 20px; - right: 20px; z-index: 1000; border: none; border-radius: 50%; @@ -19,4 +16,4 @@ .theme-toggle:hover { background-color: var(--secondary-color); -} \ No newline at end of file +}