gentelella
Version:
Gentelella v4 — free admin template. 60 pages, 20 chart variants, fully interactive inbox & kanban, live theme generator, component playground, PWA-ready. Vite 8, vanilla JS, no Bootstrap, no jQuery.
139 lines (132 loc) • 4.76 kB
JavaScript
// Gentelella v4 — data-adapter pattern
//
// Every interactive page in the template has hardcoded seed data. Replacing
// that with a real API call is the most common first task for someone using
// this template as a starter. This module gives that task a name and a shape.
//
// Usage:
// const adapter = useApiMode()
// ? httpAdapter('/api/messages')
// : seedAdapter(SEED);
//
// // both have the same surface:
// const items = await adapter.list({ folder: 'inbox' });
// await adapter.update(id, { unread: 0 });
// await adapter.create({ ... });
// await adapter.remove(id);
//
// The flag can be flipped via `?api=1` URL param (handy for live demos that
// switch between "static preview" and "real backend") OR by setting
// `window.__GENTELELLA_API__ = true` before module load.
/**
* Returns true when the page is in API mode. Two ways to opt in:
* ?api=1 in the URL (one-shot demo trigger)
* window.__GENTELELLA_API__ = true (set in a build script for prod)
*/
export function useApiMode() {
if (typeof window === 'undefined') {return false;}
if (window.__GENTELELLA_API__) {return true;}
return new URLSearchParams(window.location.search).has('api');
}
/**
* Adapter that returns hardcoded data — what every demo page uses by default.
* Mirrors the shape of httpAdapter so swapping is one line.
*
* @param {Array} seed
* @param {(item: object, query: object) => boolean} [filter]
* Optional filter to apply when `list({...})` is called.
*/
export function seedAdapter(seed, filter) {
let store = seed.slice();
let nextId = Math.max(0, ...store.map((x) => Number(x.id) || 0)) + 1;
return {
async list(query = {}) {
return filter ? store.filter((x) => filter(x, query)) : store.slice();
},
async get(id) {
return store.find((x) => String(x.id) === String(id)) || null;
},
async create(data) {
const item = { id: nextId++, ...data };
store.unshift(item);
return item;
},
async update(id, patch) {
const i = store.findIndex((x) => String(x.id) === String(id));
if (i < 0) {return null;}
store[i] = { ...store[i], ...patch };
return store[i];
},
async remove(id) {
const before = store.length;
store = store.filter((x) => String(x.id) !== String(id));
return store.length < before;
},
/** Reset to the original seed — useful for tests / demos. */
reset() { store = seed.slice(); }
};
}
/**
* Adapter that talks to a JSON REST endpoint. Conventions:
* GET `${baseUrl}` (with `?key=value` for query) → { items, ... } or items[]
* GET `${baseUrl}/:id` → item
* POST `${baseUrl}` body: data → item
* PATCH `${baseUrl}/:id` body: patch → { ok: true } or item
* DELETE `${baseUrl}/:id` → { ok: true }
*
* The list endpoint may return either a bare array or `{ <key>: [...] }`;
* pass `listKey` to extract the array from the wrapped form.
*
* @param {string} baseUrl
* @param {{ listKey?: string, fetch?: typeof fetch }} [opts]
*/
export function httpAdapter(baseUrl, opts = {}) {
const f = opts.fetch || ((...a) => globalThis.fetch(...a));
const listKey = opts.listKey;
const json = async (res) => {
if (!res.ok) {
const body = await res.text().catch(() => '');
throw new HttpError(res.status, body || res.statusText);
}
return res.json();
};
return {
async list(query = {}) {
const qs = new URLSearchParams(query).toString();
const url = qs ? `${baseUrl}?${qs}` : baseUrl;
const data = await json(await f(url));
return listKey ? (data[listKey] ?? []) : data;
},
async get(id) {
return json(await f(`${baseUrl}/${encodeURIComponent(id)}`));
},
async create(data) {
return json(await f(baseUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}));
},
async update(id, patch) {
return json(await f(`${baseUrl}/${encodeURIComponent(id)}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch)
}));
},
async remove(id) {
return json(await f(`${baseUrl}/${encodeURIComponent(id)}`, { method: 'DELETE' }));
}
};
}
/**
* HTTP error with the response status code attached. Use in catch blocks to
* differentiate "user typo" (400, 404) from "server down" (5xx, network).
*/
export class HttpError extends Error {
constructor(status, message) {
super(`HTTP ${status}: ${message}`);
this.name = 'HttpError';
this.status = status;
}
}