<?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"><channel><title><![CDATA[AnimeSelfie]]></title><description><![CDATA[AnimeSelfie]]></description><link>https://animeselfie.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 01 Nov 2026 05:43:31 GMT</lastBuildDate><atom:link href="https://animeselfie.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building AnimeSelfie: A Focused AI Photo App with TypeScript]]></title><description><![CDATA[Inside AnimeSelfie: TypeScript presets, upload validation, photo preparation, and clear progress states for an AI image app.]]></description><link>https://animeselfie.hashnode.dev/building-animeselfie-a-focused-ai-photo-app-with-typescript</link><guid isPermaLink="true">https://animeselfie.hashnode.dev/building-animeselfie-a-focused-ai-photo-app-with-typescript</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[React]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Jonesjson]]></dc:creator><pubDate>Fri, 30 Oct 2026 03:40:00 GMT</pubDate><content:encoded><![CDATA[<p>For someone uploading a selfie, the useful decisions are concrete: which photo to use and which visual style to choose. The application still has to handle uncertain generation times, rejected inputs, and interrupted status checks. Those details shape how understandable the tool feels.</p>
<p>I'm the creator of <a href="https://animeselfie.com">AnimeSelfie</a>, an anime eye collage tool. This article was drafted with AI assistance from the project's source code and documentation. It looks at how a small set of visual choices connects to validation and progress reporting in a TypeScript application.</p>
<h2>Give each style a clear visual meaning</h2>
<p>AnimeSelfie creates a collage from a solo adult portrait. The intended result combines one illustrated eye with three small stickers of an original anime counterpart based on the person in the photo.</p>
<p>The interface offers three styles:</p>
<ul>
<li><strong>Star:</strong> an illustrated eye inside a star-shaped cutout, with matching character stickers.</li>
<li><strong>Heart:</strong> a heart-shaped eye cutout using the same general collage composition.</li>
<li><strong>Manga:</strong> a monochrome treatment with ink lines and screentones across the composition.</li>
</ul>
<p><img src="https://animeselfie.com/imgs/anime-eye/gpt-star-eye-v1.webp" alt="A fictional adult portrait with a star-shaped illustrated eye and three anime character stickers" /></p>
<p><em>AI-generated AnimeSelfie demonstration featuring a fictional adult. This is an existing product example, not a customer result. Individual outputs vary.</em></p>
<p>These presets give the reader something specific to recognize in an example image. They also make output review more concrete: check the eye's placement, the resemblance of the character stickers, and whether the composition leaves the face readable.</p>
<p>The controls specify an intended composition. They cannot guarantee perfect alignment or likeness in every generated image, so reviewing the result remains part of the workflow.</p>
<h2>Share the style definition across the interface and API</h2>
<p>The application uses React and TanStack Start, with TanStack Query for server state. Its style choices begin with a small shared TypeScript definition:</p>
<pre><code class="language-ts">export const ANIME_STYLES = ['star', 'heart', 'manga'] as const;
export type AnimeStyle = (typeof ANIME_STYLES)[number];

export function isAnimeStyle(value: unknown): value is AnimeStyle {
  return (
    typeof value === 'string' &amp;&amp; ANIME_STYLES.includes(value as AnimeStyle)
  );
}
</code></pre>
<p>This excerpt comes from the project's shared image-workflow definitions. The workbench stores its selection as <code>AnimeStyle</code>, and the job-creation endpoint calls <code>isAnimeStyle</code> on the incoming request before starting a generation.</p>
<p>That gives the same set of values two jobs. The union helps application code use valid styles during development. The runtime check validates JSON arriving over the network, where a caller can send an arbitrary string regardless of what the interface displays.</p>
<p>The three style buttons also expose their selection through <code>aria-pressed</code>. Selection is therefore represented in application state and available to assistive technology, alongside the visible selected-button styling.</p>
<p>For another preset-based tool, this pattern is straightforward to reuse: define the allowed values once, derive the type, and validate the incoming value at the API boundary.</p>
<h2>Separate file checks from portrait checks</h2>
<p>The browser gives early feedback for empty files, unsupported MIME types, and the upload-size limit. The server independently checks the file and enforces a maximum of 10 MiB.</p>
<p>For JPEG, PNG, and WebP, the server inspects signature bytes and requires the detected format to match the supplied MIME type. This catches some mismatches that a filename or browser-provided type alone would miss.</p>
<p>A signature check has a limited role. It is not a complete image decoder, and it cannot establish whether the photograph is appropriate for the product.</p>
<p>Portrait permission and content checks sit alongside file validation. The upload service requires consent, stores the source privately, and passes a temporary link to an external moderation service. Before generation, the service checks that the prepared source belongs to the account and has an allowed moderation result.</p>
<p>These checks serve different purposes:</p>
<table>
<thead>
<tr>
<th>Check</th>
<th>Question it addresses</th>
</tr>
</thead>
<tbody><tr>
<td>File size and format</td>
<td>Does the upload meet the application's file requirements?</td>
</tr>
<tr>
<td>Account ownership</td>
<td>Is this user allowed to submit this stored source?</td>
</tr>
<tr>
<td>Permission confirmation</td>
<td>Has the user confirmed permission to use the portrait?</td>
</tr>
<tr>
<td>Moderation result</td>
<td>Has the automated content check allowed this image?</td>
</tr>
</tbody></table>
<p>Automated moderation does not prove a person's identity, adulthood, or consent. The permission confirmation remains a user declaration, and the tool is intended for permitted adult portraits.</p>
<h2>Prepare the photo while the user chooses a style</h2>
<p>The workbench can start preparing an upload once four conditions are present: a selected file, consent, a signed-in session, and service readiness. This allows preparation and moderation to overlap with style selection.</p>
<p>The interface reports that the photo is being prepared and checked, then indicates when it is ready. Generation waits for the prepared source ID.</p>
<p>The useful design detail is the distinction between <strong>a file selected in the browser</strong> and <strong>a source ready for generation</strong>. Those events happen at different times. Showing their states separately helps explain why a selected preview may still need preparation before it can be submitted.</p>
<p>This describes the scheduling in the implementation; it is not a measured claim about how much faster generation becomes.</p>
<h2>Let progress messages reflect what the application knows</h2>
<p>The workbench polls job state while a generation is active. It also continues checking when a failed job still has a credit refund pending. Polling stops once the job has a terminal status and settlement is no longer pending.</p>
<p>The progress component uses elapsed time to change its explanation when a job takes longer. The local clock changes the message; the server remains responsible for deciding the job's outcome.</p>
<p>That separation is particularly useful when a status request fails. A network problem fetching the latest state leaves the generation outcome uncertain. The interface explains that synchronization problem and provides a way to check again, rather than treating the failed status request as proof that image generation failed.</p>
<p>Confirmed failures carry structured reasons such as <code>content</code>, <code>source</code>, <code>timeout</code>, and <code>unavailable</code>. The interface maps those reasons to translated messages. If a credit refund is still pending, that message takes priority.</p>
<p>This avoids telling someone that credits have already returned while settlement is still underway. It also lets translations describe the same underlying condition without parsing an English error string.</p>
<p>For a similar application, useful review questions are: What does the screen say when the network drops? What does it say when processing is slow? Can it distinguish a failed image from an unfinished refund? These are suggestions for evaluating a workflow, not claims of live tests performed for this article.</p>
<h2>Explore the workflow with one portrait</h2>
<p>AnimeSelfie brings these pieces together in a short creation flow: sign in, choose a style, select a portrait, confirm permission, and generate. Completed results can be viewed and downloaded from My photos. Each generation uses three credits; failed jobs enter a credit-refund process.</p>
<p>Uploads and results are accessed through the owner's account, while external services handle moderation and image generation. Private account access does not mean the photos are processed entirely on the user's device.</p>
<p>You can explore the <a href="https://animeselfie.com/create">AnimeSelfie creation page</a>. Start with a clear adult portrait you have permission to edit, then inspect the eye alignment and character details in the result.</p>
<p>For developers, the transferable idea is to give each part of the experience a precise meaning: a selected style, a prepared upload, an active job, and a settled outcome. Which of those states has been hardest to explain in an application you've built?</p>
]]></content:encoded></item></channel></rss>