Compare commits

..

4 Commits

Author SHA1 Message Date
Mikhail Kilin
1f25b9c104 Add confirmation modal for project deletion
All checks were successful
ci/woodpecker/push/build Pipeline was successful
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 15:44:38 +03:00
Mikhail Kilin
ea8463e8bc Add isAdmin URL parameter to bypass VPN check
All checks were successful
ci/woodpecker/push/build Pipeline was successful
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 15:40:09 +03:00
Mikhail Kilin
5cd6f5b96d Add delete button to project list on main page
All checks were successful
ci/woodpecker/push/build Pipeline was successful
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 15:22:37 +03:00
Mikhail Kilin
94bf067c41 Add VPN detection: show blocker when not on home IP
Check public IP via api.ipify.org on mount and display
"Выключи VPN" fullscreen message if it doesn't match
the expected home IP. Gracefully falls through on fetch errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 14:56:55 +03:00
2 changed files with 152 additions and 1 deletions

View File

@@ -70,6 +70,22 @@ h2 {
font-size: 0.85rem;
}
.project-delete {
background: #c0392b;
color: white;
border: none;
border-radius: 4px;
padding: 0.2rem 0.5rem;
font-size: 0.8rem;
cursor: pointer;
margin-left: 0.5rem;
flex-shrink: 0;
}
.project-delete:hover {
background: #e74c3c;
}
.back-btn {
padding: 0.4rem 1rem;
font-size: 0.9rem;
@@ -159,3 +175,67 @@ h2 {
.actions button.danger:hover {
background: #e74c3c;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
}
.modal {
background: white;
border-radius: 8px;
padding: 1.5rem 2rem;
min-width: 300px;
text-align: center;
}
.modal p {
font-size: 1.1rem;
margin-bottom: 1.5rem;
}
.modal-actions {
display: flex;
gap: 0.75rem;
justify-content: center;
}
.modal-actions button {
padding: 0.5rem 1.25rem;
font-size: 1rem;
cursor: pointer;
border: 1px solid #ccc;
border-radius: 4px;
background: #333;
color: white;
}
.modal-actions button:hover {
background: #555;
}
.modal-actions button.danger {
background: #c0392b;
}
.modal-actions button.danger:hover {
background: #e74c3c;
}
.vpn-block {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
font-size: 2rem;
font-family: system-ui, -apple-system, sans-serif;
color: #333;
}

View File

@@ -11,6 +11,28 @@ import {
} from "./api";
import "./App.css";
function ConfirmModal({
message,
onConfirm,
onCancel,
}: {
message: string;
onConfirm: () => void;
onCancel: () => void;
}) {
return (
<div className="modal-overlay" onClick={onCancel}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<p>{message}</p>
<div className="modal-actions">
<button onClick={onCancel}>Отмена</button>
<button className="danger" onClick={onConfirm}>Удалить</button>
</div>
</div>
</div>
);
}
function ProjectList({
onSelect,
}: {
@@ -18,6 +40,7 @@ function ProjectList({
}) {
const [projects, setProjects] = useState<ProjectMeta[]>([]);
const [name, setName] = useState("");
const [deleteId, setDeleteId] = useState<number | null>(null);
const load = async () => {
setProjects(await fetchProjects());
@@ -54,9 +77,29 @@ function ProjectList({
<span className="project-date">
{new Date(p.created_at).toLocaleString()}
</span>
<button
className="project-delete"
onClick={(e) => {
e.stopPropagation();
setDeleteId(p.id);
}}
>
X
</button>
</div>
))}
</div>
{deleteId !== null && (
<ConfirmModal
message="Удалить проект?"
onCancel={() => setDeleteId(null)}
onConfirm={async () => {
await deleteProject(deleteId);
setDeleteId(null);
await load();
}}
/>
)}
</>
);
}
@@ -74,6 +117,7 @@ function ProjectPage({
const [content, setContent] = useState("");
const [file, setFile] = useState<File | null>(null);
const [saving, setSaving] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
@@ -174,8 +218,15 @@ function ProjectPage({
{project.file_name && (
<button onClick={handleDownload}>Download file</button>
)}
<button className="danger" onClick={handleDelete}>Delete</button>
<button className="danger" onClick={() => setShowDeleteModal(true)}>Delete</button>
</div>
{showDeleteModal && (
<ConfirmModal
message="Удалить проект?"
onCancel={() => setShowDeleteModal(false)}
onConfirm={handleDelete}
/>
)}
</>
);
}
@@ -190,6 +241,21 @@ function parseHash(): { view: "list" | "project"; projectId: number | null } {
function App() {
const [view, setView] = useState<"list" | "project">(parseHash().view);
const [projectId, setProjectId] = useState<number | null>(parseHash().projectId);
const [vpnCheck, setVpnCheck] = useState<"loading" | "ok" | "vpn">("loading");
useEffect(() => {
const params = new URLSearchParams(window.location.search);
if (params.get("isAdmin") === "true") {
setVpnCheck("ok");
return;
}
fetch("https://api.ipify.org?format=json")
.then((r) => r.json())
.then((data: { ip: string }) => {
setVpnCheck(data.ip === "95.165.73.140" ? "ok" : "vpn");
})
.catch(() => setVpnCheck("ok"));
}, []);
useEffect(() => {
const onHashChange = () => {
@@ -209,6 +275,11 @@ function App() {
}
};
if (vpnCheck === "loading") return null;
if (vpnCheck === "vpn") {
return <div className="vpn-block">Выключи VPN</div>;
}
return (
<div className="app">
{view === "list" ? (