Repo2TXT / index.html
ketannnn's picture
Project added
d5fdeca
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Repo2TXT | GitHub to Text Converter</title>
<meta name="description" content="Convert any GitHub repository into a single high-quality text file for easy analysis and documentation.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&family=Outfit:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--primary: #6366f1;
--primary-glow: rgba(99, 102, 241, 0.5);
--accent: #a855f7;
--bg: #0f172a;
--card-bg: rgba(30, 41, 59, 0.7);
--text-main: #f8fafc;
--text-muted: #94a3b8;
--border: rgba(255, 255, 255, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg);
color: var(--text-main);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow-x: hidden;
position: relative;
}
/* Animated Background Gradients */
body::before {
content: '';
position: absolute;
top: -10%;
left: -10%;
width: 40%;
height: 40%;
background: radial-gradient(circle, var(--primary-glow) 0%, transparent 70%);
z-index: -1;
filter: blur(80px);
animation: move 20s infinite alternate;
}
body::after {
content: '';
position: absolute;
bottom: -10%;
right: -10%;
width: 40%;
height: 40%;
background: radial-gradient(circle, rgba(168, 85, 247, 0.3) 0%, transparent 70%);
z-index: -1;
filter: blur(80px);
animation: move 25s infinite alternate-reverse;
}
@keyframes move {
from { transform: translate(0, 0); }
to { transform: translate(100px, 100px); }
}
.container {
width: 90%;
max-width: 600px;
padding: 2.5rem;
background: var(--card-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--border);
border-radius: 24px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
text-align: center;
z-index: 10;
animation: fadeIn 0.8s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
h1 {
font-family: 'Outfit', sans-serif;
font-size: 2.5rem;
font-weight: 800;
margin-bottom: 0.5rem;
background: linear-gradient(135deg, #fff 0%, var(--primary) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
p.subtitle {
color: var(--text-muted);
margin-bottom: 2rem;
font-size: 1.1rem;
}
.input-group {
position: relative;
margin-bottom: 1.5rem;
}
input {
width: 100%;
padding: 1.2rem 1.5rem;
background: rgba(15, 23, 42, 0.6);
border: 1px solid var(--border);
border-radius: 16px;
color: white;
font-size: 1rem;
transition: all 0.3s ease;
outline: none;
}
input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.2);
background: rgba(15, 23, 42, 0.8);
}
button {
width: 100%;
padding: 1.2rem;
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
border: none;
border-radius: 16px;
color: white;
font-family: 'Outfit', sans-serif;
font-size: 1.1rem;
font-weight: 700;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 10px 15px -3px rgba(99, 102, 241, 0.4);
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 20px 25px -5px rgba(99, 102, 241, 0.5);
filter: brightness(1.1);
}
button:active {
transform: translateY(0);
}
button:disabled {
background: #475569;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.loader {
display: none;
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.status {
margin-top: 1.5rem;
font-size: 0.9rem;
min-height: 1.2rem;
transition: color 0.3s ease;
}
.success { color: #10b981; }
.error { color: #ef4444; }
/* Process Log Styles */
.process-log {
margin-top: 2rem;
text-align: left;
background: rgba(15, 23, 42, 0.4);
padding: 1.5rem;
border-radius: 16px;
border: 1px solid var(--border);
}
.step {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
color: var(--text-muted);
font-size: 0.9rem;
transition: all 0.3s ease;
}
.step.active {
color: var(--text-main);
}
.step.completed {
color: #10b981;
}
.step-dot {
width: 8px;
height: 8px;
background: #475569;
border-radius: 50%;
transition: all 0.3s ease;
}
.step.active .step-dot {
background: var(--primary);
box-shadow: 0 0 10px var(--primary);
transform: scale(1.2);
}
.step.completed .step-dot {
background: #10b981;
}
footer {
margin-top: 2rem;
color: var(--text-muted);
font-size: 0.8rem;
}
/* Premium Micro-animations */
.glow-effect {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: -1;
border-radius: 16px;
transition: opacity 0.3s;
opacity: 0;
box-shadow: 0 0 20px var(--primary);
}
input:focus + .glow-effect {
opacity: 0.3;
}
</style>
</head>
<body>
<div class="container">
<h1>Repo2TXT</h1>
<p class="subtitle">Convert any GitHub repo to a single .txt file</p>
<div class="input-group">
<input id="repoUrl" type="text" placeholder="https://github.com/username/repo" autocomplete="off">
<div class="glow-effect"></div>
</div>
<button id="downloadBtn" onclick="handleDownload()">
<span id="btnText">Generate project.txt</span>
<div id="loader" class="loader"></div>
</button>
<div id="status" class="status"></div>
<div id="processLog" class="process-log" style="display: none;">
<div class="step" id="step1">
<div class="step-dot"></div>
<span>Validating repository URL...</span>
</div>
<div class="step" id="step2">
<div class="step-dot"></div>
<span>Establishing connection to backend...</span>
</div>
<div class="step" id="step3">
<div class="step-dot"></div>
<span>Cloning repository (sparse checkout)...</span>
</div>
<div class="step" id="step4">
<div class="step-dot"></div>
<span>Filtering and merging file contents...</span>
</div>
<div class="step" id="step5">
<div class="step-dot"></div>
<span>Finalizing project.txt...</span>
</div>
</div>
</div>
<footer>
&copy; 2024 Repo2TXT Premium. Processing is fast and secure.
</footer>
<script>
async function handleDownload() {
const urlInput = document.getElementById("repoUrl");
const btn = document.getElementById("downloadBtn");
const btnText = document.getElementById("btnText");
const loader = document.getElementById("loader");
const status = document.getElementById("status");
const processLog = document.getElementById("processLog");
const repoUrl = urlInput.value.trim();
if (!repoUrl) {
showStatus("Please enter a valid GitHub URL", "error");
return;
}
if (!repoUrl.includes("github.com")) {
showStatus("Invalid URL. Must be a GitHub repository.", "error");
return;
}
// UI State: Loading
btn.disabled = true;
btnText.textContent = "Processing...";
loader.style.display = "block";
status.textContent = "";
status.className = "status";
processLog.style.display = "block";
// Reset steps
document.querySelectorAll('.step').forEach(s => s.className = 'step');
const setStep = (id, state) => {
const el = document.getElementById(id);
if (el) el.className = `step ${state}`;
};
try {
// Step 1: Validate
setStep('step1', 'active');
await new Promise(r => setTimeout(r, 600));
setStep('step1', 'completed');
// Step 2: Connection
setStep('step2', 'active');
await new Promise(r => setTimeout(r, 400));
// Actual request starts here
const fetchPromise = fetch("/download", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ url: repoUrl })
});
// Simulate/Progress backend work in UI
setStep('step2', 'completed');
setStep('step3', 'active');
const response = await fetchPromise;
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || "Processing failed");
}
setStep('step3', 'completed');
setStep('step4', 'active');
await new Promise(r => setTimeout(r, 800)); // Visual buffer for large repos
setStep('step4', 'completed');
setStep('step5', 'active');
const blob = await response.blob();
setStep('step5', 'completed');
// Extract filename from header or use default
const contentDisposition = response.headers.get('Content-Disposition');
let filename = "project.txt";
if (contentDisposition && contentDisposition.indexOf('attachment') !== -1) {
const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
const matches = filenameRegex.exec(contentDisposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/[ '"]/g, '');
}
}
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = downloadUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(downloadUrl);
showStatus("Success! Your file is ready.", "success");
} catch (err) {
console.error(err);
showStatus(err.message, "error");
// Mark current active step as failed visually maybe?
document.querySelectorAll('.step.active').forEach(s => s.classList.add('error-step'));
} finally {
btn.disabled = false;
btnText.textContent = "Generate project.txt";
loader.style.display = "none";
}
}
function showStatus(message, type) {
const status = document.getElementById("status");
status.textContent = message;
status.className = `status ${type}`;
}
// Allow Enter key to trigger download
document.getElementById("repoUrl").addEventListener("keypress", function(e) {
if (e.key === "Enter") {
handleDownload();
}
});
</script>
</body>
</html>