problem: can't see how many products ar remaining

This commit is contained in:
gsovereignty
2024-08-16 17:32:40 +08:00
parent 4cb488ea68
commit 7d1d5e35c5
11 changed files with 163 additions and 69 deletions

View File

@@ -3,8 +3,17 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import Separator from '@/components/ui/separator/separator.svelte'; import Separator from '@/components/ui/separator/separator.svelte';
import { currentUser } from '@/stores/session'; import { currentUser, devmode } from '@/stores/session';
import { GitBranch, HelpCircle, Mail, Package, Pyramid, Rocket, Users } from 'lucide-svelte'; import {
Code,
GitBranch,
HelpCircle,
Mail,
Package,
Pyramid,
Rocket,
Users
} from 'lucide-svelte';
import { GitAltBrand, TelegramBrand } from 'svelte-awesome-icons'; import { GitAltBrand, TelegramBrand } from 'svelte-awesome-icons';
let iconClass = 'h-5 w-5 md:h-4 md:w-4'; let iconClass = 'h-5 w-5 md:h-4 md:w-4';
@@ -57,4 +66,16 @@
<HelpCircle class={iconClass} /> <HelpCircle class={iconClass} />
Help Help
</a> </a>
<Separator class="my-2" /> <Separator class="dark:bg-slate-700" />
<a
href="#"
class={getClass('dev')}
on:click={() => {
devmode.update((dm) => {
return !dm;
});
}}
>
<Code class={iconClass} />
Toggle Dev Mode
</a>

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import * as Card from '@/components/ui/card'; import * as Card from '@/components/ui/card';
import * as Table from '@/components/ui/table'; import * as Table from '@/components/ui/table';
import { Rocket } from '@/event_helpers/rockets'; import { Rocket, ZapPurchase } from '@/event_helpers/rockets';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import Pie from './Pie.svelte'; import Pie from './Pie.svelte';
import { Avatar, Name } from '@nostr-dev-kit/ndk-svelte-components'; import { Avatar, Name } from '@nostr-dev-kit/ndk-svelte-components';
@@ -9,7 +9,7 @@
import NumberIncrement from '@components/ui/number-increment'; import NumberIncrement from '@components/ui/number-increment';
export let rocket: Rocket; export let rocket: Rocket;
export let unratifiedZaps: Map<string, number>; export let unratifiedZaps: Map<string, ZapPurchase>;
let unratifiedZapsAmount = 0; let unratifiedZapsAmount = 0;
let dataLoaded = false; let dataLoaded = false;
@@ -17,7 +17,7 @@
$: { $: {
unratifiedZapsAmount = 0; unratifiedZapsAmount = 0;
for (let [_, a] of unratifiedZaps) { for (let [_, a] of unratifiedZaps) {
unratifiedZapsAmount += a / 1000; unratifiedZapsAmount += a.Amount / 1000;
} }
unratifiedZapsAmount = unratifiedZapsAmount; unratifiedZapsAmount = unratifiedZapsAmount;
} }

View File

@@ -1,27 +1,26 @@
<script lang="ts"> <script lang="ts">
import { buttonVariants } from '$lib/components/ui/button/index.js'; import { Button, buttonVariants } from '$lib/components/ui/button/index.js';
import * as Dialog from '$lib/components/ui/dialog/index.js'; import * as Dialog from '$lib/components/ui/dialog/index.js';
import * as Alert from '@/components/ui/alert';
import { Input } from '$lib/components/ui/input'; import { Input } from '$lib/components/ui/input';
import { Button } from '$lib/components/ui/button/index.js'; import * as Alert from '@/components/ui/alert';
import type { Product, Rocket, RocketProduct } from '@/event_helpers/rockets';
import { formatSats } from '@/helpers';
import { ndk } from '@/ndk'; import { ndk } from '@/ndk';
import { currentUser } from '@/stores/session'; import { currentUser } from '@/stores/session';
import { NDKZap } from '@nostr-dev-kit/ndk'; import { NDKZap } from '@nostr-dev-kit/ndk';
import { Terminal } from 'lucide-svelte';
import { requestProvider } from 'webln';
import QrCodeSvg from './QrCodeSvg.svelte';
import CopyButton from './CopyButton.svelte';
import type { Product, Rocket, RocketProduct } from '@/event_helpers/rockets';
import { formatSats } from '@/helpers';
import { Spinner } from 'flowbite-svelte'; import { Spinner } from 'flowbite-svelte';
import { CheckCircleOutline } from 'flowbite-svelte-icons'; import { CheckCircleOutline } from 'flowbite-svelte-icons';
import { tweened, type Tweened } from 'svelte/motion'; import { Terminal } from 'lucide-svelte';
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing';
import { fade, fly } from 'svelte/transition'; import { tweened } from 'svelte/motion';
import { requestProvider } from 'webln';
import CopyButton from './CopyButton.svelte';
import QrCodeSvg from './QrCodeSvg.svelte';
export let product: Product; export let product: Product;
export let rocketProduct: RocketProduct | undefined; export let rocketProduct: RocketProduct | undefined;
export let rocket: Rocket; export let rocket: Rocket;
export let disabled = false;
let invoice: string | null; let invoice: string | null;
let paymentInitiated: boolean; let paymentInitiated: boolean;
@@ -37,7 +36,7 @@
zappedUser: rocket.Event.author zappedUser: rocket.Event.author
}); });
invoice = await z.createZapRequest( invoice = await z.createZapRequest(
rocketProduct.Price * 1000, rocketProduct.Price() * 1000,
`Purchase of ${product.Name()} from ${rocket.Event.dTag}`, `Purchase of ${product.Name()} from ${rocket.Event.dTag}`,
[['product', product.ID()]] [['product', product.ID()]]
); );
@@ -83,12 +82,16 @@
{#if rocketProduct} {#if rocketProduct}
<Dialog.Root bind:open> <Dialog.Root bind:open>
<Dialog.Trigger class={buttonVariants({ variant: 'default' })}> <Dialog.Trigger>
<Button {disabled}>
{#if open} {#if open}
<Spinner class="me-2" color="white" size={4} /> Confirming... <Spinner class="me-2" color="white" size={4} /> Confirming...
{:else} {:else if !disabled}
Buy Now for {formatSats(rocketProduct.Price)} Buy Now for {formatSats(rocketProduct.Price())}
{:else if disabled}
Out of Stock!
{/if} {/if}
</Button>
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Content class="sm:max-w-[425px]"> <Dialog.Content class="sm:max-w-[425px]">
@@ -104,9 +107,9 @@
</Alert.Root> </Alert.Root>
{/if} {/if}
<Dialog.Description <Dialog.Description
>Pay {rocketProduct.Price === 1 >Pay {rocketProduct.Price() === 1
? `${rocketProduct.Price} sat` ? `${rocketProduct.Price()} sat`
: `${rocketProduct.Price} sats`} now with Lightning</Dialog.Description : `${rocketProduct.Price()} sats`} now with Lightning</Dialog.Description
> >
</Dialog.Header> </Dialog.Header>
{#if invoice} {#if invoice}

View File

@@ -1,11 +1,43 @@
<script lang="ts"> <script lang="ts">
import * as Card from '$lib/components/ui/card/index.js'; import * as Card from '$lib/components/ui/card/index.js';
import { Product, Rocket } from '@/event_helpers/rockets'; import {
Product as ProductEvent,
Rocket,
RocketProduct,
ZapPurchase
} from '@/event_helpers/rockets';
import AddProductToRocket from './AddProductToRocket.svelte'; import AddProductToRocket from './AddProductToRocket.svelte';
import PayNow from './PayNow.svelte'; import PayNow from './PayNow.svelte';
import { onMount } from 'svelte';
import { devmode } from '@/stores/session';
export let product: Product; export let product: ProductEvent;
export let rocket: Rocket; export let rocket: Rocket;
export let unratifiedZaps: Map<string, ZapPurchase> | undefined = undefined;
let productFromRocket = rocket.Products().get(product.ID());
onMount(() => {
if (!product.Validate()) {
throw new Error('this should not happen');
}
});
function remainingProducts(product: RocketProduct, zaps?: Map<string, ZapPurchase>): number {
let numberOfPurchases = 0;
if (zaps) {
for (let [_, zap] of zaps) {
if (zap.ProductID == product.ID()) {
numberOfPurchases++;
}
}
}
let remaining = product.MaxPurchases() - numberOfPurchases;
if (remaining < 0) {
remaining = 0;
}
return remaining;
}
</script> </script>
{#if product.Validate()} {#if product.Validate()}
@@ -34,18 +66,30 @@
<img src={product.CoverImage()} alt="cover" class="aspect-square object-cover" /> <img src={product.CoverImage()} alt="cover" class="aspect-square object-cover" />
</div> </div>
{/if} {/if}
<Card.Footer class="flex items-center justify-center pt-2"> <Card.Footer class="flex flex-col items-center justify-center pt-2">
{#if !rocket.Products().get(product.ID())} {#if !rocket.Products().get(product.ID()) && !productFromRocket}
<AddProductToRocket {product} {rocket} /> <AddProductToRocket {product} {rocket} />
{:else} {:else if productFromRocket}
<PayNow {product} rocketProduct={rocket.Products().get(product.ID())} {rocket} /> {#if productFromRocket.MaxPurchases() && unratifiedZaps}
<div class="flex flex-nowrap">
{remainingProducts(productFromRocket, unratifiedZaps)} available
</div>
{/if} {/if}
<PayNow
disabled={productFromRocket.MaxPurchases() > 0 &&
remainingProducts(productFromRocket, unratifiedZaps) == 0}
{product}
rocketProduct={rocket.Products().get(product.ID())}
{rocket}
/>
{/if}
{#if $devmode}
<a <a
href="#" href="#"
on:click={() => { on:click={() => {
console.log(product); console.log(product);
}}>print to console</a }}>print to console</a
> >{/if}
</Card.Footer> </Card.Footer>
</Card.Root> </Card.Root>
{/if} {/if}

View File

@@ -3,10 +3,11 @@
import { ndk } from '@/ndk'; import { ndk } from '@/ndk';
import ProductCard from './ProductCard.svelte'; import ProductCard from './ProductCard.svelte';
import { fetchEvent } from '@/event_helpers/products'; import { fetchEvent } from '@/event_helpers/products';
import { Product, type Rocket } from '@/event_helpers/rockets'; import { Product, ZapPurchase, type Rocket } from '@/event_helpers/rockets';
export let productID: string | undefined = undefined; export let productID: string | undefined = undefined;
export let rocket: Rocket; export let rocket: Rocket;
export let product: Product | undefined = undefined; export let product: Product | undefined = undefined;
export let unratifiedZaps: Map<string, ZapPurchase> | undefined = undefined;
onMount(() => { onMount(() => {
if (!product && productID) { if (!product && productID) {
@@ -16,7 +17,7 @@
</script> </script>
{#if product} {#if product}
<ProductCard {rocket} {product}> <ProductCard {unratifiedZaps} {rocket} {product}>
<slot /> <slot />
</ProductCard> </ProductCard>
{/if} {/if}

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import * as Card from '$lib/components/ui/card/index.js'; import * as Card from '$lib/components/ui/card/index.js';
import { Product, Rocket } from '@/event_helpers/rockets'; import { Product, Rocket, ZapPurchase } from '@/event_helpers/rockets';
import { fetchEvent } from '@/event_helpers/products'; import { fetchEvent } from '@/event_helpers/products';
import { ndk } from '@/ndk'; import { ndk } from '@/ndk';
import { derived, writable } from 'svelte/store'; import { derived, writable } from 'svelte/store';
@@ -9,7 +9,7 @@
import CreateMeritRequest from './CreateMeritRequest.svelte'; import CreateMeritRequest from './CreateMeritRequest.svelte';
export let rocket: Rocket; export let rocket: Rocket;
export let unratifiedZaps: Map<string, number>; export let unratifiedZaps: Map<string, ZapPurchase>;
let products = writable(new Map<string, Product>()); let products = writable(new Map<string, Product>());
@@ -28,7 +28,6 @@
let groups = derived(products, ($products) => { let groups = derived(products, ($products) => {
let productGroups = new Map<string, Map<string, Product>>(); let productGroups = new Map<string, Map<string, Product>>();
for (let [id, p] of $products) { for (let [id, p] of $products) {
console.log(p.Group());
if (!productGroups.get(p.Group())) { if (!productGroups.get(p.Group())) {
productGroups.set(p.Group(), new Map()); productGroups.set(p.Group(), new Map());
} }

View File

@@ -3,16 +3,16 @@
import ProductPurchases from './ProductPurchases.svelte'; import ProductPurchases from './ProductPurchases.svelte';
import * as Pagination from '@/components/ui/pagination'; import * as Pagination from '@/components/ui/pagination';
import { ChevronLeft, ChevronRight } from 'lucide-svelte'; import { ChevronLeft, ChevronRight } from 'lucide-svelte';
import { Product, Rocket } from '@/event_helpers/rockets'; import { Product, Rocket, ZapPurchase } from '@/event_helpers/rockets';
export let rocket: Rocket; export let rocket: Rocket;
export let products: Product[]; export let products: Product[];
export let unratifiedZaps: Map<string, number> | undefined = undefined; export let unratifiedZaps: Map<string, ZapPurchase> | undefined = undefined;
</script> </script>
<Pagination.Root count={products.length} perPage={1} siblingCount={1} let:pages let:currentPage> <Pagination.Root count={products.length} perPage={1} siblingCount={1} let:pages let:currentPage>
{#if currentPage} {#if currentPage}
<ProductCardFromId {rocket} product={products[currentPage - 1]}> <ProductCardFromId {unratifiedZaps} {rocket} product={products[currentPage - 1]}>
{#if unratifiedZaps} {#if unratifiedZaps}
<ProductPurchases bind:unratifiedZaps {rocket} {products} /> <ProductPurchases bind:unratifiedZaps {rocket} {products} />
{/if} {/if}

View File

@@ -11,7 +11,7 @@
//export let products: Product[]; //export let products: Product[];
export let rocket: Rocket; export let rocket: Rocket;
export let unratifiedZaps: Map<string, number>; export let unratifiedZaps: Map<string, ZapPurchase>;
let zaps = $ndk.storeSubscribe( let zaps = $ndk.storeSubscribe(
[{ '#a': [`31108:${rocket.Event.author.pubkey}:${rocket.Event.dTag}`], kinds: [9735] }], [{ '#a': [`31108:${rocket.Event.author.pubkey}:${rocket.Event.dTag}`], kinds: [9735] }],
@@ -85,7 +85,7 @@
validatedZapsNotInRocket.subscribe((zaps) => { validatedZapsNotInRocket.subscribe((zaps) => {
for (let [_, z] of zaps) { for (let [_, z] of zaps) {
unratifiedZaps.set(z.ZapReceipt.id, z.Amount); unratifiedZaps.set(z.ZapReceipt.id, z);
} }
unratifiedZaps = unratifiedZaps; unratifiedZaps = unratifiedZaps;
}); });

View File

@@ -2,7 +2,7 @@
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js'; import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import Button from '@/components/ui/button/button.svelte'; import Button from '@/components/ui/button/button.svelte';
import * as Card from '@/components/ui/card'; import * as Card from '@/components/ui/card';
import { Rocket } from '@/event_helpers/rockets'; import { Rocket, ZapPurchase } from '@/event_helpers/rockets';
import type { NDKEvent } from '@nostr-dev-kit/ndk'; import type { NDKEvent } from '@nostr-dev-kit/ndk';
import BitcoinAssociations from './AssociatedBitcoinAddresses.svelte'; import BitcoinAssociations from './AssociatedBitcoinAddresses.svelte';
import MeritRequests from './MeritRequests.svelte'; import MeritRequests from './MeritRequests.svelte';
@@ -14,7 +14,7 @@
export let rocket: NDKEvent; export let rocket: NDKEvent;
$: unratifiedZaps = new Map<string, number>(); $: unratifiedZaps = new Map<string, ZapPurchase>();
</script> </script>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">

View File

@@ -175,7 +175,7 @@ export class Rocket {
let _products = new Map<string, RocketProduct>(); let _products = new Map<string, RocketProduct>();
for (let p of this.Event.getMatchingTags('product')) { for (let p of this.Event.getMatchingTags('product')) {
let rp = new RocketProduct(p); let rp = new RocketProduct(p);
_products.set(rp.ID, rp); _products.set(rp.ID(), rp);
} }
return _products; return _products;
} }
@@ -551,28 +551,52 @@ export class RocketAMR {
} }
export class RocketProduct { export class RocketProduct {
ID: string; tag: NDKTag;
Price: number; ID(): string {
ValidAfter: number; //unix time return this.tag[1].split(':')[0];
MaxPurchases: number; }
Purchases: Map<string, ProductPayment>; Price(): number {
return parseInt(this.tag[1].split(':')[1], 10);
}
ValidAfter(): number {
return parseInt(this.tag[1].split(':')[2], 10);
}
MaxPurchases(): number {
return parseInt(this.tag[1].split(':')[3], 10);
}
Purchases(): Map<string, ProductPayment> {
let result: Map<string, ProductPayment> = new Map();
let purchases = JSON.parse(this.tag[3]);
for (let p of purchases) {
let payment = new ProductPayment(p);
result.set(payment.ZapID, payment);
}
return result;
}
PurchasesJSON(): string { PurchasesJSON(): string {
let purchases = []; let purchases = [];
for (let [_, p] of this.Purchases) { for (let [_, p] of this.Purchases()) {
purchases.push(`${p.ZapID}:${p.BuyerPubkey}:${p.WitnessedAt}`); purchases.push(`${p.ZapID}:${p.BuyerPubkey}:${p.WitnessedAt}`);
} }
return JSON.stringify(purchases); return JSON.stringify(purchases);
} }
Validate(): boolean {
try {
this.ID();
this.Price();
this.ValidAfter();
this.MaxPurchases();
this.Purchases();
this.PurchasesJSON();
return true;
} catch {
return false;
}
}
constructor(tag: NDKTag) { constructor(tag: NDKTag) {
this.Purchases = new Map(); this.tag = tag;
this.ID = tag[1].split(':')[0]; if (!this.Validate()) {
this.Price = parseInt(tag[1].split(':')[1], 10); throw new Error('bug!');
this.ValidAfter = parseInt(tag[1].split(':')[2], 10);
this.MaxPurchases = parseInt(tag[1].split(':')[3], 10);
let purchases = JSON.parse(tag[3]);
for (let p of purchases) {
let payment = new ProductPayment(p);
this.Purchases.set(payment.ZapID, payment);
} }
} }
} }
@@ -610,7 +634,7 @@ export class ZapPurchase {
IncludedInRocketState(rocket: NDKEvent): boolean { IncludedInRocketState(rocket: NDKEvent): boolean {
let thisProduct = this.ProductFromRocket(rocket); let thisProduct = this.ProductFromRocket(rocket);
if (thisProduct) { if (thisProduct) {
return thisProduct.Purchases.get(this.ZapReceipt.id) ? true : false; return thisProduct.Purchases().get(this.ZapReceipt.id) ? true : false;
} else { } else {
return false; return false;
} }
@@ -627,7 +651,7 @@ export class ZapPurchase {
return true; return true;
} }
let product = this.ProductFromRocket(rocket); let product = this.ProductFromRocket(rocket);
if (product && this.Amount / 1000 >= product.Price) { if (product && this.Amount / 1000 >= product.Price()) {
return true; return true;
} }
return false; return false;

View File

@@ -9,3 +9,5 @@ export async function prepareUserSession(ndk: NDKSvelte, user: NDKUser): Promise
//implement any session set up stuff here //implement any session set up stuff here
}); });
} }
export const devmode = writable(false);