Compare commits
2 Commits
4992890503
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c90bf21111 | ||
|
|
3957a86f9c |
@@ -886,6 +886,16 @@ footer .logo-svg > svg { width: var(--logo-width, 8rem); }
|
|||||||
-------------------------------------------------- */
|
-------------------------------------------------- */
|
||||||
#gridView { row-gap: 1.5rem; }
|
#gridView { row-gap: 1.5rem; }
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.catalog-sidebar {
|
||||||
|
position: sticky;
|
||||||
|
top: var(--navbar-height, 4rem);
|
||||||
|
height: calc(100vh - var(--navbar-height, 4rem));
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.inv-grid-card {
|
.inv-grid-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
---
|
---
|
||||||
|
// auth check for inventory management features
|
||||||
|
const { canAddInventory } = Astro.locals;
|
||||||
|
const { isAdmin } = Astro.locals;
|
||||||
|
// const canAddInventory = false;
|
||||||
---
|
---
|
||||||
<button
|
<button
|
||||||
class="navbar-toggler ms-4 p-1 btn btn-purple border-0"
|
class="navbar-toggler ms-4 p-1 btn btn-purple border-0"
|
||||||
@@ -30,9 +34,16 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link py-3 border-bottom border-secondary" href="/pokemon">Browse Cards</a>
|
<a class="nav-link py-3 border-bottom border-secondary" href="/pokemon">Browse Cards</a>
|
||||||
</li>
|
</li>
|
||||||
<!--<li class="nav-item">
|
{canAddInventory && (
|
||||||
<a class="nav-link py-3" href="/dashboard">Dashboard</a>
|
<li class="nav-item">
|
||||||
</li> -->
|
<a class="nav-link py-3 border-bottom border-secondary" href="/dashboard">Dashboard</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{isAdmin && (
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link py-3" href="/admin">Admin Panel</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,47 +25,77 @@ import { Show } from '@clerk/astro/components'
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Show when="signed-in">
|
<Show when="signed-in">
|
||||||
<form
|
{Astro.url.pathname === '/dashboard' ? (
|
||||||
class="d-flex align-items-center"
|
|
||||||
role="search"
|
|
||||||
id="searchform"
|
|
||||||
hx-post="/partials/cards"
|
|
||||||
hx-target="#cardGrid"
|
|
||||||
hx-trigger="load, submit"
|
|
||||||
hx-vals='{"start":"0"}'
|
|
||||||
hx-on--after-request="afterUpdate()"
|
|
||||||
hx-on--before-request="beforeSearch()"
|
|
||||||
>
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
{Astro.url.pathname === '/pokemon' && (
|
<input
|
||||||
<a class="btn btn-purple" data-bs-toggle="offcanvas" href="#filterBar" type="button" role="button" aria-controls="filterBar" aria-label="filter">
|
type="search"
|
||||||
<span class="d-block d-md-none filter-icon py-2">
|
name="q"
|
||||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M528.8 96.3C558.6 90.8 571.2 118.9 568.9 142.2C572.3 173.4 570.8 207 553.9 230.8C513.9 283.2 459.3 315.9 414.3 364.3C414.9 418.3 419.8 459.8 423.6 511.2C427.6 552.4 388.7 586.8 346.6 570.1C303.2 550.5 259.4 527.5 230.4 493.3C217 453.1 225.9 407.5 222.2 365.3C222.2 365.3 222.1 365.1 222 365C151.4 319.6 59.3 250.9 61 158.4C59.9 121 91.8 96.1 123.8 96.5C259.3 98.5 394.1 104.4 528.8 96.3zM506.1 161.4C378.3 168.2 252 162.1 125.2 160.5C128.6 227 199 270.8 250 306.8C305.5 335.4 281.6 410.5 288.3 461.7C310.8 478.9 334.6 494.6 358.9 505.8C355.4 458 350.7 415.4 350.2 364.6C349.9 349.2 355.3 333.7 366.5 321.7C384.3 302.6 402.8 287.8 421.5 270.1C446.1 245.2 477.9 225.1 499.7 196.7C509 182.2 504.7 174.5 506 161.5z"/></svg>
|
id="inventorySearchInput"
|
||||||
</span>
|
class="form-control search-input"
|
||||||
<span class="d-none d-md-block fw-medium">Filters</span>
|
placeholder="Search your inventory"
|
||||||
</a>
|
hx-post="/partials/inventory-cards"
|
||||||
)}
|
hx-trigger="keyup[key=='Enter']"
|
||||||
<input type="hidden" name="start" id="start" value="0" />
|
hx-target="#gridView"
|
||||||
<input type="hidden" name="sort" id="sortInput" value="" />
|
hx-swap="innerHTML"
|
||||||
<input type="hidden" name="language" id="languageInput" value="all" />
|
hx-vals="js:{start: 0, catalog: document.querySelector('#catalogList li[data-catalog].active')?.dataset.catalog || 'all'}"
|
||||||
<input type="search" name="q" id="searchInput" class="form-control search-input" placeholder="Search cards" />
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="button"
|
||||||
|
id="inventorySearchBtn"
|
||||||
class="btn btn-purple border-start-0"
|
class="btn btn-purple border-start-0"
|
||||||
aria-label="search"
|
aria-label="search"
|
||||||
onclick="
|
hx-post="/partials/inventory-cards"
|
||||||
const q = this.closest('form').querySelector('[name=q]').value;
|
hx-include="#inventorySearchInput"
|
||||||
dataLayer.push({ event: 'view_search_results', search_term: q });
|
hx-target="#gridView"
|
||||||
if (window.location.pathname !== '/pokemon') {
|
hx-swap="innerHTML"
|
||||||
event.preventDefault();
|
hx-vals="js:{start: 0, catalog: document.querySelector('#catalogList li[data-catalog].active')?.dataset.catalog || 'all'}"
|
||||||
event.stopPropagation();
|
|
||||||
sessionStorage.setItem('pendingSearch', q);
|
|
||||||
window.location.href = '/pokemon';
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<svg aria-hidden="true" class="search-button d-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M503.7 304.9C520.3 80.3 214-44 100.9 169.4C-14.1 383.9 203.9 614.6 419.8 466.3C459.7 500.3 494.8 542.3 531.5 578.2C561.1 607.7 606.3 562.8 576.8 533L540 496.1C520.2 471.6 495.7 449.1 473.7 428.9C471.1 426.5 468.5 424.2 466 421.9C491.9 385.4 500.1 341 503.7 304.8zM236.1 129C334 92.1 452.1 198.1 440 298.6C440.5 404.9 335.6 462.2 244 445.8C99 407.1 100.3 178.9 236.2 129z"/></svg>
|
<svg aria-hidden="true" class="search-button d-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M503.7 304.9C520.3 80.3 214-44 100.9 169.4C-14.1 383.9 203.9 614.6 419.8 466.3C459.7 500.3 494.8 542.3 531.5 578.2C561.1 607.7 606.3 562.8 576.8 533L540 496.1C520.2 471.6 495.7 449.1 473.7 428.9C471.1 426.5 468.5 424.2 466 421.9C491.9 385.4 500.1 341 503.7 304.8zM236.1 129C334 92.1 452.1 198.1 440 298.6C440.5 404.9 335.6 462.2 244 445.8C99 407.1 100.3 178.9 236.2 129z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
) : (
|
||||||
|
<form
|
||||||
|
class="d-flex align-items-center"
|
||||||
|
role="search"
|
||||||
|
id="searchform"
|
||||||
|
hx-post="/partials/cards"
|
||||||
|
hx-target="#cardGrid"
|
||||||
|
hx-trigger="load, submit"
|
||||||
|
hx-vals='{"start":"0"}'
|
||||||
|
hx-on--after-request="afterUpdate()"
|
||||||
|
hx-on--before-request="beforeSearch()"
|
||||||
|
>
|
||||||
|
<div class="input-group">
|
||||||
|
{Astro.url.pathname === '/pokemon' && (
|
||||||
|
<a class="btn btn-purple" data-bs-toggle="offcanvas" href="#filterBar" type="button" role="button" aria-controls="filterBar" aria-label="filter">
|
||||||
|
<span class="d-block d-md-none filter-icon py-2">
|
||||||
|
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M528.8 96.3C558.6 90.8 571.2 118.9 568.9 142.2C572.3 173.4 570.8 207 553.9 230.8C513.9 283.2 459.3 315.9 414.3 364.3C414.9 418.3 419.8 459.8 423.6 511.2C427.6 552.4 388.7 586.8 346.6 570.1C303.2 550.5 259.4 527.5 230.4 493.3C217 453.1 225.9 407.5 222.2 365.3C222.2 365.3 222.1 365.1 222 365C151.4 319.6 59.3 250.9 61 158.4C59.9 121 91.8 96.1 123.8 96.5C259.3 98.5 394.1 104.4 528.8 96.3zM506.1 161.4C378.3 168.2 252 162.1 125.2 160.5C128.6 227 199 270.8 250 306.8C305.5 335.4 281.6 410.5 288.3 461.7C310.8 478.9 334.6 494.6 358.9 505.8C355.4 458 350.7 415.4 350.2 364.6C349.9 349.2 355.3 333.7 366.5 321.7C384.3 302.6 402.8 287.8 421.5 270.1C446.1 245.2 477.9 225.1 499.7 196.7C509 182.2 504.7 174.5 506 161.5z"/></svg>
|
||||||
|
</span>
|
||||||
|
<span class="d-none d-md-block fw-medium">Filters</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<input type="hidden" name="start" id="start" value="0" />
|
||||||
|
<input type="hidden" name="sort" id="sortInput" value="" />
|
||||||
|
<input type="hidden" name="language" id="languageInput" value="all" />
|
||||||
|
<input type="search" name="q" id="searchInput" class="form-control search-input" placeholder="Search cards" />
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-purple border-start-0"
|
||||||
|
aria-label="search"
|
||||||
|
onclick="
|
||||||
|
const q = this.closest('form').querySelector('[name=q]').value;
|
||||||
|
dataLayer.push({ event: 'view_search_results', search_term: q });
|
||||||
|
if (window.location.pathname !== '/pokemon') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
sessionStorage.setItem('pendingSearch', q);
|
||||||
|
window.location.href = '/pokemon';
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<svg aria-hidden="true" class="search-button d-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M503.7 304.9C520.3 80.3 214-44 100.9 169.4C-14.1 383.9 203.9 614.6 419.8 466.3C459.7 500.3 494.8 542.3 531.5 578.2C561.1 607.7 606.3 562.8 576.8 533L540 496.1C520.2 471.6 495.7 449.1 473.7 428.9C471.1 426.5 468.5 424.2 466 421.9C491.9 385.4 500.1 341 503.7 304.8zM236.1 129C334 92.1 452.1 198.1 440 298.6C440.5 404.9 335.6 462.2 244 445.8C99 407.1 100.3 178.9 236.2 129z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
@@ -5,6 +5,7 @@ declare global {
|
|||||||
namespace App {
|
namespace App {
|
||||||
interface Locals {
|
interface Locals {
|
||||||
canAddInventory: boolean;
|
canAddInventory: boolean;
|
||||||
|
isAdmin: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,53 +27,31 @@ export const onRequest = clerkMiddleware(async (auth, context, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Inventory visibility check ──────────────────────────────────────────────
|
// ── Inventory visibility check ──────────────────────────────────────────────
|
||||||
// Resolves to true if the user belongs to the target org OR has the feature
|
|
||||||
const canAddInventory = process.env.INVENTORY_ACCESS === 'true' ||
|
const canAddInventory = process.env.INVENTORY_ACCESS === 'true' ||
|
||||||
(
|
(
|
||||||
isAuthenticated &&
|
isAuthenticated &&
|
||||||
userId &&
|
userId &&
|
||||||
(
|
(
|
||||||
!!has({ permission: "org:feature:inventory_add" }) || // Clerk feature flag
|
!!has({ permission: "org:feature:inventory_add" }) ||
|
||||||
(await getUserOrgIds(context, userId)).includes(TARGET_ORG_ID)
|
(await getUserOrgIds(context, userId)).includes(TARGET_ORG_ID)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Expose the flag to your Astro pages via locals
|
|
||||||
context.locals.canAddInventory = Boolean(canAddInventory);
|
context.locals.canAddInventory = Boolean(canAddInventory);
|
||||||
|
|
||||||
|
// ── Admin check ──────────────────────────────────────────────────────────
|
||||||
|
// Computed on every request (not just /admin routes) so pages can use
|
||||||
|
// Astro.locals.isAdmin to conditionally show admin-only UI anywhere.
|
||||||
|
context.locals.isAdmin = isAuthenticated && userId
|
||||||
|
? await checkIsAdmin(context, userId)
|
||||||
|
: false;
|
||||||
|
|
||||||
// ── Admin route guard ───────────────────────────────────────────
|
// ── Admin route guard ───────────────────────────────────────────
|
||||||
if (isAdminRoute(context.request)) {
|
if (isAdminRoute(context.request)) {
|
||||||
if (!isAuthenticated || !userId) {
|
if (!isAuthenticated || !userId) {
|
||||||
return redirectToSignIn();
|
return redirectToSignIn();
|
||||||
}
|
}
|
||||||
|
if (!context.locals.isAdmin) {
|
||||||
try {
|
return new Response(null, { status: 404 });
|
||||||
const client = await clerkClient(context);
|
|
||||||
const userOrgIds = await getUserOrgIds(context, userId);
|
|
||||||
const matchingOrgIds = userOrgIds.filter((id) => ADMIN_ORG_IDS.has(id));
|
|
||||||
|
|
||||||
if (matchingOrgIds.length === 0) {
|
|
||||||
return new Response(null, { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const membershipLists = await Promise.all(
|
|
||||||
matchingOrgIds.map((orgId) =>
|
|
||||||
client.organizations.getOrganizationMembershipList({ organizationId: orgId })
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const isAdmin = membershipLists.some((list) =>
|
|
||||||
list.data.some(
|
|
||||||
(m) => m.publicUserData?.userId === userId && m.role === "org:admin"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isAdmin) {
|
|
||||||
return new Response(null, { status: 404 });
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Clerk membership check failed:", e);
|
|
||||||
return context.redirect("/");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,4 +68,29 @@ async function getUserOrgIds(context: any, userId: string): Promise<string[]> {
|
|||||||
console.error("Failed to fetch user org memberships:", e);
|
console.error("Failed to fetch user org memberships:", e);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helper: is this user an org:admin in any of ADMIN_ORG_IDS? ─────────────
|
||||||
|
async function checkIsAdmin(context: any, userId: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const userOrgIds = await getUserOrgIds(context, userId);
|
||||||
|
const matchingOrgIds = userOrgIds.filter((id) => ADMIN_ORG_IDS.has(id));
|
||||||
|
if (matchingOrgIds.length === 0) return false;
|
||||||
|
|
||||||
|
const client = await clerkClient(context);
|
||||||
|
const membershipLists = await Promise.all(
|
||||||
|
matchingOrgIds.map((orgId) =>
|
||||||
|
client.organizations.getOrganizationMembershipList({ organizationId: orgId })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return membershipLists.some((list) =>
|
||||||
|
list.data.some(
|
||||||
|
(m) => m.publicUserData?.userId === userId && m.role === "org:admin"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Clerk membership check failed:", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
---
|
---
|
||||||
import Layout from "../layouts/Main.astro";
|
export const prerender = false;
|
||||||
import Footer from "../components/Footer.astro";
|
import Layout from '../layouts/Main.astro';
|
||||||
import BackToTop from "../components/BackToTop.astro";
|
|
||||||
import FirstEditionIcon from "../components/FirstEditionIcon.astro";
|
|
||||||
import { db } from '../db/index';
|
import { db } from '../db/index';
|
||||||
import { inventory, skus } from '../db/schema';
|
import { inventory, skus } from '../db/schema';
|
||||||
import { sql, sum, eq } from "drizzle-orm";
|
import { sql, sum, eq } from "drizzle-orm";
|
||||||
@@ -41,99 +39,89 @@ const catalogs = catalogRows
|
|||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Inventory Dashboard">
|
<Layout title="Inventory Dashboard">
|
||||||
<div class="container-fluid container-sm mt-3" slot="page">
|
<div class="container-fluid container-sm" slot="page">
|
||||||
<BackToTop />
|
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<aside class="col-12 col-md-2 border-end border-secondary bg-dark p-3 d-flex flex-column gap-3">
|
<aside
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
class="offcanvas-xl offcanvas-start col-12 col-xl-2 border-end border-secondary bg-dark p-3 d-flex flex-column gap-3 catalog-sidebar"
|
||||||
<h6 class="mb-0 text-uppercase text-secondary fw-bold ls-wide" style="letter-spacing:.08em">Catalogs</h6>
|
tabindex="-1"
|
||||||
|
id="catalogSidebar"
|
||||||
|
aria-labelledby="catalogSidebarLabel"
|
||||||
|
>
|
||||||
|
<div class="offcanvas-header d-xl-none">
|
||||||
|
<h6 class="offcanvas-title text-uppercase text-secondary fw-bold ls-wide" id="catalogSidebarLabel" style="letter-spacing:.08em">Catalogs</h6>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" data-bs-target="#catalogSidebar" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul id="catalogList" class="list-group list-group-flush">
|
<div class="offcanvas-body d-xl-flex flex-column gap-3 p-0">
|
||||||
<li
|
<div class="d-none d-xl-flex justify-content-between align-items-center">
|
||||||
class="list-group-item list-group-item-action fw-semibold border-0 rounded p-2 d-flex align-items-center active"
|
<h6 class="mb-0 text-uppercase text-secondary fw-bold ls-wide" style="letter-spacing:.08em">Catalogs</h6>
|
||||||
data-catalog="all"
|
</div>
|
||||||
role="button"
|
|
||||||
style="cursor:pointer"
|
|
||||||
hx-post="/partials/inventory-cards"
|
|
||||||
hx-vals={JSON.stringify({ catalog: "all" })}
|
|
||||||
hx-target="#gridView"
|
|
||||||
hx-swap="innerHTML"
|
|
||||||
>
|
|
||||||
<span class="d-flex align-items-center gap-2">
|
|
||||||
View all cards
|
|
||||||
</span>
|
|
||||||
<span class="badge rounded-pill text-bg-secondary small ms-auto">{totalQty}</span>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{catalogs.map((name) => (
|
<ul id="catalogList" class="list-group list-group-flush">
|
||||||
<li
|
<li
|
||||||
class="ms-2 list-group-item list-group-item-action bg-transparent text-light border-0 rounded px-2 py-2 d-flex align-items-center gap-2"
|
class="list-group-item list-group-item-action fw-semibold border-0 rounded p-2 d-flex align-items-center active"
|
||||||
data-catalog={name}
|
data-catalog="all"
|
||||||
role="button"
|
role="button"
|
||||||
style="cursor:pointer"
|
style="cursor:pointer"
|
||||||
hx-post="/partials/inventory-cards"
|
hx-post="/partials/inventory-cards"
|
||||||
hx-vals={JSON.stringify({ catalog: name })}
|
hx-vals={JSON.stringify({ catalog: "all" })}
|
||||||
hx-target="#gridView"
|
hx-target="#gridView"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
>
|
>
|
||||||
{name}
|
<span class="d-flex align-items-center gap-2">
|
||||||
|
All cards
|
||||||
|
</span>
|
||||||
|
<span class="badge rounded-pill text-bg-secondary small ms-auto">{totalQty}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="mt-auto pt-3 border-top border-secondary small text-secondary">
|
{catalogs.map((name) => (
|
||||||
<div class="d-flex justify-content-between mb-1"><span>Total Cards</span><span class="text-light fw-semibold">{totalQty}</span></div>
|
<li
|
||||||
<div class="d-flex justify-content-between mb-1"><span>Market Value</span><span class="text-success fw-semibold">${totalValue.toFixed(0)}</span></div>
|
class="ms-2 list-group-item list-group-item-action bg-transparent text-light border-0 rounded px-2 py-2 d-flex align-items-center gap-2"
|
||||||
<div class="d-flex justify-content-between"><span>Gain/Loss</span><span class={`fw-semibold ${totalGain >= 0 ? "text-success" : "text-danger"}`}>{totalGain >= 0 ? "+" : ""}${Math.abs(totalGain).toFixed(0)}</span></div>
|
data-catalog={name}
|
||||||
|
role="button"
|
||||||
|
style="cursor:pointer"
|
||||||
|
hx-post="/partials/inventory-cards"
|
||||||
|
hx-vals={JSON.stringify({ catalog: name })}
|
||||||
|
hx-target="#gridView"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="mt-auto pt-3 border-top border-secondary small text-secondary">
|
||||||
|
<div class="d-flex justify-content-between mb-1"><span>Total Cards</span><span class="text-light fw-semibold">{totalQty}</span></div>
|
||||||
|
<div class="d-flex justify-content-between mb-1"><span>Market Value</span><span class="text-success fw-semibold">${totalValue.toFixed(0)}</span></div>
|
||||||
|
<div class="d-flex justify-content-between"><span>Gain/Loss</span><span class={`fw-semibold ${totalGain >= 0 ? "text-success" : "text-danger"}`}>{totalGain >= 0 ? "+" : ""}${Math.abs(totalGain).toFixed(0)}</span></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="col-12 col-md-10 mt-4">
|
<main class="col-12 col-xl-10 mt-4">
|
||||||
<div class="d-flex flex-wrap gap-2 mb-4">
|
<div class="d-flex flex-wrap flex-row gap-2 mb-4">
|
||||||
|
<button
|
||||||
<a href="/pokemon" class="btn btn-vendor">Add cards</a>
|
type="button"
|
||||||
|
class="btn btn-secondary d-xl-none me-auto"
|
||||||
|
data-bs-toggle="offcanvas"
|
||||||
|
data-bs-target="#catalogSidebar"
|
||||||
|
aria-controls="catalogSidebar"
|
||||||
|
>
|
||||||
|
Catalogs
|
||||||
|
</button>
|
||||||
|
<a href="/pokemon" class="btn btn-vendor ms-auto">Add cards</a>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-secondary"
|
class="btn btn-outline-secondary"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
data-bs-target="#bulkImportModal"
|
data-bs-target="#bulkImportModal"
|
||||||
>Bulk Import</button>
|
>Bulk Import</button>
|
||||||
|
|
||||||
<div class="ms-auto position-relative">
|
|
||||||
<div class="input-group">
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
name="q"
|
|
||||||
id="inventorySearchInput"
|
|
||||||
class="form-control search-input"
|
|
||||||
placeholder="Search your inventory"
|
|
||||||
hx-post="/partials/inventory-cards"
|
|
||||||
hx-trigger="keyup[key=='Enter']"
|
|
||||||
hx-target="#gridView"
|
|
||||||
hx-swap="innerHTML"
|
|
||||||
hx-vals="js:{start: 0, catalog: document.querySelector('#catalogList li[data-catalog].active')?.dataset.catalog || 'all'}"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
id="inventorySearchBtn"
|
|
||||||
class="btn btn-purple border-start-0"
|
|
||||||
aria-label="search"
|
|
||||||
hx-post="/partials/inventory-cards"
|
|
||||||
hx-include="#inventorySearchInput"
|
|
||||||
hx-target="#gridView"
|
|
||||||
hx-swap="innerHTML"
|
|
||||||
hx-vals="js:{start: 0, catalog: document.querySelector('#catalogList li[data-catalog].active')?.dataset.catalog || 'all'}"
|
|
||||||
>
|
|
||||||
<svg aria-hidden="true" class="search-button d-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M503.7 304.9C520.3 80.3 214-44 100.9 169.4C-14.1 383.9 203.9 614.6 419.8 466.3C459.7 500.3 494.8 542.3 531.5 578.2C561.1 607.7 606.3 562.8 576.8 533L540 496.1C520.2 471.6 495.7 449.1 473.7 428.9C471.1 426.5 468.5 424.2 466 421.9C491.9 385.4 500.1 341 503.7 304.8zM236.1 129C334 92.1 452.1 198.1 440 298.6C440.5 404.9 335.6 462.2 244 445.8C99 407.1 100.3 178.9 236.2 129z"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="inventoryView">
|
<div id="inventoryView">
|
||||||
<div id="gridView" class="row g-4 row-cols-2 row-cols-md-3 row-cols-xl-4 row-cols-xxxl-5" hx-post="/partials/inventory-cards" hx-trigger="load">
|
<div id="gridView" class="row g-4 row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xxxl-5" hx-post="/partials/inventory-cards" hx-trigger="load">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -547,5 +535,4 @@ const catalogs = catalogRows
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
<Footer slot="footer" />
|
</Layout>
|
||||||
</Layout>
|
|
||||||
@@ -197,9 +197,7 @@ const altSearchUrl = (card: any) => {
|
|||||||
</datalist>
|
</datalist>
|
||||||
|
|
||||||
<label for="note" class="form-label mt-2">Note</label>
|
<label for="note" class="form-label mt-2">Note</label>
|
||||||
<textarea class="form-control" id="note" name="note" rows="5" maxlength="255" placeholder="e.g. bought at local shop, gift, graded copy…">
|
<textarea class="form-control" id="note" name="note" rows="5" maxlength="255" placeholder="e.g. bought at local shop, gift, graded copy…">{inv?.note ?? ''}</textarea>
|
||||||
{inv?.note ?? ''}
|
|
||||||
</textarea>
|
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user