رفع الملفات إلى "frontend/src"
هذا الالتزام موجود في:
101
frontend/src/App.jsx
Normal file
101
frontend/src/App.jsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { Routes, Route, Link, useLocation } from "react-router-dom";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import MySQLMySQLMigrator from "./components/MySQLMySQLMigrator";
|
||||||
|
import PostgreSQLPostgreSQLMigrator from "./components/PostgreSQLPostgreSQLMigrator";
|
||||||
|
import PostgreSQLS3Migrator from "./components/PostgreSQLS3Migrator";
|
||||||
|
import S3S3Migrator from "./components/S3S3Migrator";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [darkMode, setDarkMode] = useState(() => {
|
||||||
|
const saved = localStorage.getItem('darkMode');
|
||||||
|
return saved ? JSON.parse(saved) : true; // Default to dark mode
|
||||||
|
});
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('darkMode', JSON.stringify(darkMode));
|
||||||
|
if (darkMode) {
|
||||||
|
document.body.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('dark');
|
||||||
|
}
|
||||||
|
}, [darkMode]);
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ path: "/mysql-mysql", label: "MySQL → MySQL", icon: "🗄️" },
|
||||||
|
{ path: "/psql-psql", label: "PostgreSQL → PostgreSQL", icon: "🐘" },
|
||||||
|
{ path: "/psql-s3", label: "PostgreSQL → S3", icon: "☁️" },
|
||||||
|
{ path: "/s3-s3", label: "S3 → S3", icon: "📦" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-black dark:bg-black text-gray-900 dark:text-white transition-colors">
|
||||||
|
<div className="container mx-auto px-4 py-8 max-w-6xl">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="bg-gray-900 dark:bg-gray-900 rounded-lg shadow-2xl p-6 mb-8 border border-red-600 glow-red">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-red-600 dark:text-red-500">
|
||||||
|
Universal Database Migrator
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-400 dark:text-white mt-2">
|
||||||
|
Seamlessly migrate your databases across platforms
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setDarkMode(!darkMode)}
|
||||||
|
className="px-4 py-2 bg-red-600 dark:bg-red-700 text-white rounded-lg hover:bg-red-700 dark:hover:bg-red-800 transition-colors shadow-lg"
|
||||||
|
>
|
||||||
|
{darkMode ? "☀️ Light" : "🌙 Dark"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<nav className="bg-gray-900 dark:bg-gray-900 rounded-lg shadow-2xl p-6 mb-8 border border-red-600 glow-red">
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.path}
|
||||||
|
to={item.path}
|
||||||
|
className={`block p-4 rounded-lg border-2 transition-all ${
|
||||||
|
location.pathname === item.path
|
||||||
|
? "border-red-500 bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-white"
|
||||||
|
: "border-gray-600 dark:border-gray-700 hover:border-red-300 dark:hover:border-red-600 hover:bg-gray-800 dark:hover:bg-gray-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-3xl mb-2">{item.icon}</div>
|
||||||
|
<div className="text-sm font-medium">{item.label}</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="bg-gray-900 dark:bg-gray-900 rounded-lg shadow-2xl p-6 border border-red-600 glow-red">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/mysql-mysql" element={<MySQLMySQLMigrator />} />
|
||||||
|
<Route path="/psql-psql" element={<PostgreSQLPostgreSQLMigrator />} />
|
||||||
|
<Route path="/psql-s3" element={<PostgreSQLS3Migrator />} />
|
||||||
|
<Route path="/s3-s3" element={<S3S3Migrator />} />
|
||||||
|
<Route
|
||||||
|
path="/"
|
||||||
|
element={
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="text-6xl mb-4">🚀</div>
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Welcome to Universal Migrator</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
|
Choose a migration type from the navigation above to get started.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
69
frontend/src/api.js
Normal file
69
frontend/src/api.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
// API Base URL من متغيرات البيئة
|
||||||
|
const API_BASE = import.meta.env.VITE_API_BASE ?? "";
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Schemas
|
||||||
|
// =========================
|
||||||
|
async function getSchemas(type, payload) {
|
||||||
|
const r = await fetch(`${API_BASE}/api/get_schemas`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
type,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
});
|
||||||
|
return r.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Tables
|
||||||
|
// =========================
|
||||||
|
async function getTables(type, payload) {
|
||||||
|
const r = await fetch(`${API_BASE}/api/get_tables`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
type,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
});
|
||||||
|
return r.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Start Migration
|
||||||
|
// =========================
|
||||||
|
async function startMigration(type, payload) {
|
||||||
|
const r = await fetch(`${API_BASE}/api/migrate`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
type,
|
||||||
|
...payload
|
||||||
|
})
|
||||||
|
});
|
||||||
|
return r.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Progress
|
||||||
|
// =========================
|
||||||
|
async function getProgress(type) {
|
||||||
|
const r = await fetch(`${API_BASE}/api/progress/${type}`);
|
||||||
|
return r.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// List Buckets
|
||||||
|
// =========================
|
||||||
|
async function listBuckets(payload) {
|
||||||
|
const r = await fetch(`${API_BASE}/api/list_buckets`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
return r.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getSchemas, getTables, startMigration, getProgress, listBuckets };
|
||||||
13
frontend/src/main.jsx
Normal file
13
frontend/src/main.jsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import App from "./App.jsx";
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
137
frontend/src/styles.css
Normal file
137
frontend/src/styles.css
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
@reference "tailwindcss";
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
LIGHT MODE STYLES
|
||||||
|
=========================== */
|
||||||
|
:root {
|
||||||
|
--bg-color: #f4f6f8;
|
||||||
|
--container-bg: white;
|
||||||
|
--text-color: #333;
|
||||||
|
--input-border: #ccc;
|
||||||
|
--button-bg: #2563eb;
|
||||||
|
--button-color: white;
|
||||||
|
--link-color: #2563eb;
|
||||||
|
--pre-bg: #111;
|
||||||
|
--pre-color: #0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
DARK MODE STYLES
|
||||||
|
=========================== */
|
||||||
|
.dark {
|
||||||
|
--bg-color: #000000;
|
||||||
|
--container-bg: #0d0d0d;
|
||||||
|
--text-color: #ffffff;
|
||||||
|
--input-border: #ff0000;
|
||||||
|
--button-bg: #b91c1c;
|
||||||
|
--button-color: #ffffff;
|
||||||
|
--link-color: #ef4444;
|
||||||
|
--pre-bg: #000000;
|
||||||
|
--pre-color: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
BODY STYLES
|
||||||
|
=========================== */
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
transition: background-color 0.3s, color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: var(--container-bg);
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
CUSTOM COMPONENTS
|
||||||
|
=========================== */
|
||||||
|
.glow-red {
|
||||||
|
box-shadow: 0 0 10px rgba(239, 68, 68, 0.5), 0 0 20px rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-input {
|
||||||
|
@apply w-full px-4 py-3 border border-gray-300 dark:border-red-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-red-500 focus:border-red-500 transition-all duration-200 placeholder-gray-400 dark:placeholder-gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button {
|
||||||
|
@apply bg-red-600 hover:bg-red-700 disabled:bg-gray-400 text-white font-medium py-3 px-4 rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl disabled:cursor-not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select {
|
||||||
|
@apply w-full px-4 py-3 border border-gray-300 dark:border-red-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-red-500 focus:border-red-500 transition-all duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-label {
|
||||||
|
@apply block text-sm font-medium mb-1 text-gray-700 dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
transition: background-color 0.3s, color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: var(--container-bg);
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border: 1px solid var(--input-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--container-bg);
|
||||||
|
color: var(--text-color);
|
||||||
|
transition: border-color 0.3s, background-color 0.3s, color 0.3s;
|
||||||
|
}
|
||||||
|
select.multi-select {
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: var(--button-bg);
|
||||||
|
color: var(--button-color);
|
||||||
|
border: none;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--link-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: var(--pre-bg);
|
||||||
|
color: var(--pre-color);
|
||||||
|
padding: 10px;
|
||||||
|
height: 140px;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
المرجع في مشكلة جديدة
حظر مستخدم