A React application that fetches and displays a list of tours from an external API. It demonstrates core React concepts—state, effects, list rendering, and lifting state—in a single-page app built with Vite and plain CSS. You can view tour cards, expand or collapse descriptions, remove tours from the list, and refresh to load the list again. Ideal for learning React fundamentals and for reuse as a card-list template in other projects.
- Live Demo: https://cardview-display.vercel.app/
- Features & Functionality
- Tech Stack & Structure
- Project Structure
- Getting Started
- Environment Variables (.env)
- How It Works – Walkthrough
- API & Backend
- Components Deep Dive
- Reusing Components in Other Projects
- Scripts & Commands
- Keywords
- Conclusion
- License
- Happy Coding!
- Fetch tours on load – On mount, the app fetches tour data from a public API and shows a loading spinner until data is ready.
- Card list view – Each tour is shown as a card with image, title, price, and a short description.
- Read more / Show less – Long descriptions are truncated; a button toggles between full text and truncated view.
- Remove tour – Each card has a “not interested” button that removes that tour from the list (client-side only).
- Refresh / Re-fetch – When no tours are left, a “refresh” button appears to fetch the list again from the API.
- Responsive layout – CSS Grid adapts from one column on small screens to two or three columns on larger ones.
- Loading & empty states – Dedicated UI for loading and for “no tours left” with refresh action.
| Layer | Technology |
|---|---|
| UI | React 18 (functional components, hooks) |
| Build | Vite 4 |
| Language | JavaScript (ES modules) |
| Styling | Plain CSS (custom properties, no framework) |
| Data | Public REST API (course-api.com) |
| Tooling | ESLint 9 (flat config), React + React Hooks plugins |
There is no separate backend; the app is a front-end that consumes an external API. Routing is not used—everything lives on a single page.
02-tours/
├── index.html # Entry HTML, root div, script to main.jsx
├── package.json # Dependencies and npm scripts
├── vite.config.js # Vite config (React plugin)
├── eslint.config.js # ESLint 9 flat config
├── .gitignore
├── public/
│ └── vite.svg # Favicon / default asset
└── src/
├── main.jsx # React root, mounts <App /> into #root
├── index.css # Global + component-specific styles
├── App.jsx # Top-level state, fetch logic, conditional views
├── Tours.jsx # List container: title + grid of Tour cards
├── Tour.jsx # Single tour card (image, price, info, read more, remove)
└── Loading.jsx # Loading spinner UI- Entry:
index.html→src/main.jsx→App.jsx. - Data flow:
Appholdstoursandloading; passestoursandremoveTourtoTours;Toursmaps overtoursand rendersTourfor each item.Tourmanages its own “read more” state locally.
- Node.js (v16 or higher recommended)
- npm (or yarn/pnpm)
-
Clone the repository
git clone <your-repo-url> cd 02-tours
-
Install dependencies
npm install
-
Start the development server
npm run dev
Vite will start a local server (e.g.
http://localhost:5173). Open that URL in your browser to see the app. -
Build for production
npm run build
Output goes to the
dist/folder. To preview the production build locally:npm run preview
The app does not require environment variables to run. The API URL is hardcoded in src/App.jsx:
const url = "https://www.course-api.com/react-tours-project";If you want to make the API URL configurable (e.g. for different environments or APIs), you can use Vite’s env support.
-
Create a
.envfile in the project root (same level aspackage.json). It is already listed in.gitignore, so it will not be committed. -
Define variables with the
VITE_prefix (required for Vite to expose them to the client):VITE_API_URL=https://www.course-api.com/react-tours-project
-
Use the variable in code. In
src/App.jsx:const url = import.meta.env.VITE_API_URL || "https://www.course-api.com/react-tours-project";
-
Restart the dev server after changing
.env.
- Required: None. The app works without any
.envfile. - Optional:
VITE_API_URL– base or full URL for the tours API. If you add it, use the pattern above so a fallback keeps the default API working.
-
Initial load
Appmounts withloading: trueandtours: []. AuseEffectruns once, setsloadingto true, and calls the API. When the request completes, it updatestoursand setsloadingto false. A cleanup flag prevents state updates if the component unmounts before the request finishes. -
Loading state
Whileloadingis true,Apprenders only theLoadingcomponent (spinner). -
Empty state
Ifloadingis false andtours.length === 0,Appshows “no tours left” and a “refresh” button that callsfetchTours()to re-fetch the list. -
Tours list
Whentourshas items,Apprenders<Tours tours={tours} removeTour={removeTour} />.Toursrenders a section title and a grid ofTourcomponents, passing each tour’s fields andremoveTour. -
Single tour card
Tourreceivesid,image,info,name,price, andremoveTour. It keeps local statereadMore. It shows the image, price badge, name, and either the first 200 characters ofinfoplus “read more” or the fullinfoplus “show less.” “Not interested” callsremoveTour(id). -
Removing a tour
removeTour(id)inAppfilters out the tour with thatidand callssetTourswith the new array. React re-renders; that card disappears. -
Refresh
When the list is empty, the user clicks “refresh,” which runsfetchTours()again (same API URL), and the list is repopulated.
- Backend: None. This is a front-end-only project.
- Data source: Public REST API.
| Method | URL | Description |
|---|---|---|
| GET | https://www.course-api.com/react-tours-project |
Returns a JSON array of tour objects |
No authentication or API key is required.
Each element in the array is an object like:
{
"id": "rec6d6TNDqE4ge4OH",
"name": "Best of Paris in 7 Days Tour",
"info": "Paris is synonymous with the finest things...",
"image": "https://example.com/image.jpg",
"price": "1,995"
}- id – Unique string (used as React
keyand for removal). - name – Tour title.
- info – Long description (supports “read more” truncation).
- image – Image URL.
- price – String (e.g.
"1,995"); displayed as-is with a$prefix in the UI.
- Role: Root component. Owns
toursandloading; performs initial fetch and refresh; decides whether to show loading, empty state, or the tours list. - State:
loading(boolean),tours(array of tour objects). - Key logic:
removeTour(id),fetchTours(), and auseEffectfor the initial fetch with cancellation.
Snippet – state and remove:
const [loading, setLoading] = useState(true);
const [tours, setTours] = useState([]);
const removeTour = (id) => {
const newTours = tours.filter((tour) => tour.id !== id);
setTours(newTours);
};- Role: Presentational list. Renders a heading and a grid of
Tourcards. - Props:
tours(array),removeTour(function). - Reusable: Use it anywhere you have an array of items with
id,image,info,name,priceand a remove handler.
Snippet – mapping over tours:
const Tours = ({ tours, removeTour }) => {
return (
<section>
<div className="title">
<h2>our tours</h2>
<div className="title-underline"></div>
</div>
<div className="tours">
{tours.map((tour) => (
<Tour key={tour.id} {...tour} removeTour={removeTour} />
))}
</div>
</section>
);
};- Role: Single tour card. Displays image, price, name, expandable description, and a remove button.
- Props:
id,image,info,name,price,removeTour. - Local state:
readMore(boolean) for toggling full vs truncated description.
Snippet – truncation and read more:
const [readMore, setReadMore] = useState(false);
// ...
<p>
{readMore ? info : `${info.substring(0, 200)}...`}
<button className="info-btn" onClick={() => setReadMore(!readMore)}>
{readMore ? "show less" : " read more"}
</button>
</p>;- Role: Loading indicator (spinner). No props.
- Reusable: Drop-in for any loading state; styling is in
index.cssunder.loading.
Snippet:
const Loading = () => {
return <div className="loading"></div>;
};-
Copy components
CopyTours.jsx,Tour.jsx, andLoading.jsxinto your project. Adjust imports (e.g.import Tour from './Tour') to match your folder structure. -
Data shape
Ensure each item has at least:id,name,info,image,price. If your API uses different keys, map them before passing toToursor change prop names inTour. -
Styling
Copy the relevant parts ofindex.css::rootvariables,.tours,.single-tour,.tour-price,.tour-info,.info-btn,.delete-btn,.loading, and any layout/button styles you use. You can scope them under a class (e.g..tours-app) to avoid clashes. -
Remove behavior
In your parent (e.g.App), keep an array in state and pass aremoveTour(id)that filters byidand updates state. Same pattern as in this project. -
Different data source
Replace the fetch URL (or useVITE_API_URL) and keep the same response shape, or add a small adapter that maps your API response to{ id, name, info, image, price }.
| Command | Description |
|---|---|
npm run dev |
Start Vite dev server (e.g. http://localhost:5173) |
npm run build |
Production build → dist/ |
npm run preview |
Serve the production build locally |
npm run lint |
Run ESLint on the project (no warnings allowed) |
React, Vite, JavaScript, tours, card display, list UI, fetch API, useState, useEffect, component composition, lifting state, read more, loading state, empty state, responsive CSS, CSS variables, ESLint, front-end, educational project, open source.
This project is a small, focused example of a React SPA: fetching data, managing loading and list state, and composing presentational components (Tours, Tour, Loading). It uses no router or global state library, so it’s easy to read and adapt. You can extend it by adding filters, sorting, a real backend, or by reusing its components in other apps as described above.
This project is licensed under the MIT License. Feel free to use, modify, and distribute the code as per the terms of the license.
This is an open-source project - feel free to use, enhance, and extend this project further!
If you have any questions or want to share your work, reach out via GitHub or my portfolio at https://www.arnobmahmud.com.
Enjoy building and learning! 🚀
Thank you! 😊
