Skip to content

Commit 5233da9

Browse files
committed
app(mtf): new app to format markdown table.
1 parent 1a6181a commit 5233da9

File tree

7 files changed

+295
-21
lines changed

7 files changed

+295
-21
lines changed

public/apps/apps.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,14 @@
413413
"pinned_order": 12,
414414
"created_at": "2025-11-23T18:16:28+03:00"
415415
},
416+
{
417+
"slug": "markdown-table-formatter",
418+
"to": "/apps/markdown-table-formatter",
419+
"title": "Markdown Table Formatter",
420+
"description": "Format your markdown tables.",
421+
"icon": "CodeIcon",
422+
"created_at": "2025-12-13T02:23:40+03:00"
423+
},
416424
{
417425
"slug": "case-converter",
418426
"to": "/apps/case-converter",

public/logs/movie/taxi-driver-1976.txt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ Travis Bickle is a mentally unstable Vietnam War veteran who takes a job as a ni
2222

2323
### **Cast & Characters**
2424

25-
| Actor | Character | Role Description |
26-
| :--- | :-------: | ---------------: |
27-
| **Robert De Niro** | Travis Bickle | A lonely, insomniac Vietnam vet turned taxi driver. |
28-
| **Jodie Foster** | Iris Steensma | A 12-year-old child prostitute Travis tries to save. |
29-
| **Cybill Shepherd** | Betsy | A campaign volunteer Travis becomes obsessed with. |
30-
| **Harvey Keitel** | "Sport" (Matthew) | Iris's pimp. |
31-
| **Peter Boyle** | "Wizard" | A fellow taxi driver who offers Travis unsolicited advice. |
32-
| **Albert Brooks** | Tom | Betsy's coworker and Travis's romantic rival. |
33-
| **Martin Scorsese** | Passenger | Cameo as a passenger watching his wife through a window. |
25+
| Actor | Character | Role Description |
26+
| :------------------ | :---------------: | ---------------------------------------------------------: |
27+
| **Robert De Niro** | Travis Bickle | A lonely, insomniac Vietnam vet turned taxi driver. |
28+
| **Jodie Foster** | Iris Steensma | A 12-year-old child prostitute Travis tries to save. |
29+
| **Cybill Shepherd** | Betsy | A campaign volunteer Travis becomes obsessed with. |
30+
| **Harvey Keitel** | "Sport" (Matthew) | Iris's pimp. |
31+
| **Peter Boyle** | "Wizard" | A fellow taxi driver who offers Travis unsolicited advice. |
32+
| **Albert Brooks** | Tom | Betsy's coworker and Travis's romantic rival. |
33+
| **Martin Scorsese** | Passenger | Cameo as a passenger watching his wife through a window. |
3434

3535
---
3636

src/components/AnimatedRoutes.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const WordCounterPage = lazy(() => import('../pages/apps/WordCounterPage'));
2626
const TournamentBracketPage = lazy(
2727
() => import('../pages/apps/TournamentBracketPage'),
2828
);
29+
const MarkdownTableFormatterPage = lazy(() => import('../pages/apps/MarkdownTableFormatterPage'));
2930
const CaseConverterPage = lazy(() => import('../pages/apps/CaseConverterPage'));
3031
const Base64ConverterPage = lazy(
3132
() => import('../pages/apps/Base64ConverterPage'),
@@ -618,6 +619,10 @@ function AnimatedRoutes() {
618619
path="/apps::tb"
619620
element={<Navigate to="/apps/tournament-bracket" replace />}
620621
/>
622+
<Route
623+
path="/apps::mtf"
624+
element={<Navigate to="/apps/markdown-table-formatter" replace />}
625+
/>
621626
<Route
622627
path="/apps::cc"
623628
element={<Navigate to="/apps/case-converter" replace />}
@@ -1164,6 +1169,22 @@ function AnimatedRoutes() {
11641169
</motion.div>
11651170
}
11661171
/>
1172+
<Route
1173+
path="/apps/markdown-table-formatter"
1174+
element={
1175+
<motion.div
1176+
initial="initial"
1177+
animate="in"
1178+
exit="out"
1179+
variants={pageVariants}
1180+
transition={pageTransition}
1181+
>
1182+
<Suspense fallback={<Loading />}>
1183+
<MarkdownTableFormatterPage />
1184+
</Suspense>
1185+
</motion.div>
1186+
}
1187+
/>
11671188
<Route
11681189
path="/apps/case-converter"
11691190
element={

src/components/BreadcrumbTitle.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import React from 'react';
22

3-
const BreadcrumbTitle = ({ title, slug, breadcrumbs, gradient = true }) => {
3+
const BreadcrumbTitle = ({ title, slug, breadcrumbs, gradient = true, sansFont = false, lightStyle = true }) => {
44
// Use provided breadcrumbs array, or fallback to default ['fc', 'apps', slug]
55
const parts = breadcrumbs || (slug ? ['fc', 'apps', slug] : []);
66

77
return (
8-
<div className="relative flex flex-col items-center justify-center mb-4">
8+
<div className={`relative flex flex-col items-center justify-center mb-4 ${ sansFont ? 'font-playfairDisplay' : 'font-mono'} `}>
99
<span className="min-[1376px]:absolute min-[1376px]:left-0 min-[1376px]:top-1/2 min-[1376px]:-translate-y-1/2 text-xl md:text-2xl font-mono font-normal text-gray-500 tracking-tight mb-2 min-[1376px]:mb-0 opacity-75">
1010
{parts.map((part, index) => (
1111
<React.Fragment key={index}>
12-
<span
13-
className={index === parts.length - 1 ? 'text-primary-400' : ''}
14-
>
12+
<span className={index === parts.length - 1 ?
13+
lightStyle ? 'text-primary-400' : 'text-rose-800'
14+
: ''} >
1515
{part}
1616
</span>
1717
{index < parts.length - 1 && (
@@ -24,7 +24,9 @@ const BreadcrumbTitle = ({ title, slug, breadcrumbs, gradient = true }) => {
2424
<span
2525
className={
2626
gradient
27-
? 'bg-clip-text text-transparent bg-gradient-to-r from-primary-400 to-secondary-400'
27+
? lightStyle ?
28+
'bg-clip-text text-transparent bg-gradient-to-r from-primary-400 to-secondary-400'
29+
: 'bg-clip-text text-transparent bg-gradient-to-r from-pink-800 to-teal-800'
2830
: 'text-white'
2931
}
3032
>

src/pages/LogsPage.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,14 @@ const iconColors = [
5353
const LogsPage = () => {
5454
useSeo({
5555
title: 'Logs | Fezcodex',
56-
description:
57-
'A collection of logs, thoughts, and other miscellaneous writings.',
56+
description: 'A collection of logs, thoughts, and other miscellaneous writings.',
5857
keywords: ['Fezcodex', 'logs', 'thoughts', 'writing'],
5958
ogTitle: 'Logs | Fezcodex',
60-
ogDescription:
61-
'A collection of logs, thoughts, and other miscellaneous writings.',
59+
ogDescription: 'A collection of logs, thoughts, and other miscellaneous writings.',
6260
ogImage: 'https://fezcode.github.io/logo512.png',
6361
twitterCard: 'summary_large_image',
6462
twitterTitle: 'Logs | Fezcodex',
65-
twitterDescription:
66-
'A collection of logs, thoughts, and other miscellaneous writings.',
63+
twitterDescription: 'A collection of logs, thoughts, and other miscellaneous writings.',
6764
twitterImage: 'https://fezcode.github.io/logo512.png',
6865
});
6966

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import React, {useState} from 'react';
2+
import {Link} from 'react-router-dom';
3+
import {
4+
ArrowLeftIcon,
5+
BaseballHelmetIcon, BeanieIcon, BroomIcon,
6+
ClipboardIcon, TaxiIcon,
7+
} from '@phosphor-icons/react';
8+
import {useToast} from '../../hooks/useToast';
9+
import useSeo from '../../hooks/useSeo';
10+
import BreadcrumbTitle from '../../components/BreadcrumbTitle';
11+
import formatMarkdownTable from "../../utils/markdownUtils";
12+
import {useSidePanel} from "../../context/SidePanelContext";
13+
14+
const FruitDashboard = () => {
15+
useSeo({
16+
title: 'Markdown Table Formatter | Fezcodex',
17+
description: 'Format your markdown tables.',
18+
keywords: ['Fezcodex', 'md', 'markdown', 'table'],
19+
ogTitle: 'Markdown Table Formatter | Fezcodex',
20+
ogDescription: 'Format your markdown tables.',
21+
ogImage: 'https://fezcode.github.io/logo512.png',
22+
twitterCard: 'summary_large_image',
23+
twitterTitle: 'Markdown Table Formatter | Fezcodex',
24+
twitterDescription: 'Format your markdown tables.',
25+
twitterImage: 'https://fezcode.github.io/logo512.png',
26+
});
27+
28+
const [inputText, setInputText] = useState('');
29+
const [outputText, setOutputText] = useState('');
30+
const {addToast} = useToast();
31+
const { openSidePanel } = useSidePanel();
32+
33+
const ReasonComponent = () => {
34+
return (
35+
<div className="space-y-6">
36+
<div className="flex items-center justify-center gap-4">
37+
<TaxiIcon size={24} weight="fill" />
38+
<h1 className="text-white font-arvo">... Because of Taxi Driver ...</h1>
39+
<TaxiIcon size={24} weight="fill" />
40+
</div>
41+
<div className="bg-gray-800/50 p-4 rounded-lg border border-gray-700">
42+
<h3 className="text-white font-bold mb-2">Markdown Log Entries</h3>
43+
<p className="text-sm text-gray-400">
44+
I watched Taxi Driver (1976).
45+
Then I started to write a review, log entry for it.
46+
Creating a <strong> Markdown Table </strong>is one of the hardest parts of writing markdown.
47+
Each table row looked worse than the previous one.
48+
I needed a formatter.
49+
</p>
50+
<br/>
51+
<p className="text-sm text-gray-400">
52+
After hours of writing a React component, finally here it is.
53+
It is 5 AM and I'm <i>finally</i> done.
54+
</p>
55+
</div>
56+
</div>
57+
);
58+
}
59+
60+
const convertMarkdownTable = () => {
61+
try {
62+
let formattedTable = formatMarkdownTable(inputText);
63+
setOutputText(formattedTable);
64+
} catch (error) {
65+
addToast({
66+
title: 'Error',
67+
message: 'Failed to format given markdown table text.',
68+
duration: 3000,
69+
});
70+
setOutputText('');
71+
}
72+
};
73+
74+
const copyToClipboard = (text) => {
75+
navigator.clipboard
76+
.writeText(text)
77+
.then(() => {
78+
addToast({
79+
title: 'Success',
80+
message: 'Copied to clipboard!',
81+
duration: 2000,
82+
});
83+
})
84+
.catch(() => {
85+
addToast({
86+
title: 'Error',
87+
message: 'Failed to copy!',
88+
duration: 2000,
89+
});
90+
});
91+
};
92+
93+
return (
94+
// bg-[#CFD8DC]
95+
<div className="py-16 sm:py-24 min-h-screen bg-[#FFEDCF]">
96+
<div className="mx-auto max-w-7xl px-6 lg:px-8 text-gray-300">
97+
<Link to="/apps" className="group text-rose-800 hover:underline flex items-center justify-center gap-2 text-lg mb-4 font-arvo">
98+
<ArrowLeftIcon className="text-xl transition-transform group-hover:-translate-x-1"/>
99+
Back to Apps
100+
</Link>
101+
102+
<BreadcrumbTitle title="Markdown Table Formatter" slug="mtf" sansFont={true} lightStyle={false}/>
103+
<hr className="border-gray-700"/>
104+
<div className="flex justify-center items-center mt-16 text-[#1A2E1A]">
105+
{/*Our Text Div*/}
106+
<div
107+
className="group bg-[#FBF7F0] rounded-[0.25rem] border border-neutral-700 shadow-2xl p-6 flex flex-col justify-between relative transform transition-all duration-300 ease-in-out overflow-hidden h-full w-full max-w-4xl">
108+
<div className="absolute top-0 left-0 w-full h-full opacity-10" style={{ backgroundImage: 'radial-gradient(circle, black 1px, transparent 1px)', backgroundSize: '10px 10px' }}></div>
109+
110+
<div className="relative z-10 p-1">
111+
<div className="flex flex-row items-center justify-items-start gap-2">
112+
<h1 className="text-4xl font-serif text-[#1A2E1A] mb-2">Table Formatter</h1>
113+
<button
114+
onClick={() => openSidePanel('Why Create a Formatter', <ReasonComponent />, 400)}
115+
className="text-gray-500 hover:text-[#1A2E1A] transition-colors hover:scale-105"
116+
aria-label="Rating System Info"
117+
>
118+
<BeanieIcon className="rotate-12 hover:rotate-0 transition" size={24} />
119+
</button>
120+
</div>
121+
122+
<hr className="border-gray-700 mb-4"/>
123+
{/*Input Text*/}
124+
<div className="mb-4">
125+
<label className="block text-2xl font-normal italic text-[#2c3e2c] mb-2 font-playfairDisplay">Input Text</label>
126+
127+
<textarea
128+
className="w-full h-32 p-4 bg-[#f3e2c850] font-mono resize-y border rounded-md border-app-alpha-50 text-[#1A2E1A]"
129+
value={inputText}
130+
onChange={(e) => setInputText(e.target.value)}
131+
placeholder="Enter markdown table here..."
132+
/>
133+
</div>
134+
135+
{/*Button*/}
136+
<div className="flex justify-center gap-4 mb-4 mt-4">
137+
<button
138+
onClick={convertMarkdownTable}
139+
className="px-6 py-2 rounded-md text-lg font-arvo font-normal transition-colors duration-300 ease-in-out bg-[#f0e3c980] hover:bg-[#fef9e6] text-[#2e5341] border-[#2e5341] border flex items-center gap-2"
140+
>
141+
<BaseballHelmetIcon size={24}/>
142+
Format Table
143+
</button>
144+
145+
<button
146+
onClick={ () => setInputText('')}
147+
className="px-6 py-2 rounded-md text-lg font-arvo font-normal transition-colors duration-300 ease-in-out bg-[#f0e3c980] hover:bg-[#fef9e6] text-[#2e5341] border-[#2e5341] border flex items-center gap-2"
148+
>
149+
<BroomIcon size={24}/>
150+
Clear Text
151+
</button>
152+
</div>
153+
154+
{/*Output Text*/}
155+
<div className="mb-4">
156+
<label className="block text-2xl font-normal italic text-[#2c3e2c] mb-2 font-playfairDisplay">Output Text</label>
157+
<div className="relative overflow-hidden">
158+
<textarea
159+
readOnly
160+
className="w-full h-32 p-4 bg-[#f3e2c850] font-mono resize-y border rounded-md border-app-alpha-50 text-[#1A2E1A]"
161+
value={outputText}
162+
placeholder="Formatted text will appear here..."
163+
/>
164+
<button
165+
onClick={() => copyToClipboard(outputText)}
166+
className="absolute top-2 right-2 px-3 py-2 bg-gray-700 text-white text-sm font-arvo rounded hover:bg-gray-600 flex items-center justify-center gap-2 "
167+
>
168+
<ClipboardIcon size={16}> </ClipboardIcon>
169+
Copy
170+
</button>
171+
</div>
172+
</div>
173+
</div>
174+
</div>
175+
</div>
176+
</div>
177+
</div>
178+
);
179+
};
180+
181+
export default FruitDashboard;

src/utils/markdownUtils.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
function formatMarkdownTable(markdownTable) {
2+
// 1. Split into lines and filter empty strings
3+
const lines = markdownTable
4+
.trim()
5+
.split('\n')
6+
.map(line => line.trim())
7+
.filter(line => line.length > 0);
8+
9+
// 2. Parse rows into cells
10+
const rows = lines.map(line => {
11+
// Remove leading/trailing pipes if present to avoid empty first/last cells
12+
const content = line.replace(/^\||\|$/g, '');
13+
return content.split('|').map(cell => cell.trim());
14+
});
15+
16+
if (rows.length === 0) return '';
17+
18+
const colCount = rows[0].length;
19+
const colWidths = new Array(colCount).fill(0);
20+
21+
// 3. Calculate max width for each column
22+
rows.forEach(row => {
23+
row.forEach((cell, i) => {
24+
// Ensure we don't exceed the column count defined by the header
25+
if (i < colCount) {
26+
// Minimum width of 3 is standard for markdown (e.g., "---")
27+
colWidths[i] = Math.max(colWidths[i], cell.length, 3);
28+
}
29+
});
30+
});
31+
32+
// 4. Reconstruct the table
33+
const formattedLines = rows.map((row, rowIndex) => {
34+
// Detect if this is the separator row (usually index 1, contains only dashes/colons)
35+
const isSeparator = rowIndex === 1 && /^[:\s-]*$/.test(row.join(''));
36+
37+
const formattedCells = row.map((cell, i) => {
38+
if (i >= colCount) return null; // Skip extra cells if row is too long
39+
40+
const targetWidth = colWidths[i];
41+
42+
if (isSeparator) {
43+
// Handle alignment markers (e.g., :---, ---:, :---:)
44+
const hasLeftColon = cell.startsWith(':');
45+
const hasRightColon = cell.endsWith(':');
46+
47+
// Build the separator line
48+
const start = hasLeftColon ? ':' : '-';
49+
const end = hasRightColon ? ':' : '-';
50+
const dashes = '-'.repeat(targetWidth - 2);
51+
52+
return start + dashes + end;
53+
} else {
54+
// Standard data cell: Pad with spaces on the right
55+
return cell.padEnd(targetWidth, ' ');
56+
}
57+
}).filter(c => c !== null); // Remove skipped cells
58+
59+
return `| ${formattedCells.join(' | ')} |`;
60+
});
61+
62+
return formattedLines.join('\n');
63+
}
64+
65+
export default formatMarkdownTable;

0 commit comments

Comments
 (0)