Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
node_modules/
dist/
release/
.DS_Store
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.idea/
.vscode/
159 changes: 109 additions & 50 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@

import React, { useState, useCallback, useMemo } from 'react';
import { DumpOptions, Preset } from './types';
import { PRESETS } from './constants';
import { createCodeDump } from './services/zipProcessor';
import { OptionsPanel } from './components/OptionsPanel';
import { FileUpload } from './components/FileUpload';
import { OutputDisplay } from './components/OutputDisplay';
import { useTranslation } from './i18n';
import { LanguageSelector } from './components/LanguageSelector';
import { Button } from './components/ui/Button';

const DownloadIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
);

const CopyIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
);

const defaultOptions: DumpOptions = {
include: ['**/*'],
Expand All @@ -17,19 +31,23 @@ const defaultOptions: DumpOptions = {
strictText: false,
};

const LoadingIndicator: React.FC<{ progress: number }> = ({ progress }) => (
<div className="w-full h-full flex flex-col items-center justify-center bg-white/50 dark:bg-gray-900/50 rounded-lg">
<svg className="animate-spin -ml-1 mr-3 h-10 w-10 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p className="mt-4 text-lg font-semibold text-gray-700 dark:text-gray-200">Processing ZIP file...</p>
<div className="w-1/2 mt-2 bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
<div className="bg-blue-600 h-2.5 rounded-full" style={{ width: `${progress}%` }}></div>
const LoadingIndicator: React.FC<{ progress: number }> = ({ progress }) => {
const { t } = useTranslation();

return (
<div className="w-full h-full flex flex-col items-center justify-center bg-white/50 dark:bg-gray-900/50 rounded-lg">
<svg className="animate-spin -ml-1 mr-3 h-10 w-10 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p className="mt-4 text-lg font-semibold text-gray-700 dark:text-gray-200">{t('loading.processing')}</p>
<div className="w-1/2 mt-2 bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
<div className="bg-blue-600 h-2.5 rounded-full" style={{ width: `${progress}%` }}></div>
</div>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{t('loading.percentComplete', { percent: progress })}</p>
</div>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{progress}% complete</p>
</div>
);
);
};


function App() {
Expand All @@ -38,6 +56,7 @@ function App() {
const [output, setOutput] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [progress, setProgress] = useState(0);
const { t } = useTranslation();

const handlePresetChange = useCallback((preset: Preset) => {
const newOptions: DumpOptions = {
Expand All @@ -57,13 +76,18 @@ function App() {
if (file.type === 'application/zip' || file.type === 'application/x-zip-compressed') {
setZipFile(file);
} else {
alert('Please select a valid ZIP file.');
alert(t('app.invalidZip'));
}
}, []);
}, [t]);

const downloadName = useMemo(() => {
const normalized = zipFile?.name?.replace(/\.zip$/i, '') || 'codedump';
return `${normalized}.md`;
}, [zipFile]);

const handleGenerate = async () => {
if (!zipFile) {
alert('Please select a ZIP file first.');
alert(t('app.noZip'));
return;
}
setIsLoading(true);
Expand All @@ -75,24 +99,47 @@ function App() {
setOutput(markdownContent);
} catch (error) {
console.error('Failed to process ZIP file:', error);
alert(`An error occurred: ${error instanceof Error ? error.message : 'Unknown error'}`);
const message = error instanceof Error ? error.message : 'Unknown error';
alert(t('app.error', { message }));
setOutput(null);
} finally {
setIsLoading(false);
}
};

const handleReset = () => {
const handleReset = useCallback(() => {
setZipFile(null);
setOutput(null);
setIsLoading(false);
setProgress(0);
};
}, []);

const handleDownload = useCallback(() => {
if (!output) {
return;
}
const blob = new Blob([output], { type: 'text/markdown;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = downloadName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, [downloadName, output]);

const handleCopy = useCallback(() => {
if (!output) {
return;
}
navigator.clipboard.writeText(output);
}, [output]);

const memoizedOptionsPanel = useMemo(() => (
<OptionsPanel
options={options}
setOptions={setOptions}
<OptionsPanel
options={options}
setOptions={setOptions}
onPresetChange={handlePresetChange}
disabled={isLoading}
/>
Expand All @@ -101,38 +148,50 @@ function App() {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100 p-4 lg:p-8 font-sans">
<main className="max-w-screen-2xl mx-auto">
<header className="mb-8 text-center">
<h1 className="text-4xl font-extrabold tracking-tight text-gray-900 dark:text-white">CodeDump from ZIP</h1>
<p className="mt-2 text-lg text-gray-600 dark:text-gray-400">Generate a comprehensive Markdown file from your project's ZIP archive.</p>
<header className="mb-8">
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div className="text-center lg:text-left">
<h1 className="text-4xl font-extrabold tracking-tight text-gray-900 dark:text-white">{t('app.title')}</h1>
<p className="mt-2 text-lg text-gray-600 dark:text-gray-400">{t('app.subtitle')}</p>
</div>
<LanguageSelector />
</div>
</header>

<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 h-[calc(100vh-12rem)]">
<div className="lg:col-span-1 h-full">
{output && !isLoading && (
<div className="flex flex-wrap justify-end gap-3 mb-6">
<Button onClick={handleCopy} variant="secondary" leftIcon={<CopyIcon />}>{t('output.copy')}</Button>
<Button onClick={handleDownload} variant="primary" leftIcon={<DownloadIcon />}>{t('output.download')}</Button>
<Button onClick={handleReset} variant="secondary">{t('output.newDump')}</Button>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 h-[calc(100vh-12rem)]">
<div className="h-full flex flex-col">
{memoizedOptionsPanel}
</div>
<div className="lg:col-span-2 h-full">
{isLoading ? (
<LoadingIndicator progress={progress} />
) : output !== null ? (
<OutputDisplay content={output} filename={zipFile?.name || 'codedump'} onReset={handleReset} />
) : (
<div className="flex flex-col h-full gap-6">
<div className="flex-grow">
<FileUpload onFileSelect={handleFileSelect} disabled={isLoading} />
</div>
{zipFile && (
<div className="flex-shrink-0 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg p-4 flex justify-between items-center">
<p className="text-sm font-medium">Selected: <span className="font-bold text-blue-600 dark:text-blue-400">{zipFile.name}</span></p>
<button
onClick={handleGenerate}
className="px-6 py-2 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Generate CodeDump
</button>
</div>
)}
</div>
)}
<div className="h-full flex flex-col">
{isLoading ? (
<LoadingIndicator progress={progress} />
) : output !== null ? (
<OutputDisplay content={output} />
) : (
<div className="flex flex-col h-full gap-6">
<div className="flex-grow">
<FileUpload onFileSelect={handleFileSelect} disabled={isLoading} />
</div>
{zipFile && (
<div className="flex-shrink-0 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg p-4 flex justify-between items-center">
<p className="text-sm font-medium">{t('app.selectedFile', { filename: zipFile.name })}</p>
<button
onClick={handleGenerate}
className="px-6 py-2 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
{t('app.generate')}
</button>
</div>
)}
</div>
)}
</div>
</div>
</main>
Expand Down
6 changes: 4 additions & 2 deletions components/FileUpload.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

import React, { useState, useCallback } from 'react';
import { Card } from './ui/Card';
import { useTranslation } from '../i18n';

interface FileUploadProps {
onFileSelect: (file: File) => void;
Expand All @@ -16,6 +17,7 @@ const UploadIcon = () => (

export const FileUpload: React.FC<FileUploadProps> = ({ onFileSelect, disabled }) => {
const [isDragging, setIsDragging] = useState(false);
const { t } = useTranslation();

const handleDrag = useCallback((e: React.DragEvent) => {
e.preventDefault();
Expand Down Expand Up @@ -55,9 +57,9 @@ export const FileUpload: React.FC<FileUploadProps> = ({ onFileSelect, disabled }
<label htmlFor="file-upload" className="w-full text-center cursor-pointer">
<UploadIcon />
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
<span className="font-semibold text-blue-600 dark:text-blue-400">Upload a ZIP file</span> or drag and drop
<span className="font-semibold text-blue-600 dark:text-blue-400">{t('fileUpload.callToAction')}</span> {t('fileUpload.orDragDrop')}
</p>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-500">ZIP archive of your project</p>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-500">{t('fileUpload.hint')}</p>
</label>
</div>
</Card>
Expand Down
30 changes: 30 additions & 0 deletions components/LanguageSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import { useTranslation } from '../i18n';
import { Select } from './ui/Select';

export const LanguageSelector: React.FC = () => {
const { language, setLanguage, t } = useTranslation();

const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setLanguage(event.target.value as typeof language);
};

return (
<div className="flex items-center gap-3 justify-center lg:justify-end">
<label htmlFor="language-select" className="text-sm font-medium text-gray-700 dark:text-gray-300">
{t('language.selector')}
</label>
<div className="w-36">
<Select
id="language-select"
value={language}
onChange={handleChange}
>
<option value="en">{t('language.en')}</option>
<option value="de">{t('language.de')}</option>
</Select>
</div>

</div>
);
};
Loading