Merge pull request 'add-buttons' (#18) from add-buttons into master
Reviewed-on: #18
This commit is contained in:
commit
2e0f554f3d
@ -9,7 +9,7 @@ steps:
|
|||||||
include: [ frontend/** ]
|
include: [ frontend/** ]
|
||||||
settings:
|
settings:
|
||||||
registry: harbor.dvirlabs.com
|
registry: harbor.dvirlabs.com
|
||||||
repo: my-apps/navix-frontend
|
repo: my-apps/${CI_REPO_NAME}-frontend
|
||||||
dockerfile: frontend/Dockerfile
|
dockerfile: frontend/Dockerfile
|
||||||
context: frontend
|
context: frontend
|
||||||
tags:
|
tags:
|
||||||
@ -30,7 +30,7 @@ steps:
|
|||||||
include: [ backend/** ]
|
include: [ backend/** ]
|
||||||
settings:
|
settings:
|
||||||
registry: harbor.dvirlabs.com
|
registry: harbor.dvirlabs.com
|
||||||
repo: my-apps/navix-backend
|
repo: my-apps/${CI_REPO_NAME}-backend
|
||||||
dockerfile: backend/Dockerfile
|
dockerfile: backend/Dockerfile
|
||||||
context: backend
|
context: backend
|
||||||
tags:
|
tags:
|
||||||
@ -63,8 +63,8 @@ steps:
|
|||||||
- |
|
- |
|
||||||
TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}"
|
TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}"
|
||||||
echo "💡 Setting frontend tag to: $TAG"
|
echo "💡 Setting frontend tag to: $TAG"
|
||||||
yq -i ".frontend.tag = \"$TAG\"" manifests/navix/values.yaml
|
yq -i ".frontend.tag = \"$TAG\"" manifests/${CI_REPO_NAME}/values.yaml
|
||||||
git add manifests/navix/values.yaml
|
git add manifests/${CI_REPO_NAME}/values.yaml
|
||||||
git commit -m "frontend: update tag to $TAG" || echo "No changes"
|
git commit -m "frontend: update tag to $TAG" || echo "No changes"
|
||||||
git push origin HEAD
|
git push origin HEAD
|
||||||
|
|
||||||
@ -89,7 +89,7 @@ steps:
|
|||||||
- |
|
- |
|
||||||
TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}"
|
TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}"
|
||||||
echo "💡 Setting backend tag to: $TAG"
|
echo "💡 Setting backend tag to: $TAG"
|
||||||
yq -i ".backend.tag = \"$TAG\"" manifests/navix/values.yaml
|
yq -i ".backend.tag = \"$TAG\"" manifests/${CI_REPO_NAME}/values.yaml
|
||||||
git add manifests/navix/values.yaml
|
git add manifests/${CI_REPO_NAME}/values.yaml
|
||||||
git commit -m "backend: update tag to $TAG" || echo "No changes"
|
git commit -m "backend: update tag to $TAG" || echo "No changes"
|
||||||
git push origin HEAD
|
git push origin HEAD
|
||||||
|
|||||||
Binary file not shown.
@ -81,6 +81,58 @@ def add_app(entry: AppEntry):
|
|||||||
|
|
||||||
return {"status": "added"}
|
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}")
|
@router.get("/icon/{filename}")
|
||||||
def get_public_icon_url(filename: str):
|
def get_public_icon_url(filename: str):
|
||||||
url = f"https://{MINIO_ENDPOINT}/{BUCKET}/{filename}"
|
url = f"https://{MINIO_ENDPOINT}/{BUCKET}/{filename}"
|
||||||
|
|||||||
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
@ -22,3 +22,6 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
# Custom files
|
||||||
|
env.js
|
||||||
@ -1,19 +1,49 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import SectionGrid from './components/SectionGrid';
|
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 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() {
|
function App() {
|
||||||
const [sections, setSections] = useState([]);
|
const [sections, setSections] = useState([]);
|
||||||
|
const [editData, setEditData] = useState(null);
|
||||||
|
const [showAdd, setShowAdd] = useState(false);
|
||||||
|
const [confirmData, setConfirmData] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const loadSections = () => {
|
||||||
fetchSections()
|
fetchSections()
|
||||||
.then(data => setSections(data.sections || []))
|
.then(data => setSections(data.sections || []))
|
||||||
.catch(err => console.error('Failed to fetch sections:', err));
|
.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 (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<Clock />
|
<Clock />
|
||||||
@ -21,12 +51,52 @@ function App() {
|
|||||||
<img src="/navix-logo.svg" alt="Navix logo" className="navix-logo" />
|
<img src="/navix-logo.svg" alt="Navix logo" className="navix-logo" />
|
||||||
Navix
|
Navix
|
||||||
</h1>
|
</h1>
|
||||||
<AddAppModal onAdded={() => window.location.reload()} />
|
|
||||||
{sections.map((section) => (
|
{showAdd && (
|
||||||
<SectionGrid key={section.name} section={section} />
|
<AppModal
|
||||||
|
mode="add"
|
||||||
|
onSubmit={addAppToSection}
|
||||||
|
onClose={() => {
|
||||||
|
setShowAdd(false);
|
||||||
|
loadSections();
|
||||||
|
}}
|
||||||
|
sections={sections}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{editData && (
|
||||||
|
<AppModal
|
||||||
|
mode="edit"
|
||||||
|
initialData={editData}
|
||||||
|
onSubmit={editAppInSection}
|
||||||
|
onClose={() => {
|
||||||
|
setEditData(null);
|
||||||
|
loadSections();
|
||||||
|
}}
|
||||||
|
sections={sections}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{confirmData && (
|
||||||
|
<ConfirmDialog
|
||||||
|
message={confirmData.message}
|
||||||
|
onConfirm={confirmDelete}
|
||||||
|
onCancel={() => setConfirmData(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sections.map(section => (
|
||||||
|
<SectionGrid
|
||||||
|
key={section.name}
|
||||||
|
section={section}
|
||||||
|
onEdit={setEditData}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<IoIosAdd className="add-button" onClick={() => setShowAdd(true)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
@ -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 (
|
|
||||||
<>
|
|
||||||
<IoIosAdd className="add-button" onClick={() => setOpen(true)} />
|
|
||||||
|
|
||||||
{open && (
|
|
||||||
<div className="modal-overlay" onClick={() => setOpen(false)}>
|
|
||||||
<div className="modal" onClick={(e) => e.stopPropagation()}>
|
|
||||||
<h2>Add New App</h2>
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<select
|
|
||||||
value={customSection ? '__new__' : section}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value;
|
|
||||||
if (val === '__new__') {
|
|
||||||
setSection('');
|
|
||||||
setCustomSection(true);
|
|
||||||
} else {
|
|
||||||
setSection(val);
|
|
||||||
setCustomSection(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
required={!customSection}
|
|
||||||
>
|
|
||||||
<option value="" disabled>Select a section</option>
|
|
||||||
{sections.map(s => (
|
|
||||||
<option key={s.name} value={s.name}>{s.name}</option>
|
|
||||||
))}
|
|
||||||
<option value="__new__">➕ Create new section...</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
{customSection && (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="New section name"
|
|
||||||
value={section}
|
|
||||||
onChange={e => setSection(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<input type="text" placeholder="App name" value={name} onChange={e => setName(e.target.value)} required />
|
|
||||||
<input type="text" placeholder="Icon URL" value={icon} onChange={e => setIcon(e.target.value)} />
|
|
||||||
<input type="text" placeholder="Description" value={description} onChange={e => setDescription(e.target.value)} />
|
|
||||||
<input type="text" placeholder="App URL" value={url} onChange={e => setUrl(e.target.value)} required />
|
|
||||||
<button type="submit">Add</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AddAppModal;
|
|
||||||
@ -1,32 +1,38 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import '../style/AppCard.css';
|
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);
|
const [iconUrl, setIconUrl] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (app.icon) {
|
if (app.icon) {
|
||||||
getIconUrl(app.icon)
|
getIconUrl(app.icon)
|
||||||
.then((url) => {
|
.then((url) => setIconUrl(url))
|
||||||
console.log('Presigned icon URL for', app.name, ':', url);
|
.catch((err) => console.error(`Failed to load icon for ${app.name}:`, err));
|
||||||
setIconUrl(url);
|
}
|
||||||
})
|
}, [app.icon, app.name]);
|
||||||
.catch((err) => {
|
|
||||||
console.error(`Failed to load icon for ${app.name}:`, err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [app.icon, app.name]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-card-wrapper">
|
<div className="app-card-wrapper">
|
||||||
<a href={app.url} className="app-card" target="_blank" rel="noreferrer">
|
<a href={app.url} className="app-card" target="_blank" rel="noreferrer">
|
||||||
|
<div className="card-icons-inside">
|
||||||
|
<FaTrash className="delete-icon" onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (typeof onDelete === 'function') {
|
||||||
|
onDelete({ ...app, section });
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
<FaEdit className="edit-icon" onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (typeof onEdit === 'function') {
|
||||||
|
onEdit({ ...app, section });
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
<div className="app-icon-wrapper">
|
<div className="app-icon-wrapper">
|
||||||
{iconUrl ? (
|
{iconUrl ? <img src={iconUrl} alt={app.name} className="app-icon" /> : <span className="icon-placeholder">⚠️</span>}
|
||||||
<img src={iconUrl} alt={app.name} className="app-icon" />
|
|
||||||
) : (
|
|
||||||
<span className="icon-placeholder">⚠️</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<h3>{app.name}</h3>
|
<h3>{app.name}</h3>
|
||||||
<p>{app.description}</p>
|
<p>{app.description}</p>
|
||||||
@ -35,4 +41,4 @@ function AppCard({ app }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppCard;
|
export default AppCard;
|
||||||
@ -1,14 +1,14 @@
|
|||||||
import AppCard from './AppCard';
|
import AppCard from './AppCard';
|
||||||
import '../style/AppGrid.css';
|
import '../style/AppGrid.css';
|
||||||
|
|
||||||
function AppGrid({ apps }) {
|
function AppGrid({ apps, section, onEdit, onDelete }) {
|
||||||
return (
|
return (
|
||||||
<div className="app-grid">
|
<div className="app-grid">
|
||||||
{apps.map(app => (
|
{apps.map(app => (
|
||||||
<AppCard key={app.name} app={app} />
|
<AppCard key={app.name} app={app} section={section} onEdit={onEdit} onDelete={onDelete} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppGrid;
|
export default AppGrid;
|
||||||
70
frontend/src/components/AppModal.jsx
Normal file
70
frontend/src/components/AppModal.jsx
Normal file
@ -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 (
|
||||||
|
<div className="modal-overlay" onClick={() => { setOpen(false); if (onClose) onClose(); }}>
|
||||||
|
<div className="modal" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<h2>{mode === 'edit' ? 'Edit App' : 'Add New App'}</h2>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<select
|
||||||
|
value={customSection ? '__new__' : section}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (val === '__new__') {
|
||||||
|
setSection('');
|
||||||
|
setCustomSection(true);
|
||||||
|
} else {
|
||||||
|
setSection(val);
|
||||||
|
setCustomSection(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
required={!customSection}
|
||||||
|
disabled={mode === 'edit'}
|
||||||
|
>
|
||||||
|
<option value="" disabled>Select a section</option>
|
||||||
|
{sections.map(s => (
|
||||||
|
<option key={s.name} value={s.name}>{s.name}</option>
|
||||||
|
))}
|
||||||
|
<option value="__new__">➕ Create new section...</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{customSection && (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="New section name"
|
||||||
|
value={section}
|
||||||
|
onChange={e => setSection(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<input type="text" placeholder="App name" value={name} onChange={e => setName(e.target.value)} required />
|
||||||
|
<input type="text" placeholder="Icon URL" value={icon} onChange={e => setIcon(e.target.value)} />
|
||||||
|
<input type="text" placeholder="Description" value={description} onChange={e => setDescription(e.target.value)} />
|
||||||
|
<input type="text" placeholder="App URL" value={url} onChange={e => setUrl(e.target.value)} required />
|
||||||
|
<button type="submit">{mode === 'edit' ? 'Update' : 'Add'}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppModal;
|
||||||
18
frontend/src/components/ConfirmDialog.jsx
Normal file
18
frontend/src/components/ConfirmDialog.jsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import '../style/AddAppModal.css'; // reuse the modal style
|
||||||
|
|
||||||
|
function ConfirmDialog({ message, onConfirm, onCancel }) {
|
||||||
|
return (
|
||||||
|
<div className="modal-overlay" onClick={onCancel}>
|
||||||
|
<div className="modal" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<h2>Confirm</h2>
|
||||||
|
<p style={{ color: '#ccc', textAlign: 'center', marginBottom: '1.5rem' }}>{message}</p>
|
||||||
|
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||||
|
<button style={{ background: 'gray' }} onClick={onCancel}>Cancel</button>
|
||||||
|
<button onClick={onConfirm}>Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConfirmDialog;
|
||||||
@ -1,11 +1,11 @@
|
|||||||
import AppGrid from './AppGrid';
|
import AppGrid from './AppGrid';
|
||||||
import '../style/SectionGrid.css';
|
import '../style/SectionGrid.css';
|
||||||
|
|
||||||
function SectionGrid({ section }) {
|
function SectionGrid({ section, onEdit, onDelete }) {
|
||||||
return (
|
return (
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<h2 className="section-title">{section.name}</h2>
|
<h2 className="section-title">{section.name}</h2>
|
||||||
<AppGrid apps={section.apps} />
|
<AppGrid apps={section.apps} section={section.name} onEdit={onEdit} onDelete={onDelete} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,3 +22,25 @@ export async function getIconUrl(filename) {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
return data.url;
|
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();
|
||||||
|
}
|
||||||
|
|||||||
@ -50,3 +50,31 @@
|
|||||||
color: #ccc;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user