
Web Application

A contemplative, multi-lingual web application designed to guide users through an introspective journey. It maps human emotional states to colors, numbers, and Divine Names (Asma-ul Husna) to offer deep, psychologically grounded reflections.
The mobile counterpart is not a separate product: it is a thin WebView wrapper packaged with Expo that shows the same web address β one codebase, two distribution channels.
Most contemplation apps pull their content from a server and record what the user selects. The real problem here was not content but the tension between privacy and continuity: the user enters a mood β not personal enough to justify storing it, yet valuable enough that losing it mid-journey hurts. At the same time the application had to stay entirely static, meaning no user data was to reach a server at all.
One persona, two contexts. Someone opening the app on their phone during a short break, wanting a contemplation text after a few taps: the step count must stay low and going back must be possible on every screen. If that same person returns regularly they want to see their earlier journeys β but they will not create an account for it. Four languages (TR/EN/DE/ES) deliver the same flow with the same Divine Name texts in the reader's own language.
Sole developer. Data design of the mapping model, content structure across four languages, client-side state machine, ambient sound via Web Audio, PWA setup, security headers and Vercel deployment.
Next.js 16 App Router with no server side at all: no API routes, no database, no sessions. Every localized route is statically generated. The flow runs as a five-step state machine inside a single client component β intro, mood, color, number, Divine Name β with transitions handled through useReducer. Routing uses next-intl with no prefix on the default locale (localePrefix "as-needed"); on the unprefixed /privacy path, the proxy layer redirects the visitor to their own language when a locale cookie is present. Active session state lives in sessionStorage, completed journey history in localStorage.
The mood β color β number β Divine Name chain is not something computed; it is an editorial decision. Which five colors and which five Divine Name numbers belong to each mood was chosen by hand and sits in code as fixed tables for eight moods. Building a rule engine would have simulated a generality that does not actually exist.
Trade-offChanging content β adding a mood, updating a Divine Name assignment β requires a code change and a new deployment; it cannot be done from a panel. The second cost is in the flow itself: only the chosen number determines the outcome, the chosen color does not feed into it. The color step is a meaningful pause for the user but only a visual variable for the system.
A mood entry is too ordinary to justify making someone open an account, and too personal to deserve collecting on a server. The last 10 journeys are written to localStorage and the user can clear all of them with one button. This leaves the application with no backend at all, and with no stored user data either.
Trade-offHistory is bound to the device: change phones or clear browser data and there is no way to recover it. In private mode or when the quota fills up the write fails silently β the code swallows the error, because there is no meaningful recovery step to show the user. The ten-record cap is also an arbitrary ceiling; the eleventh journey deletes the oldest one.
Two sounds were needed: a calm tone on the intro screen and rain on the Divine Name screen. Both are generated in the browser β rain from a pink-noise buffer through a low-pass filter, the tone from four harmonic oscillators with diminishing gain. Not a single audio file enters the repository, so there is no media to download and no CSP rule needed to allow it.
Trade-offThe sound is regenerated on the client every session: a two-second noise buffer is filled sample by sample, which is a noticeable CPU cost on a low-powered phone at the moment sound is switched on. Because an AudioContext cannot start without a user gesture, sound only comes in after the button is pressed. And changing the sound is no longer a design task: asking for a new texture means writing DSP code.
The step of an abandoned journey lives in sessionStorage; the server cannot know it. The HTML the server produces always contains the intro screen, and saved state is applied only once hydration completes β through an explicit hydration flag built on useSyncExternalStore. This keeps a single static output per language, cacheable on the CDN as-is.
Trade-offA returning user sees the intro screen on the first frame and then jumps to where they left off β a short but real visual jolt. The alternative, reading state on the server, would have taken the page out of static generation and turned it into a render that runs on every request.
With no backend, the stack choice came down to a single question: a four-language structure that can be fully statically generated while carrying one interactive island. next-intl binds metadata and hreflang generation to the routing layer itself, removing the need to write SEO by hand per language. Framer Motion handles exit animation between steps via AnimatePresence β as a screen changes its component leaves the tree, which made this impossible to do in CSS alone. Tailwind and CSS custom properties let the accent color of the selected mood propagate across the whole interface from a single variable.
Six HTTP headers are defined in next.config.ts: CSP, nosniff, framing denial, referrer policy, XSS filter and a Permissions-Policy that closes camera, microphone, geolocation and payment. In the CSP, connect-src is open only to Vercel Analytics endpoints while font-src and worker-src stay on the same origin. The attack surface is narrow by construction: there is no form, no session, no API endpoint and no database, and no user input ever leaves the device. The known weakness is 'unsafe-inline' in script-src β left open for the inline bootstrap script Next.js emits; moving to a nonce-based CSP would mean giving up static generation. In production, console output is stripped apart from error and warn.
Every localized route is statically generated; there is no server computation on first paint. All five step screens are split into separate chunks via dynamic import, with only the background and progress bar in the main bundle β the cost being that on a slow connection a step transition waits for its chunk to download the first time. @next/bundle-analyzer sits ready in the repo behind an ANALYZE flag. No stored, date-stamped Lighthouse output exists for this project, so no page speed score is written here.
Today I would either make the color step genuinely affect the outcome or remove it from the flow. As it stands the user makes a choice and that choice changes nothing, which means the interface does not keep the promise it makes. Second, I would move the mapping tables out of code and into content files read at build time β in Gorev Kahramani and in this portfolio itself, content sits as MDX/JSON and passes through schema validation; the same approach here would make a content change stop being a code change, without giving up anything in static generation.
Tell me what you want to build; I'll tell you up front how long it takes and where to start.