Skip to content

Repository files navigation

Visa Wizard

A multi-step travel visa application wizard built with Nuxt 4. Users select citizenship and destination countries (powered by the REST Countries API), enter personal details, review their application, and submit it to a session list.

Production

Live Demo: https://visa-wizard-eta.vercel.app/

Features

  • Searchable country dropdowns with flag, name, and capital display
  • Fuzzy country search (e.g. Grmany matches Germany)
  • Reusable FormInput component for all text, email, tel, number, and date fields
  • Shared CountryDropdownPanel + custom scrollbar for country and phone-prefix lists
  • Three-step wizard with free navigation and inline validation
  • Phone calling-code prefix derived from citizenship country - updates automatically when the phone field is blank; if a number is already entered, the prefix is kept and an orange warning is shown when citizenship (and its dialing code) changes, until the user edits the phone or prefix
  • Review screen with country region, population, and languages
  • Submitted applications list with status badges (click to cycle status)
  • Session persistence via localStorage (lean records only - see privacy note below)
  • Step transition animations

Privacy note

Submitted applications are stored in the browser’s localStorage under visa_applications for demo convenience. Only a lean record is persisted: applicant display name, citizenship/destination summary (name, flags, codes, region, population), submission date, and status. Email, phone, date of birth, and passport number are not written to storage. This is still client-side data on a shared machine - do not use real passport details in production demos.

Prerequisites

  • Node.js 20+ (22 recommended - matches the Docker image)
  • npm 9+
  • A REST Countries API key (NUXT_REST_COUNTRIES_API_KEY)

Setup & Installation

  1. Clone the repository

    git clone https://github.com/web-master00/visa-wizard.git
    cd visa-wizard
  2. Install dependencies

    npm install
  3. Configure environment variables

    Copy the example file and add your API key:

    cp .env.example .env
    NUXT_REST_COUNTRIES_API_KEY=your_api_key_here
  4. Start the development server

    npm run dev

    The app runs at http://localhost:3000.

Available Scripts

Command Description
npm run dev Start the Nuxt dev server
npm run build Build for production
npm run preview Preview the production build locally
npm test Run unit tests (Vitest)
npm run typecheck Run Nuxt / Vue TypeScript checks
npm run lint Alias for typecheck

Deployment

Docker

Run the app in a container with Docker Compose:

# Set your API key in .env first, then:
docker compose up --build

The app is served at http://localhost:3000.

The Dockerfile uses a two-stage build (builder → runner) on node:22-alpine, copies only the .output bundle into the final image, and runs as a non-root nuxt user.

Production build (without Docker)

npm run build
node .output/server/index.mjs

Set NUXT_REST_COUNTRIES_API_KEY in the environment of the host or process manager.

Architecture Decisions

Nuxt 4 with a server API proxy

Country data is fetched through a Nitro route at server/api/countries.get.ts instead of calling the REST Countries API directly from the browser. This keeps the API key on the server, normalizes the v5 API response into a consistent Country shape, and caches the full country list in memory (1-hour TTL) after the first request.

Composable-based state management

Wizard state lives in useVisaWizard and country data in useCountries. In the running app, useCountries uses Nuxt useFetch (keyed countries-list). Outside a component instance (unit tests), it falls back to a module-level fetch cache. Wizard form state uses module-level reactive refs so step navigation does not reset filled-in data. Submitted applications are persisted to localStorage (lean shape) and reloaded on mount.

Custom UI without a component library

Per the assignment constraints, no pre-built UI library (Vuetify, Element Plus, etc.) is used. All components - including the searchable CountrySelect dropdown with keyboard navigation and a custom scrollbar - are built with Vue 3 and Tailwind CSS v4 (@tailwindcss/vite). Design tokens live in app/assets/css/tailwind.css (@theme).

Reusable FormInput component

All text-based form fields are consolidated into a single FormInput component (app/components/FormInput.vue) instead of duplicating raw <input> markup and styles across the wizard. It supports text, email, tel, number, and date input types, and exposes three visual variants:

Variant Used for
default Standard labelled fields (name, email, passport, date of birth) with optional error state
search Compact search inputs inside country dropdowns
embedded Borderless input nested inside composite controls (e.g. phone number beside the calling-code selector)

The component handles optional label/error rendering, date-picker calendar icon, uppercase passport formatting, and muted placeholder styling for empty date fields. It uses defineModel for two-way binding and exposes a focus() method so parent dropdowns can auto-focus search inputs on open.

Used in PersonalDetailsForm.vue (full name, email, phone, date, passport) and CountryDropdownPanel.vue (country search).

Custom validation (no validation library)

Form rules are implemented in app/utils/visaValidator.ts as plain functions (validateStep1, validateStep2). The composable calls these on step advance and submit, surfacing inline error messages in each form field.

Fuzzy country search

useCountries implements a subsequence-matching fuzzyMatch helper (with Unicode normalization) so partial or mistyped queries like Untd Stats still surface the correct country. This avoids pulling in a separate search library.

Step-based component rendering

app.vue renders the active step with a dynamic <component :is="..."> and a fade transition (mode="out-in") for smooth step changes. StepProgress allows clicking back to completed steps while validating forward navigation.

Testing

Unit tests use Vitest with happy-dom and @vue/test-utils. Coverage includes visaValidator utilities, applicationStorage, the useCountries composable, and the StepProgress component.

Project Structure

app/
├── app.vue                  # Root layout, step orchestration
├── components/
│   ├── AppIcon.vue          # Inline SVG icon helper
│   ├── FormInput.vue        # Reusable input (default / search / embedded variants)
│   ├── CountryDropdownPanel.vue # Shared searchable list + custom scrollbar
│   ├── CountrySelect.vue    # Searchable country dropdown
│   ├── GeographyStep.vue    # Step 1 - citizenship & destination
│   ├── PersonalDetailsForm.vue  # Step 2 - applicant details
│   ├── ReviewSubmit.vue     # Step 3 - review summary
│   ├── StepProgress.vue     # Wizard step indicator
│   └── ApplicationsList.vue # Submitted applications table
├── composables/             # useVisaWizard, useCountries, scrollbar / click-outside
├── utils/                   # visaValidator, applicationStorage
└── assets/                  # CSS, icons, images
server/
└── api/countries.get.ts     # REST Countries proxy + TTL cache
test/                        # Vitest unit tests

Environment Variables

Variable Required Description
NUXT_REST_COUNTRIES_API_KEY Yes Bearer token for the REST Countries v5 API

About

Multi-step travel visa application wizard built with Nuxt 4, Tailwind CSS, and the REST Countries API.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages