Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
API_KEY=23090sdj0sfj0sdfsdf
101 changes: 101 additions & 0 deletions billing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
const PACKAGE_API_URL = "https://api.example.com/v1/billing/packages";

const fallbackPackages = [
{
id: "starter",
name: "Starter",
description: "Core billing tools for a small team.",
priceMonthly: 19,
features: ["3 users", "Basic reports", "Email support"],
},
{
id: "growth",
name: "Growth",
description: "More controls for teams with recurring billing needs.",
priceMonthly: 49,
features: ["10 users", "Advanced reports", "Priority support"],
},
{
id: "scale",
name: "Scale",
description: "Expanded limits and support for larger billing operations.",
priceMonthly: 99,
features: ["Unlimited users", "Custom exports", "Dedicated support"],
},
];

function setStatus(message) {
$("#statusText").text(message);
}

function renderState(title, message, type) {
$("#packageGrid").html(
$("<article>", { class: `state-card ${type || ""}` }).append(
$("<strong>").text(title),
$("<p>").text(message),
),
);
}

function renderPackages(packages) {
if (!packages.length) {
renderState("No packages found", "There are no billing packages available.");
return;
}

const cards = packages.map((pkg) => {
const features = (pkg.features || []).map((feature) =>
$("<li>").text(feature),
);

return $("<article>", { class: "package-card" }).append(
$("<h2>").text(pkg.name),
$("<p>", { class: "package-description" }).text(pkg.description),
$("<p>", { class: "package-price" }).append(
`$${pkg.priceMonthly}`,
$("<span>").text(" / month"),
),
$("<ul>", { class: "feature-list" }).append(features),
$("<button>", {
type: "button",
text: "Select package",
"data-package-id": pkg.id,
}),
);
});

$("#packageGrid").empty().append(cards);
}

function loadPackages() {
setStatus("Loading packages...");
renderState("Loading", "Fetching billing packages from the API.");

$.ajax({
url: PACKAGE_API_URL,
method: "GET",
dataType: "json",
timeout: 5000,
})
.done((packages) => {
const normalized = Array.isArray(packages) ? packages : [];
setStatus(`Loaded ${normalized.length} package(s).`);
renderPackages(normalized);
})
.fail(() => {
setStatus("API unavailable. Showing demo packages.");
renderPackages(fallbackPackages);
});
}

$(function () {
loadPackages();

$("#reloadPackages").on("click", loadPackages);

$("#packageGrid").on("click", "button[data-package-id]", function () {
const packageId = $(this).data("package-id");
window.localStorage.setItem("selectedBillingPackage", packageId);
setStatus(`Selected package: ${packageId}`);
});
});
27 changes: 27 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Billing Packages</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main class="billing-page">
<header class="billing-header">
<p class="eyebrow">Billing</p>
<h1>Choose a package</h1>
</header>

<section class="billing-toolbar" aria-label="Billing controls">
<span id="statusText">Loading packages...</span>
<button id="reloadPackages" type="button">Reload</button>
</section>

<section id="packageGrid" class="package-grid" aria-live="polite"></section>
</main>

<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="./billing.js"></script>
</body>
</html>
173 changes: 173 additions & 0 deletions styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
:root {
color-scheme: light;
--bg: #f6f7f9;
--panel: #ffffff;
--text: #15191f;
--muted: #657080;
--line: #d9dee6;
--accent: #166534;
--accent-strong: #0f4f28;
--danger: #b42318;
}

* {
box-sizing: border-box;
}

body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
}

.billing-page {
width: min(1120px, calc(100% - 32px));
margin: 0 auto;
padding: 48px 0;
}

.billing-header {
margin-bottom: 28px;
}

.eyebrow {
margin: 0 0 8px;
color: var(--accent);
font-size: 13px;
font-weight: 700;
letter-spacing: 0;
text-transform: uppercase;
}

h1 {
margin: 0;
font-size: clamp(32px, 5vw, 52px);
line-height: 1;
letter-spacing: 0;
}

.billing-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
min-height: 56px;
margin-bottom: 20px;
padding: 12px 14px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel);
}

#statusText {
color: var(--muted);
font-size: 14px;
}

button {
min-height: 40px;
border: 0;
border-radius: 6px;
padding: 0 14px;
background: var(--accent);
color: #ffffff;
font: inherit;
font-weight: 700;
cursor: pointer;
}

button:hover {
background: var(--accent-strong);
}

.package-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
}

.package-card,
.state-card {
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel);
}

.package-card {
display: flex;
min-height: 340px;
flex-direction: column;
padding: 22px;
}

.package-card h2 {
margin: 0 0 8px;
font-size: 24px;
letter-spacing: 0;
}

.package-description {
min-height: 48px;
margin: 0 0 24px;
color: var(--muted);
line-height: 1.5;
}

.package-price {
margin: 0 0 20px;
font-size: 36px;
font-weight: 800;
letter-spacing: 0;
}

.package-price span {
color: var(--muted);
font-size: 15px;
font-weight: 600;
}

.feature-list {
display: grid;
gap: 10px;
margin: 0 0 24px;
padding: 0;
list-style: none;
}

.feature-list li {
color: var(--text);
font-size: 15px;
}

.package-card button {
width: 100%;
margin-top: auto;
}

.state-card {
grid-column: 1 / -1;
padding: 28px;
}

.state-card strong {
display: block;
margin-bottom: 8px;
}

.state-card p {
margin: 0;
color: var(--muted);
}

.state-card.error strong {
color: var(--danger);
}

@media (max-width: 840px) {
.package-grid {
grid-template-columns: 1fr;
}
}
5 changes: 5 additions & 0 deletions test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ hello
hello
hello
hello
hello
hello
hello
hello
hello