A modern Vue 3, TypeScript, and Tailwind CSS application simulating a movie browsing platform with a shopping cart and checkout.
Eduardo Hilário - eduardohilariodev@pm.me
(Add screenshots or GIFs showcasing key features like the movie grid, infinite scroll, cart, and checkout form)
This project serves as a practical demonstration of modern frontend development techniques using the Vue 3 ecosystem. It simulates a common e-commerce flow—browsing items (movies), adding them to a cart, and proceeding through a multi-step checkout process.
The primary motivation was to build a visually appealing, highly responsive application that showcases proficiency in:
- Component-Based Architecture: Structuring a Vue application logically.
- State Management: Effectively handling application state with Pinia.
- Asynchronous Operations: Managing API calls and loading states smoothly.
- UI/UX Best Practices: Implementing features like infinite scrolling, skeleton loading, and form validation for a better user experience.
- Modern Tooling: Utilizing Vite, TypeScript, and Tailwind CSS for an efficient development workflow.
This project leverages a modern frontend stack:
- Framework: Vue 3 (Composition API,
<script setup>) - Language: TypeScript
- Build Tool: Vite
- State Management: Pinia (for managing movies, cart, form state) - Chosen for its simplicity, type safety, and integration with Vue DevTools.
- Routing: Vue Router - Handles client-side navigation and guards.
- Styling: Tailwind CSS - Utility-first CSS for rapid UI development and responsiveness.
clsx&tailwind-mergefor conditional class management.
- Animations:
motion-v: Simple declarative animations.animate.css: Pre-built CSS animations.
- Forms:
- VeeValidate: For declarative form validation.
- Zod: For schema definition and validation, integrated with VeeValidate.
maska: Input masking for improved UX (e.g., credit card numbers).
- Asynchronous Operations: Native
fetchAPI within Pinia actions. - Composables:
- VueUse (
useIntersectionObserver, etc.) - Custom composables (e.g.,
useCurrency)
- VueUse (
- Code Quality: ESLint & Prettier
The project follows a standard component-based architecture, organized for clarity and maintainability:
src/
├── components/ # UI Components (base, ui, features)
├── composables/ # Reusable Vue Composition API functions
├── lib/ # External libraries or integrations
├── router/ # Vue Router configuration
├── stores/ # Pinia state management modules
├── styles/ # Global styles, Tailwind config
├── types/ # TypeScript type definitions
├── views/ # Page-level components mapped to routes
├── App.vue # Root Vue component
└── main.ts # Application entry point
Key architectural decisions:
- Composition API: Enables better logic reuse and organization compared to the Options API.
- Pinia: Preferred over Vuex for its simpler API, better TypeScript support, and modularity.
- Utility-First CSS: Tailwind CSS allows for rapid development and consistent styling without leaving the HTML/template.
- Movie Browsing: Displays a grid of movies fetched from an API (e.g., TMDB).
- Infinite Scrolling: Automatically loads more movies as the user scrolls down, using
useIntersectionObserverto trigger API calls for the next page. - Skeleton Loading: Shows skeleton placeholders while movie data is being fetched, improving perceived performance.
- Shopping Cart:
- Add/remove movies from the cart.
- Cart state persists using Pinia and potentially local storage.
- View cart details (items, total price).
- Checkout Form:
- Multi-step form (e.g., Shipping, Payment).
- Client-side validation using VeeValidate + Zod schemas.
- Input masking for fields like credit card numbers.
- Responsive Design: Adapts seamlessly to various screen sizes using Tailwind CSS.
- Animations: Subtle UI animations enhance user interaction.
(Consider adding small GIFs here to demonstrate specific features like adding to cart or the form validation)
- Problem: Efficiently displaying a potentially large list of movies fetched from an API without impacting initial page load or requiring pagination clicks, while providing clear loading feedback.
- Solution:
- Fetched the initial set of movies on component mount (
movieStore.fetchMovies()). - Used
@vueuse/core'suseIntersectionObserverto monitor a target element near the bottom of the movie list. - When the target became visible and data wasn't already loading (
!movieStore.loadingMore), triggered thefetchMoviesaction again with the next page number. - Managed distinct loading states (
isRecommendationsLoadingfor initial load,loadingMorefor subsequent fetches) to show appropriate feedback (skeleton vs. spinner) and prevent duplicate requests.
- Fetched the initial set of movies on component mount (
- Outcome: A seamless browsing experience where movies load progressively as the user scrolls, keeping the initial load fast and UI responsive.
- Problem: Organizing application state (movies, cart, user preferences, favorites, drawer state, etc.) in a scalable, type-safe, and easily testable manner. Managing asynchronous data fetching and state mutations cleanly.
- Solution:
- Adopted Pinia as the central state management library due to its simplicity, excellent TypeScript support, and modular nature.
- Created separate stores for distinct domains (e.g.,
useMovieStore,useCartStore,useFavoriteStore,useDrawerStore) located insrc/stores/. - Defined state properties, getters (computed state), and actions (methods for mutations and async operations) within each store using the Composition API syntax.
- Leveraged TypeScript interfaces/types (
src/types/) to strongly type the state, getters, and action payloads/return values. - Handled API interactions within Pinia actions (like in
useMovieStore), updating the store's state upon successful fetching or error handling.
- Outcome: Clear separation of concerns for state management, improved code organization and maintainability, enhanced type safety reducing potential runtime errors, and simplified component logic by centralizing state access and modifications.
- Problem: Ensuring that only one overlay or drawer component (like the shopping cart or favorites list) is active/visible at any given time to prevent UI clutter and maintain a focused user experience. Coordinating the open/close state across potentially unrelated components.
- Solution:
- Implemented a dedicated Pinia store (
useDrawerStorefound insrc/stores/drawer.ts) to manage global UI state related to drawers. - Defined state properties in the store (e.g., tracking the
activeDrawertype or simply a booleanisOpenper drawer type if only one exists). - Components responsible for opening a drawer would call an action from
useDrawerStore(e.g.,drawerStore.open('cart')) which updates the relevant state. - Drawer components themselves would use this state (or a computed property based on it) to control their visibility (e.g.,
v-if="drawerStore.isCartOpen"orv-if="drawerStore.activeDrawer === 'cart'"). - Closing a drawer would involve calling another action (e.g.,
drawerStore.close()). Actions to open a specific drawer could implicitly handle closing any other active one within the store logic.
- Implemented a dedicated Pinia store (
- Outcome: Consistent and predictable UI behavior where only one drawer is displayed at a time (or as defined by the store's logic). Centralized control simplifies state synchronization and avoids prop drilling or complex event emissions for managing visibility across the application.
- Node.js (Check
.nvmrcor specify version, e.g., v18+) - npm or bun
-
Clone the repository:
git clone <your-repo-url> cd <repository-name>
-
Install dependencies:
# Using npm npm install # Or using bun bun install
-
Environment Variables:
-
Create a
.envfile in the root directory by copying.env.example(if it exists). -
Add necessary environment variables. Example:
VITE_API_BASE_URL=https://your.api.endpoint # e.g., TMDB API VITE_API_KEY=your_api_key
-
-
Run the development server:
# Using npm npm run dev # Or using bun bun dev
-
Open your browser to
http://localhost:5173(or the port specified by Vite).
This is primarily a showcase project, but contributions or suggestions are welcome! If you have ideas for improvement:
- Fork the repository.
- Create a new branch:
git checkout -b feature/your-improvement - Make your changes.
- Commit your changes:
git commit -m 'feat: Add some feature' - Push to the branch:
git push origin feature/your-improvement - Open a Pull Request.
Please ensure any code contributions adhere to the existing style (ESLint/Prettier) and include relevant updates to documentation if necessary.
(Specify your license here, e.g., MIT)
This project is licensed under the MIT License - see the LICENSE file for details (if you have one).