Skip to content

Commit

Permalink
Feature/145 import export cart (#195)
Browse files Browse the repository at this point in the history
add ability to import and export cart

this is done by providing buttons attached to the cart item on any page. The export will generate a JSON file and the import will handle this JSON file.

additionally -- the reset button from the 1-click buy feature has been moved over to this component. If the cart has been modified -- exporting is not allow and the reset button is show. Otherwise, the reset button is hidden and the export button is visible

additionally:
* move download file function to utilits
* move dateString function to utilities
* update date format for cart and downloads to be yy-mm-dd
* move re-usable static method to component and re-name component to be for button types
* update download_helper and tests for previous changes
* fix potential failure mode of Player and add test
* update Player 1-click feature with a small fix and add test
  • Loading branch information
sabjorn committed Nov 20, 2024
1 parent 19375a1 commit 480c525
Show file tree
Hide file tree
Showing 10 changed files with 645 additions and 74 deletions.
11 changes: 11 additions & 0 deletions src/components/inputButtonPair.js → src/components/buttons.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,14 @@ export function createInputButtonPair(options = {}) {

return container;
}

export function createButton(options = {}) {
const { className, innerText, buttonClicked } = options;

const button = document.createElement("a");
button.className = className;
button.innerText = innerText;
button.addEventListener("click", buttonClicked);

return button;
}
11 changes: 0 additions & 11 deletions src/components/shoppingCart.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,3 @@ export function createShoppingCartItem(options = {}) {

return itemContainer;
}

export function createShoppingCartResetButton(options = {}) {
const { className, innerText, buttonClicked } = options;

const cartRefreshButton = document.createElement("a");
cartRefreshButton.className = className;
cartRefreshButton.innerText = innerText;
cartRefreshButton.addEventListener("click", buttonClicked);

return cartRefreshButton;
}
40 changes: 7 additions & 33 deletions src/download_helper.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import Logger from "./logger";

import { downloadFile, dateString } from "./utilities";

const preamble = `#!/usr/bin/env bash
# Generated by Bandcamp Enhancement Suite (https://github.com/sabjorn/BandcampEnhancementSuite)
#
Expand All @@ -17,6 +19,10 @@ export default class DownloadHelper {
this.mutationCallback = DownloadHelper.callback.bind(this); // necessary for class callback
this.observer = new MutationObserver(this.mutationCallback);

// re-import
DownloadHelper.dateString = dateString;
DownloadHelper.downloadFile = downloadFile;

this.linksReady;
this.button;
}
Expand Down Expand Up @@ -65,7 +71,7 @@ export default class DownloadHelper {
const preamble = DownloadHelper.getDownloadPreamble();
const downloadDocument = preamble + downloadList;

DownloadHelper.download(`bandcamp_${date}.txt`, downloadDocument);
DownloadHelper.downloadFile(`bandcamp_${date}.txt`, downloadDocument);
});
}

Expand All @@ -91,38 +97,6 @@ export default class DownloadHelper {
return filelist;
}

static download(filename, text) {
var element = document.createElement("a");

element.setAttribute(
"href",
"data:text/plain;charset=utf-8," + encodeURIComponent(text)
);
element.setAttribute("download", filename);

element.style.display = "none";
document.body.appendChild(element);

element.click();

document.body.removeChild(element);
}

static dateString() {
const currentdate = new Date();
const ye = new Intl.DateTimeFormat("en", { year: "2-digit" }).format(
currentdate
);
const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(
currentdate
);
const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(
currentdate
);

return `${da}-${mo}-${ye}`;
}

static callback() {
let allDownloadLinks = document.querySelectorAll(
".download-title .item-button"
Expand Down
9 changes: 9 additions & 0 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import DownloadHelper from "./download_helper.js";
import Player from "./player.js";
import AudioFeatures from "./audioFeatures.js";
import Checkout from "./checkout.js";
import Cart from "./pages/cart";

const main = () => {
const log = new Logger();
Expand Down Expand Up @@ -45,6 +46,14 @@ const main = () => {
let checkout = new Checkout(config_port);
checkout.init();
}

const { has_cart } = JSON.parse(
document.querySelector("[data-blob]").getAttribute("data-blob")
);
if (has_cart) {
const cart = new Cart();
cart.init();
}
};

main();
144 changes: 144 additions & 0 deletions src/pages/cart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import Logger from "../logger";

import { createButton } from "../components/buttons.js";
import {
downloadFile,
dateString,
loadJsonFile,
addAlbumToCart
} from "../utilities";
import { createShoppingCartItem } from "../components/shoppingCart.js";

export default class Cart {
constructor() {
this.log = new Logger();

// re-import
this.createButton = createButton;
this.loadJsonFile = loadJsonFile;
this.addAlbumToCart = addAlbumToCart;
this.createShoppingCartItem = createShoppingCartItem;
this.downloadFile = downloadFile;
this.reloadWindow = () => location.reload();
}

init() {
this.log.info("cart init");

const importCartButton = this.createButton({
className: "buttonLink",
innerText: "import",
buttonClicked: async () => {
try {
const { tracks_export } = await this.loadJsonFile();
const promises = tracks_export.map(track =>
this.addAlbumToCart(
track.item_id,
track.unit_price,
track.item_type
).then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const cartItem = this.createShoppingCartItem({
itemId: track.item_id,
itemName: track.item_title,
itemPrice: track.unit_price,
itemCurrency: track.currency
});

document.querySelector("#item_list").append(cartItem);
})
);

await Promise.all(promises).then(results => {
if (!results || results.length < 1) {
return;
}
this.reloadWindow();
});
} catch (error) {
this.log.error("Error loading JSON:", error);
}
}
});
document.querySelector("#sidecartReveal").prepend(importCartButton);

const exportCartButton = this.createButton({
className: "buttonLink",
innerText: "export",
buttonClicked: () => {
const { items } = JSON.parse(
document.querySelector("[data-cart]").getAttribute("data-cart")
);
if (items.length < 1) {
this.log.error("error trying to export cart with length of 0");
return;
}

const cart_id = items[0].cart_id;
const date = dateString();
const tracks_export = items
.filter(item => item.item_type === "a" || item.item_type === "t")
.map(
({
band_name,
item_id,
item_title,
unit_price,
url,
currency,
item_type
}) => ({
band_name,
item_id,
item_title,
unit_price,
url,
currency,
item_type
})
);
if (tracks_export.length < 1) return;

const filename = `${date}_${cart_id}_bes_cart_export.json`;
const data = JSON.stringify({ date, cart_id, tracks_export }, null, 2);
this.downloadFile(filename, data);
}
});
document.querySelector("#sidecartReveal").append(exportCartButton);

const cartRefreshButton = this.createButton({
className: "buttonLink",
innerText: "⟳",
buttonClicked: () => this.reloadWindow()
});
cartRefreshButton.style.display = "none";
document.querySelector("#sidecartReveal").append(cartRefreshButton);

const observer = new MutationObserver(() => {
const item_list = document.querySelectorAll("#item_list .item");
const cartDataElement = document.querySelector("[data-cart]");

if (!cartDataElement) {
return;
}
const actual_cart = JSON.parse(cartDataElement.getAttribute("data-cart"))
.items;

cartRefreshButton.style.display =
item_list.length === actual_cart.length ? "none" : "block";

exportCartButton.style.display =
item_list.length === actual_cart.length ? "block" : "none";
});

const itemList = document.getElementById("item_list");
if (itemList) {
observer.observe(itemList, {
childList: true
});
}
}
}
17 changes: 4 additions & 13 deletions src/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@ import {
getTralbumDetails,
addAlbumToCart
} from "./utilities.js";
import { createInputButtonPair } from "./components/inputButtonPair.js";
import {
createShoppingCartItem,
createShoppingCartResetButton
} from "./components/shoppingCart.js";
import { createInputButtonPair } from "./components/buttons.js";
import { createShoppingCartItem } from "./components/shoppingCart.js";
import emptyPlaylistTable from "../html/empty_playlist_table.html";

const stepSize = 10;
Expand All @@ -26,7 +23,6 @@ export default class Player {
this.addAlbumToCart = addAlbumToCart;
this.createInputButtonPair = createInputButtonPair;
this.createShoppingCartItem = createShoppingCartItem;
this.createShoppingCartResetButton = createShoppingCartResetButton;
this.extractBandFollowInfo = extractBandFollowInfo;
this.getTralbumDetails = getTralbumDetails.bind(this);
this.createInputButtonPair = createInputButtonPair;
Expand All @@ -44,13 +40,6 @@ export default class Player {

Player.movePlaylist();

const cartRefreshButton = this.createShoppingCartResetButton({
className: "buttonLink",
innerText: "⟳",
buttonClicked: () => location.reload()
});
document.querySelector("#sidecartReveal").append(cartRefreshButton);

this.updatePlayerControlInterface();

const bandFollowInfo = this.extractBandFollowInfo();
Expand Down Expand Up @@ -92,6 +81,8 @@ export default class Player {
downloadCol.append(oneClick);

document.querySelectorAll("tr.track_row_view").forEach((row, i) => {
if (tralbumDetails.tracks[i] === undefined) return;

const {
price,
currency,
Expand Down
61 changes: 61 additions & 0 deletions src/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,64 @@ export function getTralbumDetails(item_id, item_type = "a") {

return fetch(`/api/mobile/25/tralbum_details`, requestOptions);
}

export function downloadFile(filename, text) {
var element = document.createElement("a");

element.setAttribute(
"href",
"data:text/plain;charset=utf-8," + encodeURIComponent(text)
);
element.setAttribute("download", filename);

element.style.display = "none";
document.body.appendChild(element);

element.click();

document.body.removeChild(element);
}

export function dateString() {
const currentdate = new Date();
const ye = new Intl.DateTimeFormat("en", { year: "2-digit" }).format(
currentdate
);
const mo = new Intl.DateTimeFormat("en", { month: "2-digit" }).format(
currentdate
);
const da = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(
currentdate
);

return `${ye}-${mo}-${da}`;
}

export function loadJsonFile() {
return new Promise((resolve, reject) => {
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = ".json";

fileInput.onchange = event => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();

reader.onload = e => {
try {
const jsonObject = JSON.parse(e.target.result);
resolve(jsonObject);
} catch (error) {
reject(error);
}
};

reader.onerror = error => reject(error);
reader.readAsText(file);
}
};

fileInput.click();
});
}
Loading

0 comments on commit 480c525

Please sign in to comment.