Code cleanup

This commit is contained in:
2026-02-12 20:01:17 +01:00
parent f97f31a4df
commit 0978cc6194
2 changed files with 517 additions and 371 deletions

View File

@@ -1,5 +1,9 @@
import React, { useState, useRef } from "react";
import "../App.css";
import "../styles.css";
// ============================================================================
// CONSTANTS
// ============================================================================
const DEMO_TABS = `
4 6 -5 5 -4 4 3
@@ -7,29 +11,44 @@ const DEMO_TABS = `
4 4 4 5 5' 5 5 5 -5 5 4
`;
const HarmonicaTabGenerator = () => {
const [input, setInput] = useState(DEMO_TABS);
const [title, setTitle] = useState("My Harmonica Tab");
const [maxLinesPerColumn, setMaxLinesPerColumn] = useState(10);
const svgRef = useRef(null);
const SVG_CONFIG = {
lineHeight: 40,
fontSize: 28,
annotationFontSize: 12,
titleFontSize: 36,
titleHeight: 80,
padding: 40,
columnGap: 80,
charWidth: 17.3, // For tab lines (monospace)
textCharWidth: 7, // For annotation lines
minColumnWidth: 200,
canvasScale: 2, // For PNG export quality
backgroundColor: "#fffef5",
};
// Parse input and add + signs to positive numbers
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
/**
* Parse input text and identify tab lines vs annotation lines
* Adds + signs to positive numbers in tab lines
*/
const parseInput = (text) => {
const lines = text.split("\n");
return lines.map((line) => {
// Check if line contains tab notation (numbers with optional minus signs, quotes/backticks for bends, and spaces)
// Tab lines contain only numbers, spaces, minus signs, and bend markers
const isTabLine =
/^[\s\d\-'"`'']+$/.test(line.trim()) && line.trim().length > 0;
let processedContent = line;
// If it's a tab line, add + signs to positive numbers (but preserve bend notation)
// Add + signs to positive numbers in tab lines
if (isTabLine) {
processedContent = line.replace(
/(\s|^)(\d+)(['"`'']?)/g,
(match, space, number, bend) => {
return space + "+" + number + bend;
},
(match, space, number, bend) => space + "+" + number + bend,
);
}
@@ -40,8 +59,10 @@ const HarmonicaTabGenerator = () => {
});
};
// Calculate layout (columns based on max lines per column)
const calculateLayout = (parsedLines) => {
/**
* Calculate column layout based on max lines per column
*/
const calculateLayout = (parsedLines, maxLinesPerColumn) => {
const tabLines = parsedLines.filter((l) => l.isTab);
const numColumns = Math.ceil(tabLines.length / maxLinesPerColumn);
@@ -52,10 +73,7 @@ const HarmonicaTabGenerator = () => {
const columnLines = [];
let tabCount = 0;
while (
currentIndex < parsedLines.length &&
tabCount < maxLinesPerColumn
) {
while (currentIndex < parsedLines.length && tabCount < maxLinesPerColumn) {
const line = parsedLines[currentIndex];
columnLines.push(line);
@@ -71,60 +89,89 @@ const HarmonicaTabGenerator = () => {
return columns;
};
const parsedLines = parseInput(input);
const columns = calculateLayout(parsedLines);
// Calculate the width of each column based on content
/**
* Calculate the width needed for a column based on its content
*/
const calculateColumnWidth = (column) => {
let maxLength = 0;
column.forEach((line) => {
if (line.content.trim()) {
// Adjusted for letterSpacing of 0.5 instead of 2
// Monospace at 28px is about 16.8px per char + 0.5px spacing
const charWidth = line.isTab ? 17.3 : 7;
const charWidth = line.isTab
? SVG_CONFIG.charWidth
: SVG_CONFIG.textCharWidth;
const length = line.content.length * charWidth;
maxLength = Math.max(maxLength, length);
}
});
return Math.max(maxLength, 200); // Minimum 200px per column
return Math.max(maxLength, SVG_CONFIG.minColumnWidth);
};
const columnWidths = columns.map(calculateColumnWidth);
/**
* Calculate SVG dimensions based on content
*/
const calculateSVGDimensions = (columns, columnWidths) => {
const totalContentWidth = columnWidths.reduce((sum, width) => sum + width, 0);
// SVG dimensions and styling
const lineHeight = 40;
const fontSize = 28;
const annotationFontSize = 12;
const titleFontSize = 36;
const titleHeight = 80;
const padding = 40;
const columnGap = 80;
const maxLinesInAnyColumn = Math.max(
...columns.map((col) => {
return col.reduce((sum, line) => {
return sum + (line.isTab ? 1 : 0.5);
}, 0);
return col.reduce((sum, line) => sum + (line.isTab ? 1 : 0.5), 0);
}),
1,
);
const svgWidth =
totalContentWidth + (columns.length - 1) * columnGap + padding * 2;
const svgHeight =
maxLinesInAnyColumn * lineHeight + padding * 2 + titleHeight;
totalContentWidth +
(columns.length - 1) * SVG_CONFIG.columnGap +
SVG_CONFIG.padding * 2;
// Export as PNG
const svgHeight =
maxLinesInAnyColumn * SVG_CONFIG.lineHeight +
SVG_CONFIG.padding * 2 +
SVG_CONFIG.titleHeight;
return { svgWidth, svgHeight };
};
// ============================================================================
// MAIN COMPONENT
// ============================================================================
const HarmonicaTabGenerator = () => {
// State
const [input, setInput] = useState(DEMO_TABS);
const [title, setTitle] = useState("My Harmonica Tab");
const [maxLinesPerColumn, setMaxLinesPerColumn] = useState(10);
// Ref for SVG element
const svgRef = useRef(null);
// ============================================================================
// COMPUTED VALUES
// ============================================================================
const parsedLines = parseInput(input);
const columns = calculateLayout(parsedLines, maxLinesPerColumn);
const columnWidths = columns.map(calculateColumnWidth);
const { svgWidth, svgHeight } = calculateSVGDimensions(columns, columnWidths);
// ============================================================================
// EVENT HANDLERS
// ============================================================================
/**
* Export the SVG as a PNG image
*/
const exportAsPNG = () => {
const svgElement = svgRef.current;
const svgData = new XMLSerializer().serializeToString(svgElement);
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
// Set canvas size (2x for better quality)
canvas.width = svgWidth * 2;
canvas.height = svgHeight * 2;
// Set canvas size with 2x scaling for better quality
canvas.width = svgWidth * SVG_CONFIG.canvasScale;
canvas.height = svgHeight * SVG_CONFIG.canvasScale;
const img = new Image();
const svgBlob = new Blob([svgData], {
@@ -133,7 +180,7 @@ const HarmonicaTabGenerator = () => {
const url = URL.createObjectURL(svgBlob);
img.onload = () => {
ctx.scale(2, 2);
ctx.scale(SVG_CONFIG.canvasScale, SVG_CONFIG.canvasScale);
ctx.drawImage(img, 0, 0);
URL.revokeObjectURL(url);
@@ -148,7 +195,9 @@ const HarmonicaTabGenerator = () => {
img.src = url;
};
// Export as SVG
/**
* Export the SVG as an SVG file
*/
const exportAsSVG = () => {
const svgElement = svgRef.current;
const svgData = new XMLSerializer().serializeToString(svgElement);
@@ -159,23 +208,67 @@ const HarmonicaTabGenerator = () => {
link.click();
};
// ============================================================================
// RENDER
// ============================================================================
return (
<div className="gradient-bg">
<div className="container">
{/* Header */}
<h1 className="title">🎵 Harmonica Tabs Image Generator</h1>
<p className="subtitle">
Create beautiful, readable images of your harmonica tabs
</p>
{/* Instructions Section - MOVED TO TOP */}
{/* Instructions */}
<InstructionsSection />
{/* Settings */}
<SettingsSection
title={title}
setTitle={setTitle}
maxLinesPerColumn={maxLinesPerColumn}
setMaxLinesPerColumn={setMaxLinesPerColumn}
/>
{/* Input */}
<InputSection
input={input}
setInput={setInput}
exportAsPNG={exportAsPNG}
exportAsSVG={exportAsSVG}
/>
{/* Preview */}
<PreviewSection
svgRef={svgRef}
svgWidth={svgWidth}
svgHeight={svgHeight}
title={title}
columns={columns}
columnWidths={columnWidths}
exportAsPNG={exportAsPNG}
exportAsSVG={exportAsSVG}
/>
</div>
</div>
);
};
// ============================================================================
// SUB-COMPONENTS
// ============================================================================
const InstructionsSection = () => (
<div className="card instructions-card">
<h2 className="section-title">📋 How to Use</h2>
<ul className="instructions">
<li className="instruction-item">
<span className="instruction-number">1</span>
<div className="instruction-content">
<strong>Enter your tabs</strong> - Type your harmonica tab
numbers with spaces
<strong>Enter your tabs</strong> - Type your harmonica tab numbers
with spaces
</div>
</li>
<li className="instruction-item">
@@ -188,15 +281,14 @@ const HarmonicaTabGenerator = () => {
<li className="instruction-item">
<span className="instruction-number">3</span>
<div className="instruction-content">
<strong>Add annotations</strong> - Plain text appears smaller
and italicized
<strong>Add annotations</strong> - Plain text appears smaller and
italicized
</div>
</li>
<li className="instruction-item">
<span className="instruction-number">4</span>
<div className="instruction-content">
<strong>Customize layout</strong> - Set your title and rows per
column
<strong>Customize layout</strong> - Set your title and rows per column
</div>
</li>
<li className="instruction-item">
@@ -207,17 +299,21 @@ const HarmonicaTabGenerator = () => {
</li>
</ul>
</div>
);
{/* Settings Section */}
const SettingsSection = ({
title,
setTitle,
maxLinesPerColumn,
setMaxLinesPerColumn,
}) => (
<div className="card settings-card">
<h2 className="section-title">⚙️ Settings</h2>
<div className="settings-grid">
<div className="setting-item">
<label className="setting-label">
Tab Title
<span className="setting-hint">
Appears at the top of your image
</span>
<span className="setting-hint">Appears at the top of your image</span>
</label>
<input
type="text"
@@ -240,9 +336,7 @@ const HarmonicaTabGenerator = () => {
min="5"
max="20"
value={maxLinesPerColumn}
onChange={(e) =>
setMaxLinesPerColumn(parseInt(e.target.value))
}
onChange={(e) => setMaxLinesPerColumn(parseInt(e.target.value))}
className="slider"
/>
<span className="slider-value">{maxLinesPerColumn}</span>
@@ -250,8 +344,9 @@ const HarmonicaTabGenerator = () => {
</div>
</div>
</div>
);
{/* Input Section */}
const InputSection = ({ input, setInput, exportAsPNG, exportAsSVG }) => (
<div className="card">
<h2 className="section-title">✏️ Input Your Tabs</h2>
<textarea
@@ -276,13 +371,22 @@ Add quotes after numbers for bends"
</button>
</div>
<p className="tip">
💡 <strong>Pro tip:</strong> Mix tab lines with text for
annotations. Tab lines contain only numbers, spaces, and bend
markers (' " ` ').
💡 <strong>Pro tip:</strong> Mix tab lines with text for annotations. Tab
lines contain only numbers, spaces, and bend markers (' " ` ').
</p>
</div>
);
{/* Preview Section */}
const PreviewSection = ({
svgRef,
svgWidth,
svgHeight,
title,
columns,
columnWidths,
exportAsPNG,
exportAsSVG,
}) => (
<div className="card preview-card">
<h2 className="section-title">👁️ Preview</h2>
<div className="preview-container">
@@ -291,18 +395,22 @@ Add quotes after numbers for bends"
width={svgWidth}
height={svgHeight}
xmlns="http://www.w3.org/2000/svg"
style={{ background: "#fffef5", display: "block" }}
style={{ background: SVG_CONFIG.backgroundColor, display: "block" }}
>
{/* Background with soft cream color */}
<rect width={svgWidth} height={svgHeight} fill="#fffef5" />
{/* Background */}
<rect
width={svgWidth}
height={svgHeight}
fill={SVG_CONFIG.backgroundColor}
/>
{/* Title - centered at top */}
{/* Title */}
{title && (
<text
x={svgWidth / 2}
y={padding + titleFontSize}
y={SVG_CONFIG.padding + SVG_CONFIG.titleFontSize}
fontFamily="'Segoe UI', 'Arial Rounded MT Bold', Arial, sans-serif"
fontSize={titleFontSize}
fontSize={SVG_CONFIG.titleFontSize}
fontWeight="700"
fill="#5c4a72"
textAnchor="middle"
@@ -312,13 +420,14 @@ Add quotes after numbers for bends"
</text>
)}
{/* Render columns */}
{/* Columns */}
{columns.map((column, colIndex) => {
let yOffset = padding + titleHeight;
// Calculate xOffset based on cumulative widths of previous columns
let xOffset = padding;
let yOffset = SVG_CONFIG.padding + SVG_CONFIG.titleHeight;
// Calculate x position based on previous columns
let xOffset = SVG_CONFIG.padding;
for (let i = 0; i < colIndex; i++) {
xOffset += columnWidths[i] + columnGap;
xOffset += columnWidths[i] + SVG_CONFIG.columnGap;
}
return (
@@ -327,15 +436,15 @@ Add quotes after numbers for bends"
const currentY = yOffset;
if (line.isTab) {
// Render tab line with monospace font - softer color
yOffset += lineHeight;
// Tab line - monospace, larger font
yOffset += SVG_CONFIG.lineHeight;
return (
<text
key={lineIndex}
x={xOffset}
y={currentY}
fontFamily="'Courier New', monospace"
fontSize={fontSize}
fontSize={SVG_CONFIG.fontSize}
fontWeight="600"
fill="#3d5a6c"
letterSpacing="0.5"
@@ -344,15 +453,15 @@ Add quotes after numbers for bends"
</text>
);
} else if (line.content.trim()) {
// Render annotation with smaller font - soft gray
yOffset += lineHeight * 0.6;
// Annotation line - smaller, italic
yOffset += SVG_CONFIG.lineHeight * 0.6;
return (
<text
key={lineIndex}
x={xOffset}
y={currentY}
fontFamily="Arial, sans-serif"
fontSize={annotationFontSize}
fontSize={SVG_CONFIG.annotationFontSize}
fontStyle="italic"
fill="#8b7e99"
>
@@ -376,9 +485,6 @@ Add quotes after numbers for bends"
</button>
</div>
</div>
</div>
</div>
);
};
export default HarmonicaTabGenerator;

View File

@@ -1,4 +1,6 @@
/* Game Freak-inspired soft pastel color scheme */
/* ============================================================================
CSS VARIABLES - Color Palette
============================================================================ */
:root {
--cream: #fffef5;
--soft-purple: #5c4a72;
@@ -12,6 +14,9 @@
--accent-mint: #b5ead7;
}
/* ============================================================================
GLOBAL STYLES
============================================================================ */
* {
margin: 0;
padding: 0;
@@ -29,6 +34,9 @@ body {
line-height: 1.6;
}
/* ============================================================================
LAYOUT
============================================================================ */
.gradient-bg {
min-height: 100vh;
background: linear-gradient(
@@ -46,6 +54,9 @@ body {
margin: 0 auto;
}
/* ============================================================================
TYPOGRAPHY
============================================================================ */
.title {
font-size: 2.5rem;
font-weight: 800;
@@ -64,6 +75,19 @@ body {
font-weight: 500;
}
.section-title {
font-size: 1.5rem;
font-weight: 700;
color: var(--soft-purple);
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
/* ============================================================================
CARDS
============================================================================ */
.card {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
@@ -84,6 +108,7 @@ body {
transform: translateY(-2px);
}
/* Card Variants */
.instructions-card {
background: linear-gradient(
135deg,
@@ -109,16 +134,9 @@ body {
);
}
.section-title {
font-size: 1.5rem;
font-weight: 700;
color: var(--soft-purple);
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
/* ============================================================================
INSTRUCTIONS
============================================================================ */
.instructions {
list-style: none;
display: flex;
@@ -169,6 +187,9 @@ body {
font-weight: 600;
}
/* ============================================================================
SETTINGS
============================================================================ */
.settings-grid {
display: grid;
grid-template-columns: 1fr;
@@ -205,6 +226,9 @@ body {
font-style: italic;
}
/* ============================================================================
FORM INPUTS
============================================================================ */
.input-field {
padding: 0.875rem 1.125rem;
border: 2px solid var(--soft-gray);
@@ -223,6 +247,29 @@ body {
box-shadow: 0 0 0 4px rgba(137, 184, 212, 0.1);
}
.textarea {
width: 100%;
padding: 1rem;
border: 2px solid var(--soft-gray);
border-radius: 16px;
font-family: "Courier New", monospace;
font-size: 1rem;
line-height: 1.8;
resize: vertical;
background: white;
color: var(--text-primary);
transition: all 0.2s ease;
}
.textarea:focus {
outline: none;
border-color: var(--pastel-blue);
box-shadow: 0 0 0 4px rgba(137, 184, 212, 0.1);
}
/* ============================================================================
SLIDER
============================================================================ */
.slider-container {
display: flex;
align-items: center;
@@ -293,26 +340,9 @@ body {
box-shadow: 0 2px 8px rgba(92, 74, 114, 0.2);
}
.textarea {
width: 100%;
padding: 1rem;
border: 2px solid var(--soft-gray);
border-radius: 16px;
font-family: "Courier New", monospace;
font-size: 1rem;
line-height: 1.8;
resize: vertical;
background: white;
color: var(--text-primary);
transition: all 0.2s ease;
}
.textarea:focus {
outline: none;
border-color: var(--pastel-blue);
box-shadow: 0 0 0 4px rgba(137, 184, 212, 0.1);
}
/* ============================================================================
BUTTONS
============================================================================ */
.button-group {
display: flex;
gap: 1rem;
@@ -362,25 +392,9 @@ body {
background: linear-gradient(135deg, #99c4dc, var(--pastel-blue));
}
.tip {
margin-top: 1rem;
padding: 1rem;
background: linear-gradient(
135deg,
rgba(255, 154, 162, 0.1) 0%,
rgba(181, 234, 215, 0.1) 100%
);
border-radius: 12px;
color: var(--text-secondary);
font-size: 0.95rem;
border-left: 4px solid var(--accent-coral);
}
.tip strong {
color: var(--soft-purple);
font-weight: 600;
}
/* ============================================================================
PREVIEW
============================================================================ */
.preview-container {
display: flex;
align-items: flex-start;
@@ -402,7 +416,54 @@ body {
flex-shrink: 0;
}
/* Mobile responsive */
/* ============================================================================
TIP
============================================================================ */
.tip {
margin-top: 1rem;
padding: 1rem;
background: linear-gradient(
135deg,
rgba(255, 154, 162, 0.1) 0%,
rgba(181, 234, 215, 0.1) 100%
);
border-radius: 12px;
color: var(--text-secondary);
font-size: 0.95rem;
border-left: 4px solid var(--accent-coral);
}
.tip strong {
color: var(--soft-purple);
font-weight: 600;
}
/* ============================================================================
SCROLLBAR
============================================================================ */
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-track {
background: var(--soft-gray);
border-radius: 8px;
}
::-webkit-scrollbar-thumb {
background: var(--pastel-blue);
border-radius: 8px;
border: 2px solid var(--soft-gray);
}
::-webkit-scrollbar-thumb:hover {
background: var(--soft-purple);
}
/* ============================================================================
RESPONSIVE
============================================================================ */
@media (max-width: 768px) {
.title {
font-size: 2rem;
@@ -425,24 +486,3 @@ body {
padding: 1rem;
}
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-track {
background: var(--soft-gray);
border-radius: 8px;
}
::-webkit-scrollbar-thumb {
background: var(--pastel-blue);
border-radius: 8px;
border: 2px solid var(--soft-gray);
}
::-webkit-scrollbar-thumb:hover {
background: var(--soft-purple);
}