diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc
index 94945cc..f183f0b 100644
Binary files a/backend/__pycache__/main.cpython-313.pyc and b/backend/__pycache__/main.cpython-313.pyc differ
diff --git a/backend/main.py b/backend/main.py
index a4add29..9be818c 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -81,6 +81,58 @@ def add_app(entry: AppEntry):
return {"status": "added"}
+@router.post("/edit_app")
+def edit_app(entry: AppEntry):
+ if not APPS_FILE.exists():
+ return {"error": "apps.yaml not found"}
+
+ with open(APPS_FILE, "r") as f:
+ current = yaml.safe_load(f) or {"sections": []}
+
+ updated = False
+ for section in current["sections"]:
+ if section["name"] == entry.section:
+ for i, app in enumerate(section["apps"]):
+ if app["name"] == entry.app.name:
+ section["apps"][i] = entry.app.dict()
+ updated = True
+ break
+ break
+
+ if not updated:
+ return {"error": "App not found to edit"}, 404
+
+ 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():
+ return {"error": "apps.yaml not found"}
+
+ with open(APPS_FILE, "r") as f:
+ current = yaml.safe_load(f) or {"sections": []}
+
+ deleted = False
+ for section in current["sections"]:
+ if section["name"] == entry.section:
+ original_len = len(section["apps"])
+ section["apps"] = [a for a in section["apps"] if a["name"] != entry.app.name]
+ if len(section["apps"]) != original_len:
+ deleted = True
+ break
+
+ if not deleted:
+ return {"error": "App not found to delete"}, 404
+
+ with open(APPS_FILE, "w") as f:
+ yaml.safe_dump(current, f)
+
+ return {"status": "deleted"}
+
+
@router.get("/icon/{filename}")
def get_public_icon_url(filename: str):
url = f"https://{MINIO_ENDPOINT}/{BUCKET}/{filename}"
diff --git a/frontend/.gitignore b/frontend/.gitignore
index a547bf3..14a6151 100644
--- a/frontend/.gitignore
+++ b/frontend/.gitignore
@@ -22,3 +22,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?
+
+# Custom files
+env.js
\ No newline at end of file
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index ceaafb0..4077ff1 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,19 +1,49 @@
import { useEffect, useState } from 'react';
import './App.css';
import SectionGrid from './components/SectionGrid';
-import AddAppModal from './components/AddAppModal';
+import AppModal from './components/AppModal';
+import ConfirmDialog from './components/ConfirmDialog';
import Clock from './components/Clock';
-import { fetchSections } from './services/api'; // ✅ שימוש בפונקציה הנכונה
+import { IoIosAdd } from 'react-icons/io';
+import { fetchSections, addAppToSection, editAppInSection, deleteAppFromSection } from './services/api';
function App() {
const [sections, setSections] = useState([]);
+ const [editData, setEditData] = useState(null);
+ const [showAdd, setShowAdd] = useState(false);
+ const [confirmData, setConfirmData] = useState(null);
- useEffect(() => {
+ const loadSections = () => {
fetchSections()
.then(data => setSections(data.sections || []))
.catch(err => console.error('Failed to fetch sections:', err));
+ };
+
+ useEffect(() => {
+ loadSections();
}, []);
+ const handleDelete = (app) => {
+ setConfirmData({
+ message: `Are you sure you want to delete "${app.name}"?`,
+ app
+ });
+ };
+
+ const confirmDelete = async () => {
+ if (!confirmData?.app) return;
+
+ try {
+ await deleteAppFromSection({ section: confirmData.app.section, app: confirmData.app });
+ loadSections();
+ } catch (err) {
+ console.error('Failed to delete app:', err);
+ alert('Failed to delete app');
+ } finally {
+ setConfirmData(null);
+ }
+ };
+
return (
@@ -21,12 +51,52 @@ function App() {

Navix
-
window.location.reload()} />
- {sections.map((section) => (
-
+
+ {showAdd && (
+ {
+ setShowAdd(false);
+ loadSections();
+ }}
+ sections={sections}
+ />
+ )}
+
+ {editData && (
+ {
+ setEditData(null);
+ loadSections();
+ }}
+ sections={sections}
+ />
+ )}
+
+ {confirmData && (
+ setConfirmData(null)}
+ />
+ )}
+
+ {sections.map(section => (
+
))}
+
+ setShowAdd(true)} />
);
}
-export default App;
+export default App;
\ No newline at end of file
diff --git a/frontend/src/components/AddAppModal.jsx b/frontend/src/components/AddAppModal.jsx
deleted file mode 100644
index c37ee16..0000000
--- a/frontend/src/components/AddAppModal.jsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import { useEffect, useState } from 'react';
-import { addAppToSection, fetchSections } from '../services/api';
-import '../style/AddAppModal.css';
-import { IoIosAdd } from "react-icons/io";
-
-function AddAppModal({ onAdded }) {
- const [open, setOpen] = useState(false);
-
- const [section, setSection] = useState('');
- const [customSection, setCustomSection] = useState(false);
- const [name, setName] = useState('');
- const [icon, setIcon] = useState('');
- const [description, setDescription] = useState('');
- const [url, setUrl] = useState('');
- const [sections, setSections] = useState([]);
-
- useEffect(() => {
- fetchSections().then(data => setSections(data.sections || []));
- }, []);
-
- const handleSubmit = async (e) => {
- e.preventDefault();
- try {
- await addAppToSection({ section, app: { name, icon, description, url } });
- setOpen(false);
- setSection('');
- setName('');
- setIcon('');
- setDescription('');
- setUrl('');
- setCustomSection(false);
- if (onAdded) onAdded();
- } catch (err) {
- alert('Failed to add app');
- }
- };
-
- return (
- <>
- setOpen(true)} />
-
- {open && (
- setOpen(false)}>
-
e.stopPropagation()}>
-
Add New App
-
-
-
- )}
- >
- );
-}
-
-export default AddAppModal;
diff --git a/frontend/src/components/AppCard.jsx b/frontend/src/components/AppCard.jsx
index 9fe7f08..e34f6bd 100644
--- a/frontend/src/components/AppCard.jsx
+++ b/frontend/src/components/AppCard.jsx
@@ -1,32 +1,38 @@
import { useEffect, useState } from 'react';
import '../style/AppCard.css';
-import { getIconUrl } from '../services/api';
+import { getIconUrl } from '../services/api';
+import { FaEdit, FaTrash } from 'react-icons/fa';
-function AppCard({ app }) {
+function AppCard({ app, section, onEdit, onDelete }) {
const [iconUrl, setIconUrl] = useState(null);
useEffect(() => {
- if (app.icon) {
- getIconUrl(app.icon)
- .then((url) => {
- console.log('Presigned icon URL for', app.name, ':', url);
- setIconUrl(url);
- })
- .catch((err) => {
- console.error(`Failed to load icon for ${app.name}:`, err);
- });
- }
-}, [app.icon, app.name]);
+ if (app.icon) {
+ getIconUrl(app.icon)
+ .then((url) => setIconUrl(url))
+ .catch((err) => console.error(`Failed to load icon for ${app.name}:`, err));
+ }
+ }, [app.icon, app.name]);
return (
+
+ {
+ e.preventDefault();
+ if (typeof onDelete === 'function') {
+ onDelete({ ...app, section });
+ }
+ }} />
+ {
+ e.preventDefault();
+ if (typeof onEdit === 'function') {
+ onEdit({ ...app, section });
+ }
+ }} />
+
- {iconUrl ? (
-

- ) : (
-
⚠️
- )}
+ {iconUrl ?

:
⚠️}
{app.name}
{app.description}
@@ -35,4 +41,4 @@ function AppCard({ app }) {
);
}
-export default AppCard;
+export default AppCard;
\ No newline at end of file
diff --git a/frontend/src/components/AppGrid.jsx b/frontend/src/components/AppGrid.jsx
index f527f0f..e394c07 100644
--- a/frontend/src/components/AppGrid.jsx
+++ b/frontend/src/components/AppGrid.jsx
@@ -1,14 +1,14 @@
import AppCard from './AppCard';
import '../style/AppGrid.css';
-function AppGrid({ apps }) {
+function AppGrid({ apps, section, onEdit, onDelete }) {
return (
{apps.map(app => (
-
+
))}
);
}
-export default AppGrid;
+export default AppGrid;
\ No newline at end of file
diff --git a/frontend/src/components/AppModal.jsx b/frontend/src/components/AppModal.jsx
new file mode 100644
index 0000000..fcff3c7
--- /dev/null
+++ b/frontend/src/components/AppModal.jsx
@@ -0,0 +1,70 @@
+import { useEffect, useState } from 'react';
+import '../style/AddAppModal.css';
+
+function AppModal({ mode = 'add', initialData = {}, onSubmit, onClose, sections = [] }) {
+ const [open, setOpen] = useState(true);
+ const [section, setSection] = useState(initialData.section || '');
+ const [customSection, setCustomSection] = useState(false);
+ const [name, setName] = useState(initialData.name || '');
+ const [icon, setIcon] = useState(initialData.icon || '');
+ const [description, setDescription] = useState(initialData.description || '');
+ const [url, setUrl] = useState(initialData.url || '');
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ await onSubmit({ section, app: { name, icon, description, url } });
+ setOpen(false);
+ if (onClose) onClose();
+ };
+
+ if (!open) return null;
+
+ return (
+ { setOpen(false); if (onClose) onClose(); }}>
+
e.stopPropagation()}>
+
{mode === 'edit' ? 'Edit App' : 'Add New App'}
+
+
+
+ );
+}
+
+export default AppModal;
\ No newline at end of file
diff --git a/frontend/src/components/ConfirmDialog.jsx b/frontend/src/components/ConfirmDialog.jsx
new file mode 100644
index 0000000..8eb6119
--- /dev/null
+++ b/frontend/src/components/ConfirmDialog.jsx
@@ -0,0 +1,18 @@
+import '../style/AddAppModal.css'; // reuse the modal style
+
+function ConfirmDialog({ message, onConfirm, onCancel }) {
+ return (
+
+
e.stopPropagation()}>
+
Confirm
+
{message}
+
+
+
+
+
+
+ );
+}
+
+export default ConfirmDialog;
diff --git a/frontend/src/components/SectionGrid.jsx b/frontend/src/components/SectionGrid.jsx
index 2a79207..dfe326c 100644
--- a/frontend/src/components/SectionGrid.jsx
+++ b/frontend/src/components/SectionGrid.jsx
@@ -1,11 +1,11 @@
import AppGrid from './AppGrid';
import '../style/SectionGrid.css';
-function SectionGrid({ section }) {
+function SectionGrid({ section, onEdit, onDelete }) {
return (
);
}
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js
index c483f9c..6794a9e 100644
--- a/frontend/src/services/api.js
+++ b/frontend/src/services/api.js
@@ -22,3 +22,25 @@ export async function getIconUrl(filename) {
const data = await res.json();
return data.url;
}
+
+export async function editAppInSection({ section, app }) {
+ const res = await fetch(`${API_BASE}/edit_app`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ section, app })
+ });
+
+ if (!res.ok) throw new Error(await res.text());
+ return await res.json();
+}
+
+export async function deleteAppFromSection({ section, app }) {
+ const res = await fetch(`${API_BASE}/delete_app`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ section, app })
+ });
+
+ if (!res.ok) throw new Error(await res.text());
+ return res.json();
+}
diff --git a/frontend/src/style/AppCard.css b/frontend/src/style/AppCard.css
index bcce67d..b859924 100644
--- a/frontend/src/style/AppCard.css
+++ b/frontend/src/style/AppCard.css
@@ -50,3 +50,31 @@
color: #ccc;
}
+.app-card-wrapper {
+ position: relative;
+}
+
+.card-icons-inside {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ display: flex;
+ gap: 6px;
+ z-index: 2;
+}
+
+.edit-icon,
+.delete-icon {
+ background-color: rgba(0, 0, 0, 0.6);
+ border-radius: 50%;
+ padding: 4px;
+ color: white;
+ cursor: pointer;
+ font-size: 0.8rem;
+ transition: transform 0.2s ease;
+}
+
+.edit-icon:hover,
+.delete-icon:hover {
+ transform: scale(1.2);
+}