Skip to content

NicolasFlorez130/ionic-react_tt

Repository files navigation

products-tt

A small cross-platform (web + iOS) products catalog built as a technical challenge. The app lets the user search a product catalog, browse product details, and keep a personal wish list that persists locally on the device.

This was my first project with Ionic, and I took the opportunity to also try GraphQL — since the Platzi Fake Store API exposes a GraphQL endpoint — to learn both in a realistic setting. What I liked most about Ionic is how close the web-first DX feels to any other React project, while still giving you native-grade performance and UI on mobile.

It's also a deliberate example of the structure I use for any React project, web or mobile: the same conventions for routing, views, components, and state management. The paradigm doesn't change with the target — only the shell around it does.

Screenshots

Products list (search + infinite scroll) Product details (image carousel + wish list toggle)
Products list view with the search bar filtering by "sh" and a scrollable list of results grouped by category Product details view showing a wireless headphones product, its price, category, and description, with a bookmark button in the header to add it to the wish list

The bottom tab bar is Ionic's IonTabs, and each tab owns its own navigation stack — so tapping a product from the Products tab and from the Wish List tab pushes the same ProductDetailsView but inside different stacks, preserving the correct back-navigation context.

Stack

Core

  • Ionic React — UI framework and component library. Provides the native-feeling shell (tabs, pages, router outlets, back navigation, infinite scroll, inputs, cards, etc.) that adapts automatically to web and iOS.
  • Capacitor — native runtime bridge. Packages the web build as a native iOS app and exposes device APIs. Used here for @capacitor/preferences (persistent key-value storage for the wish list), plus haptics, keyboard, status-bar, and app.
  • React 18 + TypeScript.
  • Vite — dev server and build tool, with @vitejs/plugin-react and @vitejs/plugin-legacy for older-browser support.

Data layer

  • Apollo Client — GraphQL client with in-memory cache. Configured once in src/config/apollo-client.ts against the Platzi Fake Store GraphQL API (https://api.escuelajs.co/graphql) and injected via ApolloProvider at the app root.
  • GraphQL — all product data (list, search, details) is fetched through typed queries defined in src/queries/products.queries.ts using TypedDocumentNode for end-to-end type safety between queries and components.

State

  • Zustand — lightweight client state for the wish list (src/lib/stores/wish-list.store.ts). Every mutation mirrors its new state to @capacitor/preferences so the wish list survives app restarts. A useSetupWishlist hook hydrates the store on app boot.

Routing

  • React Router 5 + @ionic/react-router — Ionic's integration that preserves page transitions, stack history, and the iOS swipe-back gesture. Routes are centralized in src/lib/routes.ts as a typed builder, so URLs are generated — never hand-written — in components.

UI / styling

  • Tailwind CSS for layout and utility styling on top of Ionic's components.
  • Swiper for the product image carousel on the details view.
  • Ionicons for icons.

Tooling & tests

  • ESLint, Prettier
  • Vitest + Testing Library for unit tests (src/App.test.tsx, src/setupTests.ts).
  • Cypress for E2E.
  • GraphQL Code Generator (npm run compile / npm run watch).

How the pieces connect

        ┌──────────────────────────────────────────┐
        │              IonReactRouter              │
        │  ┌────────────────────────────────────┐  │
        │  │              IonTabs               │  │
        │  │  ┌──────────────┐  ┌────────────┐  │  │
        │  │  │  Products    │  │  Wishlist  │  │  │
        │  │  │   Outlet     │  │   Outlet   │  │  │
        │  │  └──────┬───────┘  └─────┬──────┘  │  │
        │  │         │                │         │  │
        │  │         └────┐      ┌────┘         │  │
        │  │              ▼      ▼              │  │
        │  │      ProductDetailsView            │  │
        │  │  (shared between both stacks)      │  │
        │  └────────────────────────────────────┘  │
        └──────────────────────────────────────────┘
                 │                       │
        ┌────────▼─────────┐    ┌────────▼─────────┐
        │   Apollo Client  │    │  Zustand store   │
        │   (GraphQL API)  │    │  + Preferences   │
        └──────────────────┘    └──────────────────┘
  • App.tsx wires the providers (ApolloProvider), sets up Ionic, hydrates the wish-list store, and mounts two tabs: Products and Wish List.
  • Each tab has its own IonRouterOutlet (products-outlet.tsx, wish-list-outlet.tsx) so each tab keeps its own navigation stack, the iOS-native way.
  • Both tabs route into the same ProductDetailsView, but under their own URL prefix — so the back button always returns to the tab you came from.
  • Views fetch data via Apollo (useQuery / useLazyQuery) against the typed queries. The products list uses IonInfiniteScroll with an offset/limit paginator and a debounced search input.
  • Adding or removing an item on the details view mutates the Zustand store, which in turn persists the updated array to @capacitor/preferences. The Wish List tab reads the same store reactively.

Project layout

src/
├── App.tsx                      # Providers, tabs, top-level routes
├── main.tsx
├── config/
│   └── apollo-client.ts         # Apollo + GraphQL endpoint
├── queries/
│   └── products.queries.ts      # Typed GraphQL queries
├── types/                       # Domain types (Product, Category, …)
├── lib/
│   ├── routes.ts                # Typed route builder
│   ├── keys.ts                  # Storage keys
│   ├── stores/
│   │   └── wish-list.store.ts   # Zustand + Preferences persistence
│   └── utils/
│       └── formatting.ts        # Currency formatting, …
├── components/                  # Reusable presentational pieces
│   ├── products-list.tsx
│   ├── product-details.tsx
│   └── category-details.tsx
└── app/
    ├── products/
    │   ├── products-outlet.tsx
    │   └── views/products-view.tsx
    ├── wish-list/
    │   ├── wish-list-outlet.tsx
    │   └── views/wish-list-view.tsx
    └── views/
        └── product-details-view.tsx   # Shared by both tabs

The split between app/ (feature views tied to a route) and components/ (reusable, route-agnostic UI) is the same one I use on web-only React projects. Routing is the only layer that changes when the target changes.

Scripts

npm run dev         # Vite dev server (web)
npm run build       # tsc && vite build
npm run ios         # ionic cap run ios -l --external (live-reload on device)
npm run test.unit   # Vitest
npm run test.e2e    # Cypress
npm run lint        # ESLint
npm run format      # Prettier
npm run compile     # GraphQL Codegen

Data source

All data comes from the Platzi Fake Store API via its GraphQL endpoint: https://api.escuelajs.co/graphql.

About

A cross-platform web and iOS product catalog built with Ionic React, Capacitor, and GraphQL. Features a persistent wish list using Zustand and native-grade navigation stacks.

Topics

Resources

Stars

Watchers

Forks

Contributors