diff --git a/package-lock.json b/package-lock.json index e375268..897d0f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "clsx": "^2.1.1", "file-saver": "^2.0.5", "framer-motion": "^11.11.17", + "html2canvas": "^1.4.1", "lucide-react": "^0.456.0", "mini-svg-data-uri": "^1.4.4", "next": "15.0.3", @@ -2785,6 +2786,14 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/before-after-hook": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", @@ -3110,6 +3119,14 @@ "node": ">= 8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -4229,8 +4246,7 @@ "node_modules/file-saver": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", - "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", - "license": "MIT" + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" }, "node_modules/fill-range": { "version": "7.1.1", @@ -4737,6 +4753,18 @@ "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", "license": "CC0-1.0" }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -7448,6 +7476,14 @@ "node": ">=6" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -7741,6 +7777,14 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/victory-vendor": { "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", diff --git a/package.json b/package.json index 7c09162..359d96a 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "clsx": "^2.1.1", "file-saver": "^2.0.5", "framer-motion": "^11.11.17", + "html2canvas": "^1.4.1", "lucide-react": "^0.456.0", "mini-svg-data-uri": "^1.4.4", "next": "15.0.3", diff --git a/src/app/generator/customization-options.tsx b/src/app/generator/customization-options.tsx index d49c025..fd35a70 100644 --- a/src/app/generator/customization-options.tsx +++ b/src/app/generator/customization-options.tsx @@ -33,7 +33,10 @@ const CustomizationOptions: React.FC = ({ options, on {/* Use Icons */}
- +
+ + BOTH VIEW +
= ({ options, on
{/* Show Line Numbers */}
- +
+ + ASCII VIEW ONLY +
= ({ options, on
{/* Show Descriptions */}
- +
+ + ASCII VIEW ONLY +
= ({ options, on
{/* Root Directory */}
- +
+ + ASCII VIEW ONLY +
= ({ options, on
{/* Trailing Slash */}
- +
+ + ASCII VIEW ONLY +
(null) + const [showValidationDialog, setShowValidationDialog] = useState(false) + const [proceedWithLargeRepo, setProceedWithLargeRepo] = useState(false) const [copied, setCopied] = useState(false) const [expanded, setExpanded] = useState(false) - const [viewMode, setViewMode] = useState<"ascii" | "interactive">("ascii") + const [viewMode, setViewMode] = useState("ascii") const [searchTerm, setSearchTerm] = useState("") const [showDownloadMenu, setShowDownloadMenu] = useState(false) const inputRef = useRef(null) @@ -145,7 +157,7 @@ export default function RepoProjectStructure() { ) const handleFetchStructure = useCallback( - async (url: string = repoUrl) => { + async (url: string = repoUrl, skipValidation: boolean = false) => { if (!url) { setValidation({ message: "Repository URL is required", isError: true }) return @@ -161,7 +173,22 @@ export default function RepoProjectStructure() { setLoading(true) try { - const tree = await fetchProjectStructure(url, repoType) + const { tree, validation: repoVal } = await fetchProjectStructure(url, repoType) + setRepoValidation(repoVal) + + // Check if we should show validation warnings + if (!skipValidation && !repoVal.isValid) { + setShowValidationDialog(true) + setLoading(false) + return + } + + if (!skipValidation && repoVal.warnings.length > 0 && !proceedWithLargeRepo) { + setShowValidationDialog(true) + setLoading(false) + return + } + const map = generateStructure(tree) setStructureMap(map) setValidation({ message: "", isError: false }) @@ -170,6 +197,10 @@ export default function RepoProjectStructure() { const { fileTypes, languages } = analyzeRepository(map) setFileTypeData(fileTypes) setLanguageData(languages) + + // Reset validation dialog state + setShowValidationDialog(false) + setProceedWithLargeRepo(false) } catch (err: unknown) { if (err instanceof Error) { console.error(err) @@ -184,12 +215,19 @@ export default function RepoProjectStructure() { isError: true, }) } + setRepoValidation(null) } setLoading(false) }, - [repoUrl, repoType], + [repoUrl, repoType, proceedWithLargeRepo], ) + const handleProceedWithLargeRepo = useCallback(() => { + setProceedWithLargeRepo(true) + setShowValidationDialog(false) + handleFetchStructure(repoUrl, true) + }, [repoUrl, handleFetchStructure]) + useEffect(() => { const savedUrl = localStorage.getItem("lastRepoUrl") if (savedUrl) { @@ -219,17 +257,21 @@ export default function RepoProjectStructure() { } }, []) + // Memoized filtering with performance optimization const filterStructure = useCallback((map: DirectoryMap, term: string): DirectoryMap => { + if (!term.trim()) return map + const filteredMap: DirectoryMap = new Map() + const lowerTerm = term.toLowerCase() for (const [key, value] of map.entries()) { if (value && typeof value === "object" && "type" in value && value.type === "file") { - if (key.toLowerCase().includes(term.toLowerCase())) { + if (key.toLowerCase().includes(lowerTerm)) { filteredMap.set(key, value) } } else if (value instanceof Map) { const filteredSubMap = filterStructure(value, term) - if (filteredSubMap.size > 0 || key.toLowerCase().includes(term.toLowerCase())) { + if (filteredSubMap.size > 0 || key.toLowerCase().includes(lowerTerm)) { filteredMap.set(key, filteredSubMap) } } @@ -243,10 +285,15 @@ export default function RepoProjectStructure() { [filterStructure, structureMap, searchTerm], ) - const customizedStructure = useMemo( - () => buildStructureString(filteredStructureMap, "", customizationOptions), - [filteredStructureMap, customizationOptions], - ) + // Memoized structure string with performance optimization + const customizedStructure = useMemo(() => { + // For very large structures, limit rendering to prevent performance issues + const mapSize = structureMap.size + if (mapSize > PERFORMANCE_THRESHOLDS.LARGE_REPO_ENTRIES) { + return buildStructureString(filteredStructureMap, "", customizationOptions, "", 20) // Limit depth + } + return buildStructureString(filteredStructureMap, "", customizationOptions) + }, [filteredStructureMap, customizationOptions, structureMap.size]) const copyToClipboard = useCallback(() => { navigator.clipboard.writeText(customizedStructure).then(() => { @@ -259,6 +306,7 @@ export default function RepoProjectStructure() { setRepoUrl("") localStorage.removeItem("lastRepoUrl") setStructureMap(new Map()) + setRepoValidation(null) if (inputRef.current) { inputRef.current.focus() } @@ -275,19 +323,19 @@ export default function RepoProjectStructure() { switch (format) { case "md": - content = `# Repository Structure\n\n\`\`\`\n${customizedStructure}\`\`\`` + content = `# Directory Structure\n\n\`\`\`\n${customizedStructure}\`\`\`` mimeType = "text/markdown;charset=utf-8" fileName = "README.md" break case "txt": content = customizedStructure mimeType = "text/plain;charset=utf-8" - fileName = "repository-structure.txt" + fileName = "directory-structure.txt" break case "json": content = JSON.stringify(convertMapToJson(filteredStructureMap), null, 2) mimeType = "application/json;charset=utf-8" - fileName = "repository-structure.json" + fileName = "directory-structure.json" break case "html": content = ` @@ -305,7 +353,7 @@ export default function RepoProjectStructure() { ` mimeType = "text/html;charset=utf-8" - fileName = "repository-structure.html" + fileName = "directory-structure.html" break } @@ -334,12 +382,85 @@ export default function RepoProjectStructure() { return (
+ {/* Validation Dialog */} + + + + + {repoValidation?.isValid === false ? ( + + ) : ( + + )} + Repository Size Warning + + + + {repoValidation && ( +
+
+

Repository Statistics:

+
    +
  • • Total entries: {repoValidation.totalEntries.toLocaleString()}
  • +
  • • Estimated size: {(repoValidation.estimatedSize / (1024 * 1024)).toFixed(2)}MB
  • +
+
+ + {repoValidation.errors.length > 0 && ( + + + +
    + {repoValidation.errors.map((error, index) => ( +
  • • {error}
  • + ))} +
+
+
+ )} + + {repoValidation.warnings.length > 0 && ( + + + +
    + {repoValidation.warnings.map((warning, index) => ( +
  • • {warning}
  • + ))} +
+
+
+ )} + +
+ {repoValidation.isValid && ( + + )} + +
+
+ )} +
+
+ - + RepoTreeGenerator

@@ -351,6 +472,20 @@ export default function RepoProjectStructure() { {/* Token Status */} + {/* Repository Validation Status */} + {repoValidation && structureMap.size > 0 && ( + + + + Repository processed: {repoValidation.totalEntries.toLocaleString()} entries, + estimated size: {(repoValidation.estimatedSize / (1024 * 1024)).toFixed(2)}MB + {repoValidation.totalEntries > PERFORMANCE_THRESHOLDS.LARGE_REPO_ENTRIES && + " (Large repository - some features may be slower)" + } + + + )} +

{/* Repository Type Select */}
@@ -380,7 +515,7 @@ export default function RepoProjectStructure() { {/* Repository URL Input with Private Repos Label */}
-
+
@@ -447,24 +582,40 @@ export default function RepoProjectStructure() { {/* View Mode Tabs */} + {/* Settings Dialog */} @@ -489,92 +640,96 @@ export default function RepoProjectStructure() {
- {/* Search Input */} -
- setSearchTerm(e.target.value)} - className="pl-8 w-full sm:w-48 h-8 text-sm bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600" - /> - -
+ {/* Search Input - Only show for ASCII and Interactive modes */} + {viewMode !== "analysis" && ( +
+ setSearchTerm(e.target.value)} + className="pl-8 w-full sm:w-48 h-8 text-sm bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600" + /> + +
+ )} + + {/* Action Buttons Row - Only show for ASCII and Interactive modes */} + {viewMode !== "analysis" && ( +
+ {/* Download Dropdown */} +
+ + + {showDownloadMenu && structureMap.size > 0 && ( +
+
+ File Format +
+ + + + +
+ )} +
- {/* Action Buttons Row */} -
- {/* Download Dropdown */} -
- - {showDownloadMenu && structureMap.size > 0 && ( -
-
- File Format -
- - - - -
- )} -
- - - -
+ +
+ )}
@@ -582,27 +737,52 @@ export default function RepoProjectStructure() { {/* Code Block */}
{viewMode === "ascii" ? ( - - {customizedStructure - ? customizedStructure - : searchTerm - ? noResultsMessage(searchTerm) - : noStructureMessage} - - ) : filteredStructureMap.size > 0 ? ( -
- +
{/* CSS containment for performance */} + + {customizedStructure + ? customizedStructure + : searchTerm + ? noResultsMessage(searchTerm) + : noStructureMessage} + +
+ ) : viewMode === "interactive" ? ( + filteredStructureMap.size > 0 ? ( +
+ +
+ ) : ( + + {searchTerm ? noResultsMessage(searchTerm) : noStructureMessage} + + ) + ) : viewMode === "analysis" && structureMap.size > 0 ? ( +
+
+

Repository Analysis

+

Visual breakdown of file types and programming languages in your repository.

+
+
) : ( - {searchTerm ? noResultsMessage(searchTerm) : noStructureMessage} + {noStructureMessage} )}
- {structureMap.size > 0 && ( -
-

Repository Analysis

- -
- )} {/* */}
diff --git a/src/app/legal/cookie-policy/page.tsx b/src/app/legal/cookie-policy/page.tsx index 64ed6c8..3e286a4 100644 --- a/src/app/legal/cookie-policy/page.tsx +++ b/src/app/legal/cookie-policy/page.tsx @@ -1,29 +1,57 @@ -'use client'; +import type { Metadata } from 'next'; +import BackButton from '@/components/back-button'; -import { useRouter } from 'next/navigation'; - -import { Button } from '@/components/ui/button'; -import { ArrowLeft } from 'lucide-react'; +export const metadata: Metadata = { + title: 'Cookie Policy - RepoTree', + description: + 'RepoTree Cookie Policy: We do not use cookies, tracking technologies, or third-party trackers. Full details here.', + robots: { index: true, follow: true }, + alternates: { + canonical: 'https://ascii-repotree.vercel.app/legal/cookie-policy', + }, +}; const CookiePolicy = () => { - const router = useRouter(); + const lastUpdated = '2025-07-21'; + + const structuredData = { + '@context': 'https://schema.org', + '@type': 'WebPage', + name: 'RepoTree Cookie Policy', + url: 'https://ascii-repotree.vercel.app/legal/cookie-policy', + dateModified: lastUpdated, + publisher: { + '@type': 'Organization', + name: 'RepoTree', + url: 'https://ascii-repotree.vercel.app', + }, + description: + 'RepoTree does not use cookies or tracking technologies. We do not place any cookies, including essential, functional, analytical, or advertising cookies.', + }; return ( -
+
+ {/* JSON-LD Structured Data */} +