feat: initial Transmute app — universal client-side file converter

Full-stack client-side file converter with Next.js 15 static export.
Supports images (Canvas API), documents (mammoth/pdf-lib/jspdf),
audio/video (ffmpeg.wasm), and data formats (papaparse/yaml/xml).
Dark industrial UI with Space Grotesk + JetBrains Mono, animated
drop zone, glassmorphism file cards, progress rings, and ZIP downloads.
Zero server dependencies — files never leave the browser.
This commit is contained in:
noah
2026-03-09 18:07:47 +01:00
commit 7659136045
31 changed files with 9871 additions and 0 deletions
+114
View File
@@ -0,0 +1,114 @@
import Papa from 'papaparse';
import yaml from 'js-yaml';
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
import { ConversionResult } from '@/types';
import { buildOutputFilename, getMimeType } from '@/lib/utils';
import { getExtension } from '@/lib/fileDetector';
async function readFileAsText(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target?.result as string);
reader.onerror = () => reject(new Error('Failed to read file'));
reader.readAsText(file);
});
}
function csvToJson(text: string): object[] {
const result = Papa.parse(text, { header: true, skipEmptyLines: true });
return result.data as object[];
}
function jsonToCsv(data: unknown): string {
const arr = Array.isArray(data) ? data : [data];
return Papa.unparse(arr);
}
function tsvToJson(text: string): object[] {
const result = Papa.parse(text, { header: true, skipEmptyLines: true, delimiter: '\t' });
return result.data as object[];
}
function jsonToTsv(data: unknown): string {
const arr = Array.isArray(data) ? data : [data];
return Papa.unparse(arr, { delimiter: '\t' });
}
function xmlToJson(text: string): unknown {
const parser = new XMLParser({ ignoreAttributes: false });
return parser.parse(text);
}
function jsonToXml(data: unknown): string {
const builder = new XMLBuilder({ ignoreAttributes: false, format: true });
return builder.build(typeof data === 'string' ? JSON.parse(data) : data);
}
function jsonToYaml(data: unknown): string {
return yaml.dump(typeof data === 'string' ? JSON.parse(data) : data);
}
function yamlToJson(text: string): unknown {
return yaml.load(text);
}
async function toIntermediate(file: File, ext: string): Promise<unknown> {
const text = await readFileAsText(file);
switch (ext) {
case 'json':
return JSON.parse(text);
case 'csv':
return csvToJson(text);
case 'tsv':
return tsvToJson(text);
case 'xml':
return xmlToJson(text);
case 'yaml':
case 'yml':
return yamlToJson(text);
default:
throw new Error(`Unsupported source format: ${ext}`);
}
}
function fromIntermediate(data: unknown, targetFormat: string): string {
switch (targetFormat) {
case 'json':
return JSON.stringify(data, null, 2);
case 'csv':
return jsonToCsv(data);
case 'tsv':
return jsonToTsv(data);
case 'xml':
return jsonToXml(data);
case 'yaml':
case 'yml':
return jsonToYaml(data);
default:
throw new Error(`Unsupported target format: ${targetFormat}`);
}
}
export async function convertData(
file: File,
targetFormat: string,
onProgress?: (progress: number) => void
): Promise<ConversionResult> {
onProgress?.(20);
const ext = getExtension(file.name);
const intermediate = await toIntermediate(file, ext);
onProgress?.(60);
const output = fromIntermediate(intermediate, targetFormat);
onProgress?.(90);
const blob = new Blob([output], { type: getMimeType(targetFormat) });
onProgress?.(100);
return {
blob,
filename: buildOutputFilename(file.name, targetFormat),
};
}