Skip to content

TetraMeister/Excursions-JS-App

Repository files navigation

Client – excursion browsing and order

Admin panel – managing excursions

Excursions – API & FETCH

A two-part excursion booking app built around a small REST API. It is split into a client side, where users browse excursions, add them to their order and submit it, and an admin panel, where excursions can be added, edited and removed. All communication with the database goes through fetch(), with JSON Server acting as the backend.

Main features:

  • Browse excursions and build an order (cart) with live price calculation
  • Form validation for both the order and the ticket-count inputs
  • Full admin panel with create / edit / remove operations on excursions
  • A single API layer (Provider) shared between the client and the admin

 

💡 Technologies

HTML5 CSS3 JavaScript Webpack Babel JSON

 

💿 Installation

The project uses node and npm. Having them installed, type into the terminal:

npm install

Then start the Webpack dev server:

npm start

The app needs the API running in parallel. Open a second terminal and start JSON Server:

npm run server

From now on:

  • the client is available at http://localhost:8080/index.html
  • the admin panel is available at http://localhost:8080/admin.html
  • the API exposes two resources: http://localhost:3000/excursions and http://localhost:3000/orders

 

🤔 Solutions provided in the project

  • One API layer, reused on both sides – the whole conversation with the backend lives in a single Provider class with a complete CRUD interface. The private _fetch() method is the only place that touches fetch(), normalises the JSON response and rejects on a non-2xx status:
class Provider {
  constructor(url) {
    this.url = url;
  }

  loadData() {
    return this._fetch(this.url);
  }

  addData(data) {
    const options = {
      method: "POST",
      body: JSON.stringify(data),
      headers: { 'Content-Type': 'application/json' },
    };

    return this._fetch(this.url, options)
  }

  removeData(id) {
    const options = { method: "DELETE" };

    return this._fetch(`${this.url}/${id}`, options);
  }

  _fetch(url, options) {
    return fetch(url, options)
      .then((resp) => {
        if (resp.ok) {
          return resp.json();
        }
        return Promise.reject(resp);
      })
  }
}

 

  • Resource-specific providers through inheritanceProviderExcursions and ProviderOrders extend Provider and only declare their endpoint. The base URL is resolved from window.location.hostname, so the same code works locally and inside a remote environment like Codespaces without changing a line:
import Provider from "./Provider";

class ProviderExcursions extends Provider {
  constructor() {
    super(`http://${window.location.hostname}:3000/excursions`);
  }
}

export default ProviderExcursions

 

  • Inline editing in the admin panel – the same button drives a two-step edit → save flow. The first click turns the relevant fields into contentEditable and relabels the button; the second click reads the edited content and pushes it to the API:
if (submitDatasetID === 'edytuj' || submitDatasetID === 'zapisz') {
  if (this._isEditable(editableList)) {
    this._editExcursions(parentEl, formEl, id);
  } else {
    submitterEl.dataset.id = 'zapisz';
    submitterEl.value = 'zapisz';
    editableList.forEach((el) => {
      el.contentEditable = true;
    });
  }
}

 

  • Prototype-based rendering – instead of building HTML strings in JS, each item is cloned from a hidden --prototype node in the markup, filled with data and appended. The HTML structure stays in the HTML file, where it belongs:
const newLiEl = this.prototypeEl.cloneNode(true);

newLiEl.dataset.id = dataObj.id;
newLiEl.querySelector('.excursions__title').textContent = name;
newLiEl.querySelector('.excursions__description').textContent = description;
newLiEl.querySelector('.excursions__field-priceAdult').textContent = priceAdult;
newLiEl.querySelector('.excursions__field-priceChild').textContent = priceChild;

newLiEl.classList.remove('excursions__item--prototype');
this.panelEl.appendChild(newLiEl);

 

A few smaller decisions worth pointing out:

Issue Solution
Keep the client and admin bundles separate Webpack entry chunks: client and admin, each wired to its own HTML via HtmlWebpackPlugin
fetch() support in older browsers whatwg-fetch polyfill added at the top of every entry point
Recalculate the cart total after each change data.forEach((order) => { total += order.total; })

 

💭 Conclusions for future projects

The provider layer turned out to be the strongest part of this project — because all the API logic sits behind one class with a stable interface, the UI code never talks to fetch() directly. That separation makes the next two improvements feel natural rather than disruptive:

Authentication for the admin panel:

Right now the admin panel is open. A real deployment would put the create / edit / remove operations behind a login. Since every request already passes through Provider._fetch(), adding an Authorization header (or a token-refresh step) would be a change in one place rather than across the whole codebase.

Easy migration to React components:

The prototype-clone pattern is essentially a manual version of what a framework does for free. Each excursion card and each summary row is already a self-contained, data-driven unit — moving this to React would mean turning every _create...El method into a component and swapping the manual DOM updates for state. The provider classes could stay almost untouched and be reused inside hooks.

Responsive layout (RWD):

The current layout is desktop-first and built on fixed proportions — the panels split 70% / 30%, the cards sit at width: calc(50% - 20px), and there are no media queries. On narrow screens this breaks down quickly. The next iteration would add breakpoints so the side panel stacks under the excursion list and the cards collapse to a single column, ideally moving the grid sizing to flex/grid with minmax() instead of hard-coded percentages.

 

🙋‍♂️ Feel free to contact me

Write sth nice ;) Find me on LinkedIn: Łukasz Skrzypczyński.

 

👏 Special thanks

Thanks to my Mentor – Mateusz Bogolubow (devmentor.pl) – for providing me with this task and for the code review.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors