JustPaste
HomeCategoriesAboutDonateContactTerms of UsePrivacy Policy
JustPaste

Free online notepad — write and share instantly

Navigate

  • Home
  • Timeline
  • Categories

Info

  • About
  • Donate
  • Contact

Legal

  • Terms of Use
  • Privacy Policy

© 2026 JustPaste.app. All rights reserved.

Made with ♥ by JustPaste

Untitled Page | JustPaste.app
11 days ago7 views
👨‍💻Programming
/* =====================================================
   TrainGo — Shared Storage Layer (store.js)

   Load this BEFORE script.js / home.js / admin.js:
     <script src="store.js"></script>
     <script src="home.js"></script>

   Everything that touches localStorage goes through here.
   When you swap to a real backend later, only this file
   changes — the pages keep calling the same functions.
   ===================================================== */

const DB = (() => {

  /* ===== KEYS ===== */
  const KEYS = {
    USERS:   "traingo_users",
    TICKETS: "traingo_tickets",
    TRAINS:  "traingo_trains",
    SESSION: "traingo_session",
    COUNTERS:"traingo_counters",
  };

  /* ===== LOW-LEVEL READ / WRITE ===== */
  function read(key, fallback) {
    try {
      const raw = localStorage.getItem(key);
      return raw === null ? fallback : JSON.parse(raw);
    } catch (err) {
      console.warn(`Storage read failed for "${key}":`, err);
      return fallback;
    }
  }

  function write(key, value) {
    try {
      localStorage.setItem(key, JSON.stringify(value));
      return true;
    } catch (err) {
      // QuotaExceededError, or private-mode restrictions
      console.error(`Storage write failed for "${key}":`, err);
      return false;
    }
  }

  function remove(key) {
    try {
      localStorage.removeItem(key);
      return true;
    } catch {
      return false;
    }
  }

  /** True if localStorage is actually usable in this browser/context. */
  function isAvailable() {
    try {
      const probe = "__traingo_probe__";
      localStorage.setItem(probe, "1");
      localStorage.removeItem(probe);
      return true;
    } catch {
      return false;
    }
  }

  /* ===== ID GENERATION =====
     Sequential per-prefix counters, so IDs read cleanly
     (USR-000001) instead of relying on timestamps.     */
  function nextId(prefix, width = 6) {
    const counters = read(KEYS.COUNTERS, {});
    const next = (counters[prefix] || 0) + 1;
    counters[prefix] = next;
    write(KEYS.COUNTERS, counters);
    return `${prefix}-${String(next).padStart(width, "0")}`;
  }

  /* ===== USERS ===== */
  const users = {
    all() {
      return read(KEYS.USERS, []);
    },

    customers() {
      return users.all().filter((u) => u.role !== "admin");
    },

    admins() {
      return users.all().filter((u) => u.role === "admin");
    },

    find(username) {
      if (!username) return null;
      return users.all().find(
        (u) => u.username.toLowerCase() === String(username).toLowerCase()
      ) || null;
    },

    findById(userId) {
      return users.all().find((u) => u.userId === userId) || null;
    },

    exists(username) {
      return users.find(username) !== null;
    },

    /** Adds a user. Returns the created record, or null if the name is taken. */
    add(data) {
      if (users.exists(data.username)) return null;

      const record = {
        userId: data.userId || nextId(data.role === "admin" ? "ADM" : "USR"),
        username: data.username,
        role: data.role || "customer",
        email: data.email || "",
        password: data.password || "",
        mobile: data.mobile || "",
        aadhaar: data.aadhaar || "",
        address: data.address || "",
        createdOn: new Date().toISOString(),
      };

      const list = users.all();
      list.push(record);
      return write(KEYS.USERS, list) ? record : null;
    },

    /** Patches fields on one user. Handles username changes safely. */
    update(username, patch) {
      const list = users.all();
      const idx = list.findIndex((u) => u.username === username);
      if (idx === -1) return null;

      if (patch.username && patch.username !== username && users.exists(patch.username)) {
        return null; // new name collides
      }

      list[idx] = { ...list[idx], ...patch, updatedOn: new Date().toISOString() };
      if (!write(KEYS.USERS, list)) return null;

      // keep tickets and session in sync with a renamed account
      if (patch.username && patch.username !== username) {
        tickets.renameOwner(username, patch.username);
        const s = session.get();
        if (s && s.username === username) {
          session.set(patch.username, list[idx].role);
        }
      }
      return list[idx];
    },

    /** Removes a user and every ticket they own. */
    remove(username) {
      const list = users.all().filter((u) => u.username !== username);
      const ok = write(KEYS.USERS, list);
      tickets.removeByOwner(username);
      return ok;
    },

    /** Returns the user if the password matches, else null. */
    authenticate(username, password) {
      const u = users.find(username);
      if (!u) return null;
      return u.password === password ? u : null;
    },
  };

  /* ===== TICKETS ===== */
  const tickets = {
    all() {
      return read(KEYS.TICKETS, []);
    },

    active() {
      return tickets.all().filter((t) => t.status === "Active");
    },

    cancelled() {
      return tickets.all().filter((t) => t.status === "Cancelled");
    },

    /** Tickets belonging to one user (matches by id or username). */
    byUser(userIdOrName) {
      return tickets.all().filter(
        (t) => t.userId === userIdOrName || t.userName === userIdOrName
      );
    },

    find(ticketId) {
      return tickets.all().find((t) => t.ticketId === ticketId) || null;
    },

    add(data) {
      const record = {
        ticketId: nextId("TKT", 8),
        status: "Active",
        bookedOn: new Date().toISOString(),
        ...data,
      };
      const list = tickets.all();
      list.push(record);
      return write(KEYS.TICKETS, list) ? record : null;
    },

    cancel(ticketId) {
      const list = tickets.all();
      const t = list.find((x) => x.ticketId === ticketId);
      if (!t) return false;
      t.status = "Cancelled";
      t.cancelledOn = new Date().toISOString();
      return write(KEYS.TICKETS, list);
    },

    removeByOwner(username) {
      const list = tickets.all().filter((t) => t.userName !== username);
      return write(KEYS.TICKETS, list);
    },

    renameOwner(oldName, newName) {
      const list = tickets.all();
      list.forEach((t) => {
        if (t.userName === oldName) t.userName = newName;
      });
      return write(KEYS.TICKETS, list);
    },

    /** Seats booked per ticket category (active only). */
    countByClass() {
      const out = {};
      tickets.active().forEach((t) => {
        out[t.category] = (out[t.category] || 0) + t.count;
      });
      return out;
    },

    /** { "Q1 2026": { bookings, seats } } — active only. */
    countByQuarter() {
      const out = {};
      tickets.active().forEach((t) => {
        const d = new Date(t.bookedOn);
        const q = `Q${Math.floor(d.getMonth() / 3) + 1} ${d.getFullYear()}`;
        if (!out[q]) out[q] = { bookings: 0, seats: 0 };
        out[q].bookings += 1;
        out[q].seats += t.count;
      });
      return out;
    },

    /** Seats already sold on one train for one date. */
    seatsTaken(trainId, dateStr) {
      return tickets.active()
        .filter((t) => t.trainId === trainId && String(t.boardingDT).startsWith(dateStr))
        .reduce((sum, t) => sum + t.count, 0);
    },
  };

  /* ===== TRAINS ===== */
  const trains = {
    all() {
      return read(KEYS.TRAINS, []);
    },

    find(id) {
      return trains.all().find((t) => t.id === id) || null;
    },

    /** Trains running a given origin -> destination pair. */
    onRoute(from, to) {
      return trains.all().filter((t) => t.from === from && t.to === to);
    },

    add(data) {
      const record = {
        id: nextId("TR", 5),
        registeredOn: new Date().toISOString(),
        ...data,
      };
      const list = trains.all();
      list.push(record);
      return write(KEYS.TRAINS, list) ? record : null;
    },

    remove(id) {
      return write(KEYS.TRAINS, trains.all().filter((t) => t.id !== id));
    },
  };

  /* ===== SESSION ===== */
  const session = {
    get() {
      return read(KEYS.SESSION, null);
    },

    set(username, role) {
      return write(KEYS.SESSION, {
        username,
        role: role || "customer",
        loginAt: new Date().toISOString(),
      });
    },

    clear() {
      return remove(KEYS.SESSION);
    },

    /** The full user record for whoever is logged in. */
    user() {
      const s = session.get();
      return s ? users.find(s.username) : null;
    },

    isAdmin() {
      const s = session.get();
      return !!s && s.role === "admin";
    },

    isLoggedIn() {
      return session.get() !== null;
    },

    /**
     * Page guard. Call at the top of a portal page.
     *   DB.session.require("admin", "index.html")
     * Redirects and returns false if access is not allowed.
     */
    require(role, redirectTo = "index.html") {
      const s = session.get();
      const ok = s && (!role || s.role === role);
      if (!ok) {
        window.location.href = redirectTo;
        return false;
      }
      return true;
    },
  };

  /* ===== SEED =====
     Creates the default admin on first run so the admin
     portal is reachable before anyone registers.        */
  function seed() {
    if (users.admins().length === 0) {
      users.add({
        username: "adminuser",
        role: "admin",
        email: "[email protected]",
        password: "Admin@123",
        mobile: "9999999999",
      });
    }
  }

  /* ===== MAINTENANCE ===== */
  function exportAll() {
    return {
      users: users.all(),
      tickets: tickets.all(),
      trains: trains.all(),
      session: session.get(),
    };
  }

  function resetAll() {
    Object.values(KEYS).forEach(remove);
    seed();
  }

  /* ===== PUBLIC API ===== */
  return {
    KEYS, users, tickets, trains, session,
    nextId, seed, exportAll, resetAll, isAvailable,
    read, write, remove,
  };
})();

/* Warn once if storage is unusable (private mode, blocked cookies). */
if (!DB.isAvailable()) {
  console.warn("localStorage is unavailable — data will not persist.");
} else {
  DB.seed();
}

All 3 HTML files

Add one line above the existing script tag:

html
  <script src="store.js"></script>

So index.html ends with store.js then script.js; home.html with store.js then home.js; admin.html with store.js then admin.js.

script.js

Delete your USERS_KEY, getUsers, saveUser, findUser, and the seedAdmin block — store.js replaces all of them.

Add at the very top, after the DOM references:

js
// already signed in? skip the login form
(() => {
  const s = DB.session.get();
  if (s) window.location.href = s.role === "admin" ? "admin.html" : "home.html";
})();

Login handler — replace everything after if (!isValid) return;:

js
  const username = document.getElementById("login-username").value.trim();
  const password = document.getElementById("login-password").value;

  if (!DB.users.find(username)) {
    setError("login-username-err", "No account found with this username.");
    return;
  }

  const user = DB.users.authenticate(username, password);
  if (!user) {
    setError("login-password-err", "Incorrect password.");
    return;
  }

  DB.session.set(user.username, user.role);

  const isAdmin = user.role === "admin";
  const btn = loginForm.querySelector(".btn");
  const btnText = btn.querySelector(".btn__text") || btn;
  btnText.textContent = "Boarding…";
  btn.disabled = true;

  setTimeout(() => {
    showToast(isAdmin ? "🛡️  Admin login successful." : "✓  Welcome aboard!");
    setTimeout(() => {
      window.location.href = isAdmin ? "admin.html" : "home.html";
    }, 700);
  }, 1000);

Register handler — replace the findUser check and saveUser call:

js
  const username = document.getElementById("reg-username").value.trim();

  if (DB.users.exists(username)) {
    setError("reg-username-err", "Username already taken.");
    return;
  }

  const created = DB.users.add({
    username,
    role: "customer",
    email: document.getElementById("reg-email").value.trim(),
    password: document.getElementById("reg-password").value,
    mobile: document.getElementById("reg-mobile").value.trim(),
    aadhaar: document.getElementById("reg-aadhaar").value.trim(),
  });

  if (!created) {
    showToast("Registration failed — storage error.", true);
    return;
  }
home.js

Delete everything from const USERS_KEY through function currentUser(), plus readJSON, writeJSON, getUsers, getTickets, saveTickets, getSession. Keep the STATIONS array; delete the hardcoded TRAINS array.

Add at the top, before anything else:

js
// customer portal guard
(() => {
  const s = DB.session.get();
  if (!s) { window.location.href = "index.html"; return; }
  if (s.role === "admin") { window.location.href = "admin.html"; }
})();

initSession — replace the whole function:

js
function initSession() {
  const user = DB.session.user();
  if (!user) return null;
  document.getElementById("current-user").textContent = user.username;
  document.getElementById("bk-id").value = user.userId;
  const nameField = document.getElementById("bk-name");
  if (nameField && !nameField.value) nameField.value = user.username;
  return user;
}

Logout:

js
document.getElementById("logout-btn").addEventListener("click", () => {
  DB.session.clear();
  window.location.href = "index.html";
});

refreshTrains — replace the TRAINS.forEach tail so customers see admin-registered trains:

js
  const matching = DB.trains.onRoute(from, to);

  if (matching.length === 0) {
    trainSelect.innerHTML = `<option value="">No trains registered on this route</option>`;
    return;
  }

  trainSelect.innerHTML = `<option value="">Select a train</option>`;
  matching.forEach((t) => {
    trainSelect.insertAdjacentHTML("beforeend",
      `<option value="${t.id}">${t.id} — ${t.name} (dep ${t.depart})</option>`);
  });

Booking submit — replace the train lookup and the ticket build:

js
  const train = DB.trains.find(document.getElementById("bk-train").value);
  if (!train) {
    setError("bk-train-err", "That train is no longer available.");
    refreshTrains();
    return;
  }

  const date = document.getElementById("bk-date").value;
  const count = Number(document.getElementById("bk-count").value);

  const free = train.seats - DB.tickets.seatsTaken(train.id, date);
  if (count > free) {
    setError("bk-count-err", free > 0 ? `Only ${free} seat(s) left.` : "Train is fully booked.");
    return;
  }

  const ticket = DB.tickets.add({
    trainId: train.id,
    trainName: train.name,
    userId: document.getElementById("bk-id").value,
    userName: document.getElementById("bk-name").value.trim(),
    mobile: document.getElementById("bk-mobile").value.trim(),
    age: document.getElementById("bk-age").value.trim(),
    boarding: document.getElementById("bk-from").value,
    destination: document.getElementById("bk-to").value,
    category: document.getElementById("bk-class").value,
    boardingDT: `${date} ${train.depart}`,
    arrivalDT: computeArrival(date, train.depart, train.duration),
    count,
  });

  if (!ticket) { showToast("Booking failed — storage error.", true); return; }

renderTickets — replace the getTickets() line so users only see their own:

js
  const user = DB.session.user();
  const tickets = DB.tickets.byUser(user.userId)
    .concat(DB.tickets.byUser(user.username))
    .filter((t, i, a) => a.findIndex((x) => x.ticketId === t.ticketId) === i);

Cancel handler:

js
  const ok = DB.tickets.cancel(pendingCancelId);
  closeCancelModal();
  renderTickets();
  renderAggregates();
  showToast(ok ? "Ticket cancelled." : "Cancellation failed.", !ok);

renderAggregates — replace the first line (const tickets = getTickets()) with the same mine lookup as renderTickets, then const active = mine.filter(t => t.status === "Active").

loadUpdateForm — swap currentUser() for DB.session.user().

Update submit — replace the users/idx/writeJSON block:

js
  const user = DB.session.user();
  const patch = {
    email: document.getElementById("up-email").value.trim(),
    mobile: document.getElementById("up-mobile").value.trim(),
    address: document.getElementById("up-address").value.trim(),
  };

  if (pwToggle.checked) {
    if (user.password !== document.getElementById("up-current").value) {
      setError("up-current-err", "Current password is incorrect.");
      return;
    }
    patch.password = document.getElementById("up-new").value;
  }

  if (!DB.users.update(user.username, patch)) {
    showToast("Update failed — storage error.", true);
    return;
  }
admin.js

Delete USERS_KEY through currentAdmin() — all the storage helpers.

guardAdmin — replace:

js
function guardAdmin() {
  const s = DB.session.get();
  if (!s) { window.location.href = "index.html"; return false; }
  if (s.role !== "admin") { window.location.href = "home.html"; return false; }
  document.getElementById("current-user").textContent = s.username;
  return true;
}

Logout:

js
document.getElementById("logout-btn").addEventListener("click", () => {
  DB.session.clear();
  window.location.href = "index.html";
});

In renderDashboard, swap four lines:

js
  const customers = DB.users.customers();
  const active = DB.tickets.active();
  const byClass = DB.tickets.countByClass();
  const byQ = DB.tickets.countByQuarter();

and DB.trains.all().length for the trains stat. Delete the manual byClass/byQ accumulation loops — the store computes them.

deleteCustomer — delete the function; call DB.users.remove(uname) directly in the confirm callback (it cascades to tickets automatically).

Train add — replace the manual object and push:

js
  const train = DB.trains.add({
    name: document.getElementById("tr-name").value.trim(),
    seats: Number(document.getElementById("tr-seats").value),
    from: document.getElementById("tr-from").value,
    to: document.getElementById("tr-to").value,
    depart: document.getElementById("tr-depart").value,
    duration: Number(document.getElementById("tr-duration").value),
    owner: document.getElementById("tr-owner").value,
  });
  if (!train) { showToast("Registration failed.", true); return; }

Train remove: DB.trains.remove(id)

renderTrains: const trains = DB.trains.all();

loadProfile: const admin = DB.session.user();

Profile submit — replace the users/idx block:

js
  const admin = DB.session.user();
  const newUsername = document.getElementById("pf-username").value.trim();

  const patch = {
    username: newUsername,
    email: document.getElementById("pf-email").value.trim(),
    mobile: document.getElementById("pf-mobile").value.trim(),
  };

  if (pfToggle.checked) {
    if (admin.password !== document.getElementById("pf-current").value) {
      setError("pf-current-err", "Current password is incorrect.");
      return;
    }
    patch.password = document.getElementById("pf-new").value;
  }

  const updated = DB.users.update(admin.username, patch);
  if (!updated) { setError("pf-username-err", "Username already taken."); return; }

  document.getElementById("current-user").textContent = updated.username;

Delete-self:

js
      DB.users.remove(s.username);
      DB.session.clear();
      window.location.href = "index.html";
← Back to timeline