diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc index 6ae05f8..1be8463 100644 Binary files a/backend/__pycache__/main.cpython-313.pyc and b/backend/__pycache__/main.cpython-313.pyc differ diff --git a/backend/apps.yaml b/backend/apps.yaml index ea06927..8c186c3 100644 --- a/backend/apps.yaml +++ b/backend/apps.yaml @@ -35,3 +35,7 @@ sections: name: Vault url: https://vault.dvirlabs.com name: Dev-tools +- apps: [] + name: fgbhn +- apps: [] + name: dfgb diff --git a/backend/main.py b/backend/main.py index 9be818c..b60c0e4 100644 --- a/backend/main.py +++ b/backend/main.py @@ -20,7 +20,6 @@ app.add_middleware( load_dotenv() -# ENV MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT") MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY") MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY") @@ -34,12 +33,13 @@ minio_client = Minio( ) BUCKET = MINIO_BUCKET or "navix-icons" +APPS_FILE = Path(__file__).parent / "apps.yaml" + @router.get("/") def root(): return {"message": "Welcome to the FastAPI application!"} -APPS_FILE = Path(__file__).parent / "apps.yaml" @router.get("/apps") def get_apps(): @@ -48,15 +48,19 @@ def get_apps(): with open(APPS_FILE, "r") as f: return yaml.safe_load(f) + class AppData(BaseModel): name: str icon: str description: str url: str + class AppEntry(BaseModel): section: str app: AppData + original_name: str | None = None + @router.post("/add_app") def add_app(entry: AppEntry): @@ -81,6 +85,7 @@ def add_app(entry: AppEntry): return {"status": "added"} + @router.post("/edit_app") def edit_app(entry: AppEntry): if not APPS_FILE.exists(): @@ -93,20 +98,21 @@ def edit_app(entry: AppEntry): for section in current["sections"]: if section["name"] == entry.section: for i, app in enumerate(section["apps"]): - if app["name"] == entry.app.name: + if app["name"] == (entry.original_name or entry.app.name): section["apps"][i] = entry.app.dict() updated = True break break if not updated: - return {"error": "App not found to edit"}, 404 + return JSONResponse(status_code=404, content={"error": "App not found to edit"}) with open(APPS_FILE, "w") as f: yaml.safe_dump(current, f) return {"status": "updated"} + @router.post("/delete_app") def delete_app(entry: AppEntry): if not APPS_FILE.exists(): @@ -125,7 +131,7 @@ def delete_app(entry: AppEntry): break if not deleted: - return {"error": "App not found to delete"}, 404 + return JSONResponse(status_code=404, content={"error": "App not found to delete"}) with open(APPS_FILE, "w") as f: yaml.safe_dump(current, f) @@ -138,10 +144,11 @@ def get_public_icon_url(filename: str): url = f"https://{MINIO_ENDPOINT}/{BUCKET}/{filename}" return JSONResponse(content={"url": url}) + app.include_router(router, prefix="/api") +# ✅ This is the missing part: if __name__ == "__main__": import uvicorn uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) - diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0d90a6d..885f3d7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,7 +11,8 @@ "react": "^19.1.0", "react-beautiful-dnd": "^13.1.1", "react-dom": "^19.1.0", - "react-icons": "^5.5.0" + "react-icons": "^5.5.0", + "react-toastify": "^11.0.5" }, "devDependencies": { "@eslint/js": "^9.25.0", @@ -1488,6 +1489,14 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2440,6 +2449,18 @@ "node": ">=0.10.0" } }, + "node_modules/react-toastify": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz", + "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==", + "dependencies": { + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, "node_modules/redux": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index e2a3347..45ce11c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,8 @@ "react": "^19.1.0", "react-beautiful-dnd": "^13.1.1", "react-dom": "^19.1.0", - "react-icons": "^5.5.0" + "react-icons": "^5.5.0", + "react-toastify": "^11.0.5" }, "devDependencies": { "@eslint/js": "^9.25.0", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 4077ff1..193523c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -4,8 +4,15 @@ import SectionGrid from './components/SectionGrid'; import AppModal from './components/AppModal'; import ConfirmDialog from './components/ConfirmDialog'; import Clock from './components/Clock'; +import { ToastContainer, toast } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; import { IoIosAdd } from 'react-icons/io'; -import { fetchSections, addAppToSection, editAppInSection, deleteAppFromSection } from './services/api'; +import { + fetchSections, + addAppToSection, + editAppInSection, + deleteAppFromSection, +} from './services/api'; function App() { const [sections, setSections] = useState([]); @@ -14,10 +21,13 @@ function App() { const [confirmData, setConfirmData] = useState(null); const loadSections = () => { - fetchSections() - .then(data => setSections(data.sections || [])) - .catch(err => console.error('Failed to fetch sections:', err)); - }; + fetchSections() + .then(data => { + const filtered = (data.sections || []).filter(section => (section.apps?.length ?? 0) > 0); + setSections(filtered); + }) + .catch(err => console.error('Failed to fetch sections:', err)); +}; useEffect(() => { loadSections(); @@ -26,7 +36,7 @@ function App() { const handleDelete = (app) => { setConfirmData({ message: `Are you sure you want to delete "${app.name}"?`, - app + app, }); }; @@ -34,16 +44,48 @@ function App() { if (!confirmData?.app) return; try { - await deleteAppFromSection({ section: confirmData.app.section, app: confirmData.app }); + await deleteAppFromSection({ + section: confirmData.app.section, + app: confirmData.app, + }); + toast.error(`App "${confirmData.app.name}" deleted successfully!`); loadSections(); } catch (err) { console.error('Failed to delete app:', err); - alert('Failed to delete app'); + toast.error('Failed to delete app'); } finally { setConfirmData(null); } }; + const handleAddSubmit = async (data) => { + try { + await addAppToSection(data); + toast.success(`App "${data.app.name}" added successfully!`); + setShowAdd(false); + loadSections(); + } catch (err) { + console.error('Failed to add app:', err); + toast.error('Failed to add app'); + } + }; + + const handleEditSubmit = async (data) => { + try { + await editAppInSection({ + section: data.section, + app: data.app, + original_name: data.original_name || data.app.name, + }); + toast.warning(`App "${data.app.name}" updated successfully!`); + setEditData(null); + loadSections(); + } catch (err) { + console.error('Failed to update app:', err); + toast.error('Failed to update app'); + } + }; + return (
@@ -55,11 +97,8 @@ function App() { {showAdd && ( { - setShowAdd(false); - loadSections(); - }} + onSubmit={handleAddSubmit} + onClose={() => setShowAdd(false)} sections={sections} /> )} @@ -68,11 +107,8 @@ function App() { { - setEditData(null); - loadSections(); - }} + onSubmit={handleEditSubmit} + onClose={() => setEditData(null)} sections={sections} /> )} @@ -85,18 +121,32 @@ function App() { /> )} - {sections.map(section => ( + {sections.map((section) => ( + setEditData({ ...app, section: section.name, original_name: app.name }) + } onDelete={handleDelete} /> ))} setShowAdd(true)} /> + +
); } -export default App; \ No newline at end of file +export default App; diff --git a/frontend/src/components/AppModal.jsx b/frontend/src/components/AppModal.jsx index 6823f3c..07efc06 100644 --- a/frontend/src/components/AppModal.jsx +++ b/frontend/src/components/AppModal.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import '../style/AddAppModal.css'; function AppModal({ mode = 'add', initialData = {}, onSubmit, onClose, sections = [] }) { @@ -12,7 +12,24 @@ function AppModal({ mode = 'add', initialData = {}, onSubmit, onClose, sections const handleSubmit = async (e) => { e.preventDefault(); - await onSubmit({ section, app: { name, icon, description, url } }); + + const appData = { + name, + icon, + description, + url, + }; + + const payload = { + section, + app: appData, + }; + + if (mode === 'edit') { + payload.original_name = initialData.original_name || initialData.name; + } + + await onSubmit(payload); setOpen(false); if (onClose) onClose(); }; @@ -40,8 +57,10 @@ function AppModal({ mode = 'add', initialData = {}, onSubmit, onClose, sections disabled={mode === 'edit'} > - {sections.map(s => ( - + {sections.map((s) => ( + ))} @@ -51,15 +70,37 @@ function AppModal({ mode = 'add', initialData = {}, onSubmit, onClose, sections type="text" placeholder="New section name" value={section} - onChange={e => setSection(e.target.value)} + onChange={(e) => setSection(e.target.value)} required /> )} - setName(e.target.value)} required /> - setIcon(e.target.value)} /> - setDescription(e.target.value)} /> - setUrl(e.target.value)} required /> + setName(e.target.value)} + required + /> + setIcon(e.target.value)} + /> + setDescription(e.target.value)} + /> + setUrl(e.target.value)} + required + /> @@ -67,4 +108,4 @@ function AppModal({ mode = 'add', initialData = {}, onSubmit, onClose, sections ); } -export default AppModal; \ No newline at end of file +export default AppModal; diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 6794a9e..5e7f656 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -23,11 +23,11 @@ export async function getIconUrl(filename) { return data.url; } -export async function editAppInSection({ section, app }) { +export async function editAppInSection({ section, app, original_name }) { const res = await fetch(`${API_BASE}/edit_app`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ section, app }) + body: JSON.stringify({ section, app, original_name }) }); if (!res.ok) throw new Error(await res.text());