Compare commits
4 Commits
87eb93512b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f25b9c104 | ||
|
|
ea8463e8bc | ||
|
|
5cd6f5b96d | ||
|
|
94bf067c41 |
@@ -70,6 +70,22 @@ h2 {
|
|||||||
font-size: 0.85rem;
|
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 {
|
.back-btn {
|
||||||
padding: 0.4rem 1rem;
|
padding: 0.4rem 1rem;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
@@ -159,3 +175,67 @@ h2 {
|
|||||||
.actions button.danger:hover {
|
.actions button.danger:hover {
|
||||||
background: #e74c3c;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,28 @@ import {
|
|||||||
} from "./api";
|
} from "./api";
|
||||||
import "./App.css";
|
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({
|
function ProjectList({
|
||||||
onSelect,
|
onSelect,
|
||||||
}: {
|
}: {
|
||||||
@@ -18,6 +40,7 @@ function ProjectList({
|
|||||||
}) {
|
}) {
|
||||||
const [projects, setProjects] = useState<ProjectMeta[]>([]);
|
const [projects, setProjects] = useState<ProjectMeta[]>([]);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
|
const [deleteId, setDeleteId] = useState<number | null>(null);
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
setProjects(await fetchProjects());
|
setProjects(await fetchProjects());
|
||||||
@@ -54,9 +77,29 @@ function ProjectList({
|
|||||||
<span className="project-date">
|
<span className="project-date">
|
||||||
{new Date(p.created_at).toLocaleString()}
|
{new Date(p.created_at).toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
|
<button
|
||||||
|
className="project-delete"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setDeleteId(p.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
X
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</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 [content, setContent] = useState("");
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -174,8 +218,15 @@ function ProjectPage({
|
|||||||
{project.file_name && (
|
{project.file_name && (
|
||||||
<button onClick={handleDownload}>Download file</button>
|
<button onClick={handleDownload}>Download file</button>
|
||||||
)}
|
)}
|
||||||
<button className="danger" onClick={handleDelete}>Delete</button>
|
<button className="danger" onClick={() => setShowDeleteModal(true)}>Delete</button>
|
||||||
</div>
|
</div>
|
||||||
|
{showDeleteModal && (
|
||||||
|
<ConfirmModal
|
||||||
|
message="Удалить проект?"
|
||||||
|
onCancel={() => setShowDeleteModal(false)}
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -190,6 +241,21 @@ function parseHash(): { view: "list" | "project"; projectId: number | null } {
|
|||||||
function App() {
|
function App() {
|
||||||
const [view, setView] = useState<"list" | "project">(parseHash().view);
|
const [view, setView] = useState<"list" | "project">(parseHash().view);
|
||||||
const [projectId, setProjectId] = useState<number | null>(parseHash().projectId);
|
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(() => {
|
useEffect(() => {
|
||||||
const onHashChange = () => {
|
const onHashChange = () => {
|
||||||
@@ -209,6 +275,11 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (vpnCheck === "loading") return null;
|
||||||
|
if (vpnCheck === "vpn") {
|
||||||
|
return <div className="vpn-block">Выключи VPN</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
{view === "list" ? (
|
{view === "list" ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user