<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Emad Ibrahim on Medium]]></title>
        <description><![CDATA[Stories by Emad Ibrahim on Medium]]></description>
        <link>https://medium.com/@eibrahim?source=rss-3dad5916cd29------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*4-VIXnuROhQwXN1YmkDMcQ.png</url>
            <title>Stories by Emad Ibrahim on Medium</title>
            <link>https://medium.com/@eibrahim?source=rss-3dad5916cd29------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 20:07:01 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@eibrahim/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[My parking calculator calls the meter cheapest, even when it warns of a ticket]]></title>
            <link>https://medium.com/@eibrahim/my-parking-calculator-calls-the-meter-cheapest-even-when-it-warns-of-a-ticket-637a1827eb6a?source=rss-3dad5916cd29------2</link>
            <guid isPermaLink="false">https://medium.com/p/637a1827eb6a</guid>
            <dc:creator><![CDATA[Emad Ibrahim]]></dc:creator>
            <pubDate>Sat, 14 Nov 2026 13:09:17 GMT</pubDate>
            <atom:updated>2026-11-14T13:09:17.674Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CxAqq1uUjdwZggqRACKZQQ.jpeg" /></figure><p>I built a free <a href="https://www.mycurbspot.com/tools/parking-cost-calculator">Parking Cost Calculator</a> for My Curb Spot. You pick a city, enter how long you’ll park, and it compares the cost of a street meter, a garage, and a surface lot. The cheapest option gets a “Cheapest” badge, and the meter gets a warning if you’d stay past its time limit.</p><p>Before writing this up, I ran every city and every duration through the same math the page uses. The meter wins every single time. That includes the cases where the page also tells you you’ll probably get a ticket.</p><h3>How it works</h3><p>It’s one client-side Next.js 16 page in TypeScript and Tailwind CSS, built with Claude Code. There are presets for 25 US cities. Each preset has an hourly rate for meters, garages, and surface lots, plus a typical meter time limit: 2 hours in 15 cities, 4 hours in the other 10. You can also type your own rates or switch any of the three options off.</p><p>The cost is the hourly rate times the hours you entered. The lowest total gets the badge, and a callout says how much you’d save compared with the most expensive option. The commit also added an Open Graph image, FAQ, HowTo, and breadcrumb structured data, and a card on the tools page.</p><h3>The meter always wins</h3><p>In all 25 presets, the meter rate is lower than the lot rate, and the lot rate is lower than the garage rate. The cost is a straight multiplication, so the order never changes with time. Park for 15 minutes or 24 hours and the result is the same: meter first, lot second, garage last.</p><p>I checked every preset at every quarter hour the form accepts, from 15 minutes to 24 hours 45 minutes. That’s 2,475 combinations. The meter gets the “Cheapest” badge in all 2,475. In 2,195 of them, the stay is longer than the meter’s time limit, so the same card also shows “Exceeds typical 2hr meter limit — risk of $25-$79 overtime ticket!”</p><p>Here’s one I ran on the live page. New York City, 8 hours. Meter: $28.00, with the Cheapest badge and the ticket warning. Surface lot: $64.00. Garage: $96.00. Below that: “You could save up to $68.00.”</p><p>That $28 isn’t a price you can pay. A 2-hour meter won’t take 8 hours of money. Even if you could feed it all day and got only one ticket, the page’s own $25 to $79 range puts the real total at $53 to $107, against $64 for the lot. The page’s FAQ gives better advice than the calculator does: “For longer durations, surface lots often offer the best value since you avoid overtime meter risks.” The calculator can’t ever show that.</p><h3>Garages never hit a daily maximum</h3><p>The FAQ also says most garages charge hourly with a daily maximum. The calculator doesn’t model one. A full day in a New York garage comes out at $288, which is 24 hours at $12. Long stays are exactly when people look up parking costs, and that’s where the numbers drift furthest from what a garage would really charge.</p><h3>Why I didn’t catch it</h3><p>I tested it the way most people would use it the first time: pick a city, park for an hour or two, look at the result. At those durations the meter really is the cheapest choice, there’s no warning, and everything looks right. The problem only shows up once you go past the meter limit. A one-line loop over the presets would have shown that the winner never changes.</p><p>I also added the warning as a label on the card instead of a cost in the math. That made the page look careful while the result ignored the risk it was describing.</p><h3>What I learned</h3><ul><li>If the inputs are a small, fixed set, run all of them. Here, one loop showed that the answer never changes.</li><li>A warning shouldn’t sit next to a recommendation that ignores it. Either put the risk into the number, or stop recommending that option.</li><li>Check the calculator against its own FAQ. Mine says the opposite of what the tool shows for long stays.</li><li>Simple per-hour math can be fine for short stays. It needs caps and limits before it’s right for long ones.</li></ul><h3>What I’m fixing</h3><p>First, the meter. Once the stay is longer than the meter’s limit, the meter card will say it can’t cover that stay instead of showing a price, and it won’t be eligible for the Cheapest badge. Then garages and lots get a daily maximum per city, so long stays come out at a realistic number.</p><p>I’ll also label the city rates as typical estimates, since they’re fixed numbers in the code rather than live prices. And I’ll add a test that runs every city and duration and checks that the cheapest option changes as the stay gets longer. The browser tab title is on the list too. It ends in “| My Curb Spot | My Curb Spot” because the page sets the brand and the site layout adds it again, and 25 tool pages do that.</p><p>The city presets, the toggles, and the overtime warning are a decent start. The part that picks a winner has to respect the rules it already knows about.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=637a1827eb6a" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[My monetization quiz shows a 54% match for 90% of answers]]></title>
            <link>https://medium.com/@eibrahim/my-monetization-quiz-shows-a-54-match-for-90-of-answers-22bcc04ed684?source=rss-3dad5916cd29------2</link>
            <guid isPermaLink="false">https://medium.com/p/22bcc04ed684</guid>
            <dc:creator><![CDATA[Emad Ibrahim]]></dc:creator>
            <pubDate>Fri, 13 Nov 2026 13:10:02 GMT</pubDate>
            <atom:updated>2026-11-13T13:10:02.926Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VkTzC7YGc-fMSlhriISCSA.jpeg" /></figure><p>I built a free <a href="https://www.pitchanapp.com/tools/monetization-strategy-picker">App Monetization Strategy Picker</a> for Pitch An App. You answer seven questions about your app: what kind of app it is, how big the audience is, who it’s for, what value it delivers, how often people use it, whether they’ll pay, and how crowded the market is. It recommends one of five models (freemium, ads, subscriptions, one-time purchase, or hybrid) with pros, cons, a pricing cue, and two runner-up options.</p><p>Before writing this up, I ran every possible set of answers through the scoring code. There are 30,720 of them. The recommendations mostly make sense. The number shown next to them doesn’t.</p><h3>How it works</h3><p>It’s one client-side Next.js page in TypeScript and Tailwind CSS, built with Codex CLI and GPT-5.4. Each answer option adds points to some of the five models. For example, “B2B or professional workflow” adds 4 points to subscriptions, 2 to freemium, and 1 to hybrid. After the last question, the page adds up the points, sorts the models, and shows the winner with a “Match score” percentage and a “Strong fit” or “Balanced fit” label. The commit also added SEO metadata, a generated Open Graph image, and a card on the tools page.</p><h3>The match score is 54% for 90% of answers</h3><p>The match score takes the winner’s share of all points as a percentage, adds twice the gap between first and second place, and clamps the result between 54 and 93.</p><p>The problem is the share. Most answer options give points to two to four models at once, so the winner usually gets between 26% and 40% of the total. The median is 31%. In one case I tested, hybrid won with 13 of 50 points. That’s 26%, plus 2 for its one-point lead, so 28. The clamp raises it to 54.</p><p>Across all 30,720 answer combinations, 27,749 show exactly 54%. Only 9 reach the 93% ceiling. I checked both ends on the live page. A B2B team tool that people use several times a day, where buyers already pay for alternatives, gets Subscriptions with 26 points against 7 for the runner-up, and a 93% match. The hybrid case shows 54%. So does almost everything in between.</p><p>A number that almost never changes tells the user nothing. Worse, 54% reads like a weak match even when the winner is clear. The “Strong fit” label works better, because it only looks at the gap: four points or more.</p><h3>Ties go to freemium</h3><p>In 11% of combinations, two or more models tie for first place. The page sorts scores with JavaScript’s sort, which keeps tied items in their original order: freemium, ads, subscriptions, one-time, hybrid. So freemium wins every tie it’s part of, and the result never mentions the tie.</p><p>In one combination I tried on the live page, freemium, subscriptions, and hybrid all scored 11 points. The headline said Freemium, 54% match. That person had said their users prefer a simple upfront purchase.</p><h3>Subscriptions wins half the time</h3><p>Subscriptions is the top pick in 51% of all combinations. Freemium gets 27%. Ads, one-time purchase, and hybrid get about 7% each. The weights explain it: subscriptions has 55 points spread across the options, more than any other model, while ads and one-time purchase each appear in only 11 of the 31 options.</p><p>Some of that lean is fair, since a lot of software does fit subscriptions. But the weights were set one option at a time, and nobody looked at the distribution they produce. If a quiz with five answers picks one of them half the time, that should be a decision, not an accident.</p><h3>Why I didn’t catch it</h3><p>The quiz looks fine if you try it with answers that point clearly at one model. Those give sensible winners and high match scores, because clear answers create big gaps. The 54% floor shows up when the answers are mixed, which is what most real apps look like. Checking every combination took a short Python script and about a second.</p><h3>What I learned</h3><ul><li>If a scoring tool has a small set of possible inputs, run all of them. The distribution tells you more than any number of hand-picked tests.</li><li>A clamp can hide a broken formula. If the floor is the most common result, the formula is wrong.</li><li>Decide how ties work, and show them. Sort order is not a product decision.</li><li>Check how often each answer wins. If one wins half the time, make sure you meant that.</li></ul><h3>What I’m fixing</h3><p>First, the match score. I’ll either base it on the gap between first and second place, which is what actually measures confidence, or drop the percentage and keep the Strong fit and Balanced fit labels. Then ties: when models tie, the result should say so and show them side by side.</p><p>Then I’ll rebalance the weights and add a test that runs every combination and fails if one model wins too often or if most answers land on the same match score. I’ll also fix the browser tab title, which ends in “| Pitch An App | Pitch An App” because the page title includes the brand and the site layout adds it again. 21 tool pages have that.</p><p>The questions and the write-ups for each model are solid. The part that turns answers into a number needs work.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=22bcc04ed684" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[My fake chat generator’s Download button doesn’t download anything]]></title>
            <link>https://medium.com/@eibrahim/my-fake-chat-generators-download-button-doesn-t-download-anything-f2167715de75?source=rss-3dad5916cd29------2</link>
            <guid isPermaLink="false">https://medium.com/p/f2167715de75</guid>
            <dc:creator><![CDATA[Emad Ibrahim]]></dc:creator>
            <pubDate>Thu, 12 Nov 2026 13:06:23 GMT</pubDate>
            <atom:updated>2026-11-12T13:06:23.793Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*055vUwOqS6uygFdBrgFSVg.jpeg" /></figure><p>I built a free <a href="https://www.chatspark.dev/tools/fake-chat-generator">Fake Chat Generator</a> for ChatSpark. You pick a style (iMessage, WhatsApp, Slack, Discord, or a plain generic chat), add participants with names and avatar colors, type messages with timestamps, and download the conversation as a PNG. It’s meant for mockups, slides, and app screenshots.</p><p>Before writing this up, I opened the live page in a fresh browser and went through it like a first-time user. The editor and the preview work. The one button that matters doesn’t.</p><h3>How it works</h3><p>It’s one client-side Next.js page in TypeScript and Tailwind CSS, with no backend. The state is three lists: participants, messages, and the selected style. Each style is a small React component that renders the same messages differently: blue and gray bubbles for iMessage, a beige background with names for WhatsApp, avatar rows for Slack, a dark theme for Discord. You can reorder or delete messages, and the preview updates as you type.</p><p>The export uses html2canvas. It walks the preview’s DOM, redraws it onto a canvas at 2x scale, and triggers a download. The commit also added SEO metadata, a generated Open Graph image, FAQ, HowTo, and breadcrumb structured data, and a link from the tools index.</p><h3>The Download PNG button fails silently</h3><p>I added two messages and clicked Download PNG in each of the five styles. No file downloaded in any of them. The button flashed “Exporting…” and went back to “Download PNG”. Nothing on the page said anything went wrong.</p><p>The console did: “Attempting to parse an unsupported color function ‘oklch’.” ChatSpark uses Tailwind CSS v4, and v4 defines its default palette in oklch. So a class like text-gray-400 on the timestamps gives the browser an oklch color. html2canvas 1.4.1, the last release, only understands older color formats like hex and rgb. When it reads the first oklch color, it throws, and the export stops.</p><p>The export code has a try/finally block with no catch. The finally resets the button, and the error goes nowhere the user can see. So the tool’s main promise, “Download the chat as a PNG screenshot”, which is step 5 of the page’s own how-to, fails without telling anyone.</p><h3>Deleting “Me” sends messages to nobody</h3><p>The sender dropdown starts on “Me”. If you delete the Me participant, the dropdown shows the next person, Alex. But the page’s state still points at the deleted participant. The remove function picks a fallback sender from the participant list as it was before the deletion, and that’s the person you just deleted.</p><p>So the next message you add is assigned to someone who no longer exists. The message list shows it as “Unknown”, and the preview hides it, because each preview skips messages whose sender it can’t find. You see the dropdown say Alex, you type a message, and it never shows up in the chat. There’s also no one marked as “Me” anymore, so every bubble sits on the left until you pick a new Me.</p><h3>Why I didn’t catch it</h3><p>The export depends on two libraries working together, and I didn’t test them together. html2canvas worked fine for years when Tailwind used hex and rgb colors. The page built, the preview looked right, and the build passed. Nothing failed until a real click in a real browser.</p><h3>What I learned</h3><ul><li>Click the button the tool is named for, in production, before you call it shipped. A clean build doesn’t prove the main action works.</li><li>A try/finally with no catch hides failures. If an export can fail, show the user a message.</li><li>Check that your rendering libraries understand your CSS. Tailwind v4’s move to oklch broke html2canvas, and any tool that parses computed styles itself can break the same way.</li><li>When you delete something, compute the fallback from the list after the delete, not before.</li></ul><h3>What I’m fixing</h3><p>First, the export. I’ll switch to a capture library that understands modern color functions, or give the preview explicit hex colors so it doesn’t depend on Tailwind’s palette. Then I’ll add a catch that tells the user when an export fails, and a check that clicks Download PNG in every style and confirms a file comes back.</p><p>Then the sender bug: pick the fallback sender from the updated list, and keep one participant marked as Me. I’ll also make timestamps step forward for each new message, since right now every message gets the same time unless you change it by hand.</p><p>The editor and the five styles are the bulk of the work, and they’re done. The download is what people came for, and it has to work.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f2167715de75" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[My Runway Calculator Calls a Long Runway "Profitable"]]></title>
            <link>https://medium.com/@eibrahim/my-runway-calculator-calls-a-long-runway-profitable-944d149efb28?source=rss-3dad5916cd29------2</link>
            <guid isPermaLink="false">https://medium.com/p/944d149efb28</guid>
            <dc:creator><![CDATA[Emad Ibrahim]]></dc:creator>
            <pubDate>Wed, 11 Nov 2026 16:44:04 GMT</pubDate>
            <atom:updated>2026-11-11T16:44:04.780Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*93lIUS4o_b_ZL3WTl9BOmA.jpeg" /></figure><p>I built a free <a href="https://www.ideascore.co/tools/startup-runway-calculator">Startup Runway Calculator</a> for Idea Score. It takes cash on hand, monthly revenue, monthly expenses, revenue growth, expense drift, and planned hires, then draws a cash projection.</p><p>The useful part is making assumptions visible. A flat burn-rate calculation can’t show when a new salary starts or how recurring growth changes the curve. This tool can. But while checking the implementation for this write-up, I found a label that overstates what the model knows.</p><h3>What shipped</h3><p>The calculator is a client-side Next.js page written in TypeScript with Tailwind CSS. React state holds the inputs and hiring plan. A memoized forecast produces the cash points, and an SVG renders the line and shaded area. There is no charting dependency in this page.</p><p>Each planned hire has a title, monthly cost, and start month. The forecast includes that cost from the selected month onward. You can change the assumptions and see the projection update without submitting a form.</p><p>The shipping commit also added a tools-index link, metadata, a landscape Open Graph image, and FAQ, HowTo, and breadcrumb structured data. Those are the supporting pieces around the calculator; they aren’t evidence of search traffic or demand.</p><h3>Two calculations, two different questions</h3><p>The simple-runway card divides current cash by current monthly net burn. The chart simulates up to 36 months with changing revenue, expenses, and hiring costs. Keeping both views makes sense: the first explains the baseline, while the second shows the effect of the assumptions. A planned hire changes the curve; the simple baseline still uses current expenses.</p><p>But the formatter for that baseline contains a shortcut: if the result is non-finite or greater than 999 months, it displays “Profitable.” That combines a very long but finite runway with the case where current revenue covers expenses.</p><h3>The example that exposed it</h3><p>I checked the live tool with $10,000,000 in cash, $10,000 in monthly revenue, $20,000 in monthly expenses, zero revenue growth, zero expense drift, and no hires.</p><p>The baseline calculation is 1,000 months: $10,000,000 divided by $10,000 of monthly net burn. The live page displays “Profitable” in the simple-runway card. Right beside it, the net-burn card still shows a $10K monthly cash loss.</p><p>This is deliberately an edge case, not a typical startup budget. It makes the mistake easy to see: cash reserves change how long the money lasts, but don’t change whether monthly revenue covers monthly expenses.</p><h3>What I learned</h3><p>A display limit needs its own label. “More than 999 months” would preserve the meaning of this finite answer. “Profitable” changes the meaning entirely. A formatter that looks like presentation code is making a claim about the underlying numbers.</p><p>The forecast horizon needs the same care. The chart describes a bounded 36-month scenario under supplied assumptions, not an unlimited prediction. A clear label should preserve those limits rather than turn a convenient internal value into certainty.</p><p>Checking the number next to the label was more revealing than checking the chart alone. The interface simultaneously showed “Profitable” and a monthly cash loss. That is a useful edge-case check for any calculator with a friendly summary label.</p><h3>What comes next</h3><p>The next change I’d prioritize is separating a large finite runway from a non-burning baseline in the formatter. I’d keep the simple calculation and the growth-and-hiring forecast visibly distinct, and remove the unused projected-runway calculation if it has no role in the interface.</p><p>That correction isn’t shipped in this post. The calculator is live, and the misleading label is still there. This build was a reminder to review the words beside a chart as carefully as the formula behind it.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=944d149efb28" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[My activities finder has one filter, and the page promises two]]></title>
            <link>https://medium.com/@eibrahim/my-activities-finder-has-one-filter-and-the-page-promises-two-46e5e0b5e4fd?source=rss-3dad5916cd29------2</link>
            <guid isPermaLink="false">https://medium.com/p/46e5e0b5e4fd</guid>
            <dc:creator><![CDATA[Emad Ibrahim]]></dc:creator>
            <pubDate>Tue, 10 Nov 2026 13:04:24 GMT</pubDate>
            <atom:updated>2026-11-10T13:04:24.763Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UKfqGsKGTMIiZsj6yVUTHA.jpeg" /></figure><p>I built a free <a href="https://www.sitterrank.com/tools/age-appropriate-activities">Age-Appropriate Activities Finder</a> for Sitter Rank. It’s for babysitters and parents who need an idea right now: pick the child’s age group, and you get indoor, outdoor, educational, and creative activities, each with a difficulty level and a materials list.</p><p>It works, and the content is useful. But while writing this up, I read the page the way a sitter would, and the words on it promise more than the tool does.</p><h3>How it works</h3><p>It’s one client-side Next.js page in TypeScript and Tailwind, with no backend. The data is a hard-coded array of 60 activities. Each one has a name, a description, a difficulty (Easy, Moderate, or Advanced), a materials list, an age group, and a category. There are six age groups, from Infant (0–1 years) to Teen (12+ years), and four categories.</p><p>The only state on the page is the selected age group. It defaults to Preschool. Click a different age card and the list below re-renders, grouped under four headings: Indoor, Outdoor, Educational, and Creative. The commit also added SEO metadata, a generated Open Graph image, FAQ, HowTo, and breadcrumb structured data, and links to the other Sitter Rank tools.</p><h3>Category is a heading, not a filter</h3><p>The intro text says: “Filter by age group and category.” Step 2 of the how-to says “Browse activities by category.” But there is only one control on the page: the six age buttons. Category isn’t something you choose. It’s a heading the list is sorted under. If it’s raining and you only want indoor ideas, you scroll past the outdoor ones. You can’t filter by difficulty or by what’s in the house either.</p><p>So it’s a finder with one filter. The page copy describes two.</p><h3>“60+” means exactly 60</h3><p>The intro says “Browse 60+ activities.” The number comes from the array length, and the plus sign is hard-coded after it. The array holds exactly 10 activities for each of the six age groups, so the live page says “60+” about a list of 60. Because every group has 10, every age card shows the same “10 activities” badge. That badge looks like information, but it can never change.</p><h3>The age ranges overlap</h3><p>The ranges are 0–1, 1–3, 3–5, 5–8, 8–12, and 12+. Each boundary age appears in two groups. A three-year-old is both a Toddler and a Preschooler, and an eight-year-old is both School Age and Tween. The first step tells you to “select the age range that matches the child,” and for those ages two buttons match. Meanwhile, a chip in the header says “Ages 0–17,” while the last card just says “12+ years.”</p><h3>Difficulty does less than it looks</h3><p>Each card has a colored difficulty badge. All 20 Infant and Toddler activities are Easy, and every Advanced activity is in the Tween or Teen group. For the youngest kids, the badge says the same thing on every card, so it gives a sitter nothing to choose with.</p><h3>What I learned</h3><ul><li>Read your page copy against your controls. If the text says “filter by category,” there should be a category filter.</li><li>Don’t hard-code a “+” next to a count. Show the real number, or leave the number out.</li><li>Age ranges need one owner per age. Use ranges that don’t share endpoints, like “3–4” and “5–7,” or let people enter an age.</li><li>A badge that’s the same on every card is decoration. Either make the data vary or drop the badge.</li></ul><h3>What I’m fixing</h3><p>Next, I’m adding category and difficulty toggles above the list, plus an “uses things you already have” filter based on the materials, and putting the selection in the URL so a parent can send a sitter a link to “indoor ideas for a toddler.” I’ll make the age ranges non-overlapping, show the real count, and make the header chip match the last group. I also want a mixed-ages view, because the page’s own FAQ tells sitters with two kids to use the younger child’s group, and right now you can only look at one group at a time.</p><p>The activity ideas are the hard part, and they’re done. The finder around them needs to do what its own copy says.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=46e5e0b5e4fd" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[My Flexbox playground’s preview uses CSS it never gives you]]></title>
            <link>https://medium.com/@eibrahim/my-flexbox-playgrounds-preview-uses-css-it-never-gives-you-500a58dbd5d0?source=rss-3dad5916cd29------2</link>
            <guid isPermaLink="false">https://medium.com/p/500a58dbd5d0</guid>
            <dc:creator><![CDATA[Emad Ibrahim]]></dc:creator>
            <pubDate>Mon, 09 Nov 2026 13:05:59 GMT</pubDate>
            <atom:updated>2026-11-09T13:05:59.682Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jU-5TQ-iAuprU274K-TrKg.jpeg" /></figure><p>I built a free <a href="https://www.zapcode.dev/tools/css-flexbox-playground">CSS Flexbox Playground</a> for Zap Code. You pick a preset (Centered, Sidebar, Holy grail, Navbar, or Card grid), change the container and item properties, watch the layout update, and copy the generated CSS into your own project.</p><p>The copy button is the whole point. While writing this up, I pasted that CSS into a plain page next to the preview and compared the two. They don’t match, because the preview gets help from styles that never reach the copied CSS.</p><h3>How it works</h3><p>It’s one client-side Next.js page in TypeScript and Tailwind, with no backend. The container controls cover display, flex-direction, justify-content, align-items, align-content, flex-wrap, and gap. Each item has flex-grow, flex-shrink, flex-basis, align-self, and order. The page keeps that state in React and rebuilds two things on every change: inline styles for the preview, and a CSS string with a .flex-container rule and one .flex-item:nth-child() rule per item.</p><p>Those two outputs come from two different functions. That’s where the problems start.</p><h3>The preview has a height your page doesn’t</h3><p>The preview function adds sizing that the CSS function leaves out. The container gets a 360px minimum height. Each item gets its own minimum height from the preset, between 72px and 240px, plus a 96px minimum width. None of these appear in the generated CSS.</p><p>The Centered preset says it centers “a single element both horizontally and vertically in one move.” In the preview, the box sits 96px from the top of a 360px container, so it looks centered. I pasted the generated CSS into a container of the same width, with a single box inside. That container is 56px tall, exactly as tall as the box. Horizontal centering still works. Vertical centering does nothing, because there’s no extra height to center in. The 360px that made it work belongs to the preview, not to your page.</p><p>The Sidebar preset has the same problem. In the preview, both columns stretch to 322px. With the copied CSS, they’re 56px tall. align-items: stretch is doing its job, but it’s stretching to the height of the content.</p><h3>The preview is too narrow for its own default</h3><p>At desktop width, the preview sits in a 440px side column, which leaves about 306px for the flex items. The default preset is Holy grail: a header, then Nav, Main, and Aside in one row, then a footer. Nav is 170px and Aside is 150px. Add the gaps and that’s already more than 306px before Main gets anything.</p><p>So in the live preview, Aside wraps onto its own row under Nav and Main. The first layout you see when the page loads is not the three-column layout the preset is named after. The Card grid preset has the same issue. Each card has a 180px basis, so two cards need more room than the preview has, and all six stack in a single column.</p><p>The CSS for both presets is fine. At a normal page width, the holy grail lines up and the cards form a grid. The preview just never shows it.</p><h3>You can’t type a negative order</h3><p>The order field accepts values from -10 to 10, and a negative order is how you pull an item to the front. But each field clamps its value on every keystroke. When you type the minus sign, the number input reports an empty value, which becomes 0. Then you type 2. On the live page, typing -2 leaves 02 in the field and order: 2 in the CSS. The item moves the other way.</p><h3>What I learned</h3><ul><li>If a tool shows a preview and also exports code, build both from the same source. Two functions that are supposed to agree will drift apart.</li><li>Any style you add to make a preview look good is one the user doesn’t get. Either export it, or label it on the preview so people know what’s there.</li><li>Test presets at the size the preview actually renders, not the size you imagined while writing them.</li><li>Don’t clamp a number field while the user is still typing. Clamp on blur, or keep the raw text until it parses.</li></ul><h3>What I’m fixing</h3><p>Next, the preview will render the generated CSS itself, inside a frame. Any helper sizing, like a container height, will be part of the exported CSS, with a comment saying what it’s for. The Centered preset will export a height so vertical centering works when you paste it. The preview will get a width toggle so wide presets like Holy grail and Card grid can show their real layout. The number fields will keep what you type and clamp only when you leave the field.</p><p>A playground exists to show people what their CSS will do. Mine showed what my CSS plus a few hidden extras would do.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=500a58dbd5d0" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[My Markup Calculator’s Chart Contradicts Its Own Caption]]></title>
            <link>https://medium.com/@eibrahim/my-markup-calculators-chart-contradicts-its-own-caption-d92cb1af982d?source=rss-3dad5916cd29------2</link>
            <guid isPermaLink="false">https://medium.com/p/d92cb1af982d</guid>
            <dc:creator><![CDATA[Emad Ibrahim]]></dc:creator>
            <pubDate>Sun, 08 Nov 2026 13:11:28 GMT</pubDate>
            <atom:updated>2026-11-08T13:11:28.228Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lDRu35d4MiPv4mi83VDeDA.jpeg" /></figure><p>I built a free <a href="https://www.vibemart.app/tools/markup-calculator">Markup Calculator</a> for Vibe Mart. You enter any two of cost, selling price, and markup percentage, and it works out the third, plus profit and margin. Under the results there’s a small “Markup vs. margin” chart, a reference table that runs from 10% to 200% markup, and an FAQ.</p><p>The math is right. While writing this up, I tested the live page, and the parts around the math don’t hold up as well. The chart contradicts the sentence printed under it, and the three modes can give you three different answers for the same product.</p><h3>How it works</h3><p>It’s one client-side Next.js page in TypeScript and Tailwind, with no backend. There are three modes: Cost + Markup %, Cost + Selling Price, and Selling Price + Markup %. Each mode shows the two inputs it needs and hides the third. The page keeps all three values in state and recalculates on every keystroke. Markup is profit divided by cost. Margin is profit divided by selling price. The page also ships FAQ, HowTo, and breadcrumb structured data, and an Open Graph image rendered at the edge.</p><h3>The chart says the opposite of its caption</h3><p>Open the page and the defaults are a $50 cost with a 60% markup, so the selling price is $80. Markup is 60% and margin is 37.5%. Below that are two bars, and under them a caption: “Markup is always higher than margin for the same transaction.”</p><p>On the live page, the markup bar is 30% wide and the margin bar is 37.5% wide. The shorter bar is the bigger number.</p><p>That’s because the two bars use different scales. The margin bar maps 0 to 100%. The markup bar maps 0 to 200%, so I divided its width by two to make room for big markups. At 100% markup both bars sit at exactly half. Below that, which covers most of the everyday markups people look up, the margin bar is drawn longer than the markup bar. Above 200%, the markup bar is full and stops growing. At 300% markup it’s full, next to a 75% margin bar.</p><p>The bars carry no scale labels, so nothing tells you they’re measured differently. It’s the one picture on the page meant to explain markup versus margin, and at the defaults it shows the reverse.</p><h3>Switching modes changes the product</h3><p>The page keeps a cost, a markup, and a selling price in state the whole time, even when a mode hides one of them. Switching modes doesn’t recalculate the hidden value. It shows whatever was typed last.</p><p>Here’s the live page. In Cost + Markup %, change the cost to $100. The selling price becomes $160 at a 60% markup. Now click Cost + Selling Price, which feels like the same question asked from another side. The selling price field shows $80, the old default, and the results say markup is -20%, profit is -$20, and margin is -25%. Click Selling Price + Markup % and the cost becomes $50.</p><p>So one product with a $100 cost shows a $60 profit, a $20 loss, and a $50 cost, depending on which button you clicked last. The page never says that a hidden field is stale.</p><h3>Losses don’t show up in the chart</h3><p>Both bars clamp at zero. A $50 cost sold for $25 gives a -50% markup and a -100% margin, and the chart shows two empty bars. The numbers are correct, but the chart is blank exactly when the result most needs your attention.</p><h3>What I learned</h3><ul><li>If two bars are meant to be compared, put them on the same scale, or label each one. I scaled each bar for its own range and lost the comparison the chart was there to make.</li><li>Check a chart against its caption with real numbers. The default inputs were enough to show the problem.</li><li>When a form has modes that hide fields, hidden values go stale. Either carry the last result into the new mode, or show which values you’re using.</li><li>Clamping is fine for layout, but it shouldn’t hide the case where you’re losing money.</li></ul><h3>What I’m fixing</h3><p>Next, I’m putting both bars on one shared scale with labeled ends, so the longer bar is always the bigger number. When you switch modes, the calculator will carry over the last computed values, so a $100 cost at 60% markup stays a $160 selling price in every mode. Losses will get their own red state and a short note, not empty bars.</p><p>The formulas were the easy part. The work was in making sure the chart, the caption, and the mode buttons all describe the same transaction.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d92cb1af982d" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[My Party Checklist Moves Your Checkmarks When You Change the Guest Count]]></title>
            <link>https://medium.com/@eibrahim/my-party-checklist-moves-your-checkmarks-when-you-change-the-guest-count-34ce54d3a44d?source=rss-3dad5916cd29------2</link>
            <guid isPermaLink="false">https://medium.com/p/34ce54d3a44d</guid>
            <dc:creator><![CDATA[Emad Ibrahim]]></dc:creator>
            <pubDate>Sat, 07 Nov 2026 13:05:17 GMT</pubDate>
            <atom:updated>2026-11-07T13:05:17.160Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0u94J8Efh1MX7D5nQQKtBw.jpeg" /></figure><p>I built a free <a href="https://www.partyhubrental.com/tools/party-planning-checklist">Party Planning Checklist Generator</a> for PartyHub Rental. You pick an event type (birthday, wedding, corporate, baby shower, graduation, holiday, anniversary, or reunion), enter a guest count and a total budget, and it gives you a checklist that runs from eight weeks out to the day after. You can view it by timeline or by category, tick tasks off as you go, watch a progress bar fill up, and print it.</p><p>While writing this up, I ran the checklist logic in a script and then tested what I found on the live page. The biggest problem is with the checkboxes, which are the main thing people use. If you change your plans after generating the list, your ticks move to tasks you never did.</p><h3>How it works</h3><p>It’s one client-side Next.js page in TypeScript, styled with Tailwind, with no backend. A single function builds the list. It starts with about 36 tasks every event gets, adds a few for the event type, and adds more when you have over 50 guests or a budget over $2,000. As each task is added, it gets an id from a counter: 1, 2, 3, and so on. The page saves your ticks as a set of those ids.</p><p>The page also rebuilds the list on every render from whatever is in the form right now, not from what was there when you clicked Generate. Those two shortcuts don’t work together.</p><h3>Ticks stay on the number, not the task</h3><p>Here’s what happens on the live page. Generate a birthday checklist for 30 guests: 38 tasks. Tick “Choose and book your venue,” “Send out invitations,” and “Book entertainment.” Then change the guest count to 51, because the guest list grew.</p><p>The list jumps to 42 tasks, because big events get an event coordinator, parking, security, and a staff briefing. “Hire an event coordinator for 50+ guests” goes into the eight-weeks-out section, ahead of the invitations, so every task after it moves down one number. Your ticks don’t move with them. Now the checked tasks are the venue, “Hire an event coordinator,” and “Send out invitations.” “Book entertainment” is unchecked, and the counter still says 3/42 completed.</p><p>Switch the event type to Wedding and it gets worse. The heading changes to “Your Wedding Reception Checklist,” and the ticks land on “Book officiant and wedding planner” and “Start dress/attire shopping.” You didn’t do either one. In my script, after the 30-to-51 change, 34 of the 38 original ids pointed to a different task.</p><p>Your ticks only clear when you click Generate again, and the page never tells you that you should.</p><h3>The guest count field won’t let you delete it</h3><p>The guest field won’t go below 1, and it enforces that on every keystroke. If you click after the 30 and press Backspace twice, the field shows 1. Type 60 and you get 160. While that happens, the list briefly says “Create your guest list (aim for your target of 1 guests),” and once it reaches 160 it adds the large-event tasks. That shifts your ticks again. The budget field does the same thing with a $100 minimum.</p><h3>The FAQ promises more than the code does</h3><p>One FAQ answer lists the things people most often forget: ice and coolers, trash bags, a first aid kit, phone chargers, extra seating, parking directions, a day-of timeline, and tip money for vendors. Then it says, “This checklist generator covers all of these.”</p><p>It doesn’t. I generated every list the tool can make, for all eight event types, small and large. None of them mention coolers, trash bags, phone chargers, or extra seating. Parking only shows up above 50 guests. Tipping is a day-after task, “Tip and review your vendors,” not “bring cash.”</p><p>The FAQ also says weddings may need three to six months. The checklist always starts at eight weeks, so “Book officiant and wedding planner” and “Start dress/attire shopping” are due eight weeks before a wedding. A wedding checklist shares 36 of its 42 tasks with a birthday checklist. An anniversary gets no tasks of its own at all. The budget only matters once it passes $2,000, so a 150-guest wedding with a $2,000 budget gets no photographer task.</p><h3>What I learned</h3><ul><li>Give list items ids that come from what the item is, not from where it sits in the list. A counter is fine until the list changes.</li><li>If a list is generated from inputs, either keep it fixed until the user regenerates it, or carry their progress over by task. Silently changing it under their ticks is the worst option.</li><li>Don’t enforce a minimum on every keystroke in number fields. Let people clear the field while typing and check the value when they leave it.</li><li>Check marketing copy against the code. “Covers all of these” took one script to disprove.</li></ul><h3>What I’m fixing</h3><p>Next, I’m going to give each task a stable key based on its wording, so ticks follow the task. The generated list will stay fixed until you click Generate again, with a note when the form no longer matches it. The guest and budget fields will only enforce their minimums when you leave the field. I’ll add coolers, trash bags, chargers, extra seating, and parking directions to the base list. Weddings will get a longer timeline that starts months out, and anniversaries will get a few tasks of their own.</p><p>It’s a small tool, but the checkboxes are what make a checklist useful. If your ticks can move on their own, the progress bar can’t be trusted.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=34ce54d3a44d" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[I Built a Tone Checker. Its Own Rewrite Still Needs an Editor.]]></title>
            <link>https://medium.com/@eibrahim/i-built-a-tone-checker-its-own-rewrite-still-needs-an-editor-17ca36a87657?source=rss-3dad5916cd29------2</link>
            <guid isPermaLink="false">https://medium.com/p/17ca36a87657</guid>
            <dc:creator><![CDATA[Emad Ibrahim]]></dc:creator>
            <pubDate>Fri, 06 Nov 2026 13:04:01 GMT</pubDate>
            <atom:updated>2026-11-06T13:04:01.498Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Z-TPK3SFx8ZfwtzHWjYGZA.jpeg" /></figure><p>I built a free <a href="https://www.chatspark.dev/tools/tone-analyzer">Message Tone Analyzer for ChatSpark</a>. Paste a draft and it shows tone signals, readability, possible friction, and three suggested rewrites. It is meant for the moment before sending a support reply or follow-up email, when a second look can catch wording that lands badly.</p><p>The implementation is deliberately small: a client-side Next.js page, TypeScript rules, and no model call for the analysis. That makes the scoring easy to inspect. It also makes the limits easy to reproduce.</p><h3>What shipped</h3><p>The checker scores eight categories: friendly, formal, casual, passive-aggressive, urgent, sarcastic, angry, and professional. Phrases, punctuation, and capitalization contribute signals. The results explain the wording behind the scores rather than showing only a label.</p><p>There is also a readability estimate, average sentence length, and a friction score. The rewrite section offers warmer, professional, and shorter versions, with copy buttons. Preset messages make it possible to try the tool without pasting a real conversation.</p><p>The release also includes page metadata, a landscape social image, and an entry in the tools index. Those are the less interesting pieces of building a tool, but a usable page still needs to be discoverable and shareable.</p><h3>One preset tells the story</h3><p>I tried the built-in passive-aggressive example on the live page: “Per my last email, I am still waiting on the file. Not sure why this has not been sent yet…”</p><p>The checker labeled it passive-aggressive and high friction. It showed readability at 99/100 and friction at 62/100. That is a useful distinction: a message can be easy to read and unpleasant to receive.</p><p>The explanations pointed to blame-heavy phrases, indirect criticism, and the ellipsis. Those are concrete things a writer can reconsider. The percentages are rule scores, though, not measured probabilities of how a particular recipient will feel.</p><h3>Replacing phrases is not editing a sentence</h3><p>The professional rewrite began: “Hello,” followed by “following up on my previous email, I am following up on the file.” It ended with “Thank you.”</p><p>The repeated phrase comes directly from two replacements: “per my last email” becomes “following up on my previous email,” while “still waiting” becomes “following up.” Each substitution looks reasonable in isolation. Together, they make an awkward sentence, and the body starts with a lowercase letter.</p><p>This is the boundary of the current approach. It can identify wording worth reviewing and soften specific phrases. It cannot reliably compose those edits into a polished message. Calling an output “Professional rewrite” does not make it ready to send.</p><h3>Why keep the rules?</h3><p>The tone-analysis and rewrite functions execute in the browser rather than sending the draft to a model API. They give immediate feedback and leave the matching rules available to inspect. For a quick writing aid, that is a useful starting point.</p><p>The tradeoff is context. For example, the rules include “great job” as a sarcasm signal. The same phrase can be sincere praise. A score should invite a second look, with the sender making the final judgment.</p><h3>What comes next</h3><p>My next priority would be testing complete rewritten messages, including the presets, rather than checking substitutions one at a time. Repeated phrases, sentence capitalization, and preserved meaning deserve attention before adding more tone categories.</p><p>For now, use the output as a draft to edit. You can <a href="https://www.chatspark.dev/tools/tone-analyzer">try the tone analyzer here</a>. The most useful part may be the pause it creates before you hit send.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=17ca36a87657" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[My Dog Name Generator’s Random Button Picked Iris and Showed Me 27 Dogs]]></title>
            <link>https://medium.com/@eibrahim/my-dog-name-generators-random-button-picked-iris-and-showed-me-27-dogs-b4471ac0c5fc?source=rss-3dad5916cd29------2</link>
            <guid isPermaLink="false">https://medium.com/p/b4471ac0c5fc</guid>
            <dc:creator><![CDATA[Emad Ibrahim]]></dc:creator>
            <pubDate>Thu, 05 Nov 2026 13:04:57 GMT</pubDate>
            <atom:updated>2026-11-05T13:04:57.337Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5eufjbAUAYDJdG7x4N9yhQ.jpeg" /></figure><p>I built a free <a href="https://www.petroute.app/tools/dog-name-generator">Dog Name Generator</a> for PetRoute. It’s a searchable list of dog names, each with a meaning, an origin, a few suggested breeds, and tags for gender, size, personality, and theme. You can filter on any of those, heart the names you like (they’re saved in your browser), and hit a Random button when you don’t know where to start.</p><p>While writing this up, I went through the name list with a script and then checked what I found on the live page. The Random button and the name count both had problems I hadn’t noticed when I shipped it.</p><h3>How it works</h3><p>It’s one client-side Next.js page in TypeScript, with no backend. The names live in a hard-coded array. Each filter is a simple equality check, and the search box matches your text anywhere in a name, its meaning, or its breed list. After filtering, the page removes repeated names, keeping the first one it finds, and shows 24 at a time with a “Show more” button. Favorites go into localStorage, so there’s no account.</p><p>That keeps the page fast and easy to change. The problems come from two shortcuts: Random reuses the search box, and the de-duplication runs after the filters.</p><h3>Random picks a name, then searches for it</h3><p>Random doesn’t show you the name it picked. It types that name into the search box. Search is a substring match across names, meanings, and breeds, so short names catch a lot of other dogs.</p><p>When Random lands on Iris, you get 27 results. “Iris” is inside “Irish,” and the list is full of Irish Setters and Irish Wolfhounds. Iris is the eighth card, after Murphy, Rusty, Molly, Finn, Forrest, Moss, and Fern. Sky does the same thing through “Husky”: 28 results, with Sky ninth. Sam matches every Samoyed and returns 11.</p><p>Most picks are fine. I clicked Random eight times on the live page and got exactly one result every time. But by my count, 33 of the 329 names return more than one card, and when that happens the name Random chose isn’t marked anywhere. You see a page of dogs and have to guess which one it meant.</p><h3>351+ names, 329 on the page</h3><p>The intro says “Browse 351+ dog names.” That number is the length of the array, and the array has duplicates: 20 names appear more than once, which adds 22 extra rows. With no filters, the page shows 24 names and “Show more (305 remaining),” which is 329. The code comment above the array says the goal was 500+, and I stopped well short of it.</p><p>The duplicates aren’t identical, either, and that leaks into the filters. Charlie is tagged male in one row and unisex in another, so he appears under both. Remi is a medium male in one row and a small female in another. Bark Twain is both male and female. Because de-duplication runs after the filters, a name’s tags depend on which filter you pick.</p><h3>Smaller things</h3><ul><li><strong>The origin filter is mostly “English.”</strong> 224 of the 351 rows are tagged English. There’s no Greek option, so Athena, whose meaning is literally “Greek goddess of wisdom,” is filed under English. So are Zeus, Apollo, and Hera, and so are Anubis and Isis from Egyptian mythology. Only the Norse names got their own origin.</li><li><strong>The advice and the list pull in different directions.</strong> The tips say to pick names with one or two syllables, and the funny section suggests Droolius Caesar, Sir Barks-a-Lot, and Snickerdoodle. They’re fun, but the page doesn’t say they break its own advice.</li></ul><h3>What I learned</h3><ul><li><strong>Don’t reuse search to show a selection.</strong> Search finds everything that contains your text, while a random pick means one specific record. Treating them as the same thing worked for most names and failed on the short ones.</li><li><strong>Count what users see, not what’s in the array.</strong> The headline number came straight from the data, duplicates included. Counting the de-duplicated list, the same one the page renders, would have caught it.</li><li><strong>Put a uniqueness check on hand-built data.</strong> I added names in batches to reach a target, and the batches overlapped. A small test that fails on a repeated name would have caught all 20 of them, along with their conflicting tags.</li></ul><h3>What’s next</h3><p>First, make Random show the picked name on its own instead of typing it into search, and highlight it. Then merge the duplicate rows into one entry each with consistent tags, add a test that rejects repeated names, and have the intro count the names the page actually shows. After that I’ll add a Greek origin for the mythology names and mark the long joke names as just for fun.</p><p>If you’re naming a new dog, <a href="https://www.petroute.app/tools/dog-name-generator">try the generator</a> and filter by breed or personality. If Random gives you a page full of Irish Setters, look for the name in the search box.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b4471ac0c5fc" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>