From af2343233c784b4a56e0a5a924131fc246f3e7b6 Mon Sep 17 00:00:00 2001 From: ericspaghetti <12552123+ericspaghetti@users.noreply.github.com> Date: Mon, 11 Mar 2024 01:36:35 -0700 Subject: [PATCH 1/2] updates --- components/display-product-modal.tsx | 43 +- components/display-products.tsx | 165 +- components/home/marketplace.tsx | 153 +- components/home/my-listings.tsx | 36 +- components/listing-page.tsx | 15 - components/nav-bottom.tsx | 18 + components/nav-side.tsx | 13 +- components/product-form.tsx | 354 ++-- .../utility-components/compact-categories.tsx | 14 +- .../dropdowns/category-dropdown.tsx | 43 + .../dropdowns/location-dropdown.tsx | 162 +- .../utility-components/file-uploader.tsx | 7 +- .../utility-components/product-card.tsx | 47 +- components/utility-components/search.tsx | 38 + components/utility/STATIC-VARIABLES.ts | 40 - components/utility/STATIC-VARIABLES.tsx | 59 + components/utility/nostr-helper-functions.ts | 3 +- .../utility/product-parser-functions.tsx | 52 +- package-lock.json | 399 +++-- package.json | 4 +- pages/_app.tsx | 212 ++- pages/api/nostr/cache-service.ts | 27 +- pages/api/nostr/fetch-service.ts | 99 +- pages/settings/user-profile.tsx | 6 +- public/locationSelection.json | 1525 +++++++++++++---- tailwind.config.ts | 23 +- utils/context/context.ts | 60 +- utils/location/location.ts | 62 + utils/text.ts | 13 + 29 files changed, 2555 insertions(+), 1137 deletions(-) create mode 100644 components/utility-components/dropdowns/category-dropdown.tsx create mode 100644 components/utility-components/search.tsx delete mode 100644 components/utility/STATIC-VARIABLES.ts create mode 100644 components/utility/STATIC-VARIABLES.tsx create mode 100644 utils/location/location.ts create mode 100644 utils/text.ts diff --git a/components/display-product-modal.tsx b/components/display-product-modal.tsx index 118e0906..00f9114e 100644 --- a/components/display-product-modal.tsx +++ b/components/display-product-modal.tsx @@ -19,15 +19,16 @@ import { import ProductForm from "./product-form"; import ImageCarousel from "./utility-components/image-carousel"; import CompactCategories from "./utility-components/compact-categories"; -import { locationAvatar } from "./utility-components/dropdowns/location-dropdown"; +import { LocationAvatar } from "./utility-components/dropdowns/location-dropdown"; import { SHOPSTRBUTTONCLASSNAMES } from "./utility/STATIC-VARIABLES"; import RequestPassphraseModal from "./utility-components/request-passphrase-modal"; import ConfirmActionDropdown from "./utility-components/dropdowns/confirm-action-dropdown"; import { getLocalStorageData } from "./utility/nostr-helper-functions"; import { ProfileWithDropdown } from "./utility-components/profile/profile-dropdown"; +import { ProductData } from "./utility/product-parser-functions"; interface ProductModalProps { - productData: any; + productData?: ProductData; handleModalToggle: () => void; showModal: boolean; handleSendMessage: (pubkeyToOpenChatWith: string) => void; @@ -43,16 +44,6 @@ export default function DisplayProductModal({ handleReviewAndPurchase, handleDelete, }: ProductModalProps) { - const { - pubkey, - createdAt, - title, - images, - categories, - location, - currency, - totalCost, - } = productData; const { signInMethod, userPubkey } = getLocalStorageData(); const [requestPassphrase, setRequestPassphrase] = React.useState(false); @@ -67,6 +58,19 @@ export default function DisplayProductModal({ return [dateString, timeString]; }; + if (!productData) return null; + + const { + pubkey, + createdAt, + title, + images, + categories, + location, + currency, + totalCost, + } = productData; + const handleShare = async () => { // The content you want to share const shareData = { @@ -143,8 +147,19 @@ export default function DisplayProductModal({ pubkey={productData.pubkey} dropDownKeys={["shop", "message"]} /> - - {location} + + {location.regionName || + location.displayName || + location.countryName}
diff --git a/components/display-products.tsx b/components/display-products.tsx index 121c1027..f8169a58 100644 --- a/components/display-products.tsx +++ b/components/display-products.tsx @@ -1,8 +1,8 @@ -import { useState, useEffect, useContext } from "react"; +import { useState, useEffect, useContext, memo } from "react"; import { Filter, SimplePool, nip19 } from "nostr-tools"; import { getLocalStorageData } from "./utility/nostr-helper-functions"; import { NostrEvent } from "../utils/types/types"; -import { ProductContext, ProfileMapContext } from "../utils/context/context"; +import { MyListingsContext, ProductContext } from "../utils/context/context"; import ProductCard from "./utility-components/product-card"; import DisplayProductModal from "./display-product-modal"; import { useRouter } from "next/router"; @@ -12,56 +12,30 @@ import { DeleteListing } from "../pages/api/nostr/crud-service"; import { Button } from "@nextui-org/react"; import { SHOPSTRBUTTONCLASSNAMES } from "./utility/STATIC-VARIABLES"; import { DateTime } from "luxon"; +import { getNameToCodeMap } from "@/utils/location/location"; +import { getKeywords } from "@/utils/text"; const DisplayEvents = ({ focusedPubkey, - selectedCategories, - selectedLocation, - selectedSearch, canShowLoadMore, + context, }: { focusedPubkey?: string; - selectedCategories: Set; - selectedLocation: string; - selectedSearch: string; canShowLoadMore?: boolean; + context: typeof ProductContext | typeof MyListingsContext; }) => { - const [productEvents, setProductEvents] = useState([]); - const [isProductsLoading, setIsProductLoading] = useState(true); - const productEventContext = useContext(ProductContext); - const profileMapContext = useContext(ProfileMapContext); - const [focusedProduct, setFocusedProduct] = useState(""); // product being viewed in modal + const productEventContext = useContext(context); + + const [isLoading, setIsLoading] = useState(false); + const [isLoadingMore, setIsLoadingMore] = useState(false); + const [focusedProduct, setFocusedProduct] = useState(); // product being viewed in modal const [showModal, setShowModal] = useState(false); - const [isLoadingMore, setIsLoadingMore] = useState(false); + const router = useRouter(); useEffect(() => { - if (!productEventContext) return; - if (!productEventContext.isLoading && productEventContext.productEvents) { - setIsProductLoading(true); - let sortedProductEvents = [ - ...productEventContext.productEvents.sort( - (a: NostrEvent, b: NostrEvent) => b.created_at - a.created_at, - ), - ]; // sorts most recently created to least recently created - let parsedProductData: ProductData[] = []; - sortedProductEvents.forEach((event) => { - let parsedData = parseTags(event); - if (parsedData) parsedProductData.push(parsedData); - }); - setProductEvents(parsedProductData); - setIsProductLoading(false); - } - }, [productEventContext]); - - const isThereAFilter = () => { - return ( - selectedCategories.size > 0 || - selectedLocation || - selectedSearch.length > 0 || - focusedPubkey - ); - }; + setIsLoading(productEventContext.isLoading); + }, [productEventContext.isLoading]); const handleDelete = async (productId: string, passphrase?: string) => { try { @@ -99,44 +73,12 @@ const DisplayEvents = ({ router.push(`/listing/${productId}`); }; - const productSatisfiesCategoryFilter = (productData: ProductData) => { - if (selectedCategories.size === 0) return true; - return Array.from(selectedCategories).some((selectedCategory) => { - const re = new RegExp(selectedCategory, "gi"); - return productData?.categories?.some((category) => { - const match = category.match(re); - return match && match.length > 0; - }); - }); - }; - - const productSatisfieslocationFilter = (productData: ProductData) => { - return !selectedLocation || productData.location === selectedLocation; - }; - - const productSatisfiesSearchFilter = (productData: ProductData) => { - if (!selectedSearch) return true; // nothing in search bar - if (!productData.title) return false; // we don't want to display it if product has no title - const re = new RegExp(selectedSearch, "gi"); - const match = productData.title.match(re); - return match && match.length > 0; - }; - - const productSatisfiesAllFilters = (productData: ProductData) => { - return ( - productSatisfiesCategoryFilter(productData) && - productSatisfieslocationFilter(productData) && - productSatisfiesSearchFilter(productData) - ); - }; - const displayProductCard = ( productData: ProductData, index: number, handleSendMessage: (pubkeyToOpenChatWith: string) => void, ) => { if (focusedPubkey && productData.pubkey !== focusedPubkey) return; - if (!productSatisfiesAllFilters(productData)) return; return ( { try { setIsLoadingMore(true); - if (productEventContext.isLoading) return; - productEventContext.isLoading = true; const oldestListing = - productEvents.length > 0 - ? productEvents[productEvents.length - 1] + productEventContext.productEvents.length > 0 + ? productEventContext.productEvents[ + productEventContext.productEvents.length - 1 + ] : null; const oldestListingCreatedAt = oldestListing ? oldestListing.createdAt @@ -166,22 +108,41 @@ const DisplayEvents = ({ ); const pool = new SimplePool(); + + const buildTagsFilters: string[] = []; + if (productEventContext.filters.categories.size > 0) { + buildTagsFilters.push( + ...Array.from(productEventContext.filters.categories), + ); + } + if (productEventContext.filters.searchQuery.length > 0) { + buildTagsFilters.push( + ...getKeywords(productEventContext.filters.searchQuery), + ); + } const filter: Filter = { kinds: [30402], since, until: oldestListingCreatedAt, + ...(productEventContext.filters.location && { + "#g": [getNameToCodeMap(productEventContext.filters.location)], + }), + ...(buildTagsFilters.length > 0 && { + "#t": buildTagsFilters, + }), }; const events = await pool.querySync(getLocalStorageData().relays, filter); events.forEach((event) => { if (event.id !== oldestListing?.id) { - productEventContext.addNewlyCreatedProductEvent(event); + const product = parseTags(event); + if (product) { + productEventContext.addNewlyCreatedProductEvents([product]); + } } }); - productEventContext.isLoading = false; setIsLoadingMore(false); } catch (err) { console.log(err); - productEventContext.isLoading = false; setIsLoadingMore(false); } }; @@ -189,28 +150,38 @@ const DisplayEvents = ({ return ( <>
- {/* DISPLAYS PRODUCT LISTINGS HERE */} -
- {productEvents.map((productData: ProductData, index) => { - return displayProductCard(productData, index, handleSendMessage); - })} -
- {profileMapContext.isLoading || - productEventContext.isLoading || - isProductsLoading || - isLoadingMore ? ( + {isLoading ? (
- ) : canShowLoadMore ? ( -
- + ) : ( +
+ {productEventContext.productEvents.map( + (productData: ProductData, index) => { + return displayProductCard( + productData, + index, + handleSendMessage, + ); + }, + )}
+ )} + {canShowLoadMore && !isLoading ? ( + isLoadingMore ? ( +
+ +
+ ) : ( +
+ +
+ ) ) : null}
([]), + productContext.filters.categories, + ); + const [selectedLocation, setSelectedLocation] = useState( + productContext.filters.location, ); - const [selectedLocation, setSelectedLocation] = useState(""); - const [selectedSearch, setSelectedSearch] = useState(""); + const [showApplyFilter, setShowApplyFilter] = useState(false); + const { isOpen, onOpen, onClose } = useDisclosure(); // Update focusedPubkey when pubkey in url changes @@ -56,6 +52,35 @@ export function MarketplacePage() { } }); + useEffect(() => { + const areSetFiltersEqual = (a: Set, b: Set) => + a.size === b.size && [...a].every((value) => b.has(value)); + + const areStringFiltersEqual = (a: string | null, b: string | null) => + a === b || (a === null && b === "") || (a === "" && b === null); + + if ( + areStringFiltersEqual( + productContext.filters.location, + selectedLocation, + ) && + areSetFiltersEqual( + productContext.filters.categories, + selectedCategories, + ) && + areStringFiltersEqual(productContext.filters.searchQuery, searchQuery) + ) { + setShowApplyFilter(false); + } else { + setShowApplyFilter(true); + } + }, [ + selectedLocation, + selectedCategories, + searchQuery, + productContext.filters, + ]); + const routeToShop = (npubkey: string) => { npubkey = encodeURIComponent(npubkey); if (npubkey === "") { @@ -65,70 +90,42 @@ export function MarketplacePage() { router.push("/" + npubkey); }; - const handleCreateNewListing = () => { - const loggedIn = isUserLoggedIn(); - if (loggedIn) { - router.push("/?addNewListing"); - } else { - onOpen(); - } + const applyFilters = () => { + productContext.setFilters({ + searchQuery: searchQuery, + categories: selectedCategories, + location: selectedLocation, + }); }; return (
-
- } - onChange={(event) => { - const value = event.target.value; - setSelectedSearch(value); - }} - > - +
+ + { - setSelectedLocation(event.target.value); - }} + selectedLocation={selectedLocation} + setSelectedLocation={setSelectedLocation} />
-
- -
+ {showApplyFilter ? ( +
+ +
+ ) : null} {focusedPubkey ? (
diff --git a/components/home/my-listings.tsx b/components/home/my-listings.tsx index 847f78d8..bd76c3c4 100644 --- a/components/home/my-listings.tsx +++ b/components/home/my-listings.tsx @@ -1,12 +1,18 @@ import router from "next/router"; -import React, { useState } from "react"; +import React, { useContext, useEffect } from "react"; import DisplayEvents from "../display-products"; import { getLocalStorageData } from "../utility/nostr-helper-functions"; import { Button, useDisclosure } from "@nextui-org/react"; import { SHOPSTRBUTTONCLASSNAMES } from "../utility/STATIC-VARIABLES"; import SignInModal from "../sign-in/SignInModal"; +import { MyListingsContext, ProductContext } from "@/utils/context/context"; +import { SimplePool, Filter } from "nostr-tools"; +import parseTags from "../utility/product-parser-functions"; export const MyListingsPage = () => { + const myListingsContext = useContext(MyListingsContext); + const productContext = useContext(ProductContext); + let usersPubkey = getLocalStorageData().userPubkey; const { isOpen, onOpen, onClose } = useDisclosure(); @@ -19,6 +25,30 @@ export const MyListingsPage = () => { onOpen(); } }; + + useEffect(() => { + try { + async function load() { + myListingsContext.setIsLoading(true); + const pool = new SimplePool(); + const filter: Filter = { + authors: [usersPubkey], + kinds: [30402], + }; + const events = await pool.querySync( + getLocalStorageData().relays, + filter, + ); + const myListings = events.map((event) => parseTags(event)); + myListingsContext.addNewlyCreatedProductEvents(myListings, true); + myListingsContext.setIsLoading(false); + } + load(); + } catch (err) { + console.log(err); + myListingsContext.setIsLoading(false); + } + }, productContext.productEvents); return (
@@ -33,9 +63,7 @@ export const MyListingsPage = () => { {usersPubkey ? ( ([])} - selectedLocation={""} - selectedSearch={""} + context={MyListingsContext} /> ) : null}
diff --git a/components/listing-page.tsx b/components/listing-page.tsx index cb3a8fe4..e60cd738 100644 --- a/components/listing-page.tsx +++ b/components/listing-page.tsx @@ -10,21 +10,6 @@ export default function ListingPage({ productData?: ProductData; }) { if (!productData) return null; - const { - createdAt, - title, - summary, - publishedAt, - images, - categories, - location, - price, - currency, - shippingType, - shippingCost, - totalCost, - } = productData; - const invoiceDisplay = () => { return (
diff --git a/components/nav-bottom.tsx b/components/nav-bottom.tsx index 8a472cb2..074b95ae 100644 --- a/components/nav-bottom.tsx +++ b/components/nav-bottom.tsx @@ -9,6 +9,7 @@ import { EnvelopeOpenIcon, ArrowLeftOnRectangleIcon, ChartBarIcon, + PlusIcon, } from "@heroicons/react/24/outline"; import { Button, DropdownItem, useDisclosure } from "@nextui-org/react"; import { countNumberOfUnreadMessagesFromChatsContext } from "@/utils/messages/utils"; @@ -24,6 +25,7 @@ import { import { useRouter } from "next/router"; import SignInModal from "./sign-in/SignInModal"; import { ProfileWithDropdown } from "./utility-components/profile/profile-dropdown"; +import { SHOPSTRBUTTONCLASSNAMES } from "./utility/STATIC-VARIABLES"; const BottomNav = () => { const { isHomeActive, isMessagesActive, isMetricsActive, isProfileActive } = @@ -68,6 +70,14 @@ const BottomNav = () => { } }; + const handleCreateNewListing = () => { + if (signedIn) { + router.push("/?addNewListing"); + } else { + onOpen(); + } + }; + return (
{ )}
+ +
); diff --git a/components/nav-side.tsx b/components/nav-side.tsx index 58cfcdb8..4b9573ed 100644 --- a/components/nav-side.tsx +++ b/components/nav-side.tsx @@ -10,7 +10,7 @@ import { ChartBarIcon, Cog6ToothIcon, ArrowLeftOnRectangleIcon, - ArrowRightOnRectangleIcon, + PlusIcon, } from "@heroicons/react/24/outline"; import { countNumberOfUnreadMessagesFromChatsContext } from "@/utils/messages/utils"; import { ChatsContext } from "@/utils/context/context"; @@ -183,6 +183,17 @@ const SideNav = () => { + Add new listing
+
+ {/* Ugly styling, TODO fix navbar styling and alignment */} + +
+
{signedIn ? (
diff --git a/components/product-form.tsx b/components/product-form.tsx index 9ff1bdb8..c7174169 100644 --- a/components/product-form.tsx +++ b/components/product-form.tsx @@ -16,10 +16,17 @@ import { SelectSection, Chip, Image, + Switch, + Divider, } from "@nextui-org/react"; import { InformationCircleIcon, TrashIcon } from "@heroicons/react/24/outline"; import Carousal from "@itseasy21/react-elastic-carousel"; -import { SHOPSTRBUTTONCLASSNAMES } from "./utility/STATIC-VARIABLES"; +import { + CURRENCY_OPTIONS, + CurrencyType, + SHOPSTRBUTTONCLASSNAMES, + ShippingOptionsType, +} from "./utility/STATIC-VARIABLES"; import { PostListing, @@ -35,10 +42,16 @@ import ConfirmActionDropdown from "./utility-components/dropdowns/confirm-action import { ProductContext } from "../utils/context/context"; import { capturePostListingMetric } from "./utility/metrics-helper-functions"; import { addProductToCache } from "../pages/api/nostr/cache-service"; -import { ProductData } from "./utility/product-parser-functions"; +import parseTags, { ProductData } from "./utility/product-parser-functions"; import { ProductFormValues } from "@/pages/api/nostr/post-event"; import { buildSrcSet } from "@/utils/images"; import { FileUploaderButton } from "./utility-components/file-uploader"; +import { + buildListingGeotags, + getNameToCodeMap, +} from "@/utils/location/location"; +import CategoryDropdown from "./utility-components/dropdowns/category-dropdown"; +import { getKeywords } from "@/utils/text"; declare global { interface Window { @@ -55,6 +68,18 @@ interface ProductFormProps { onSubmitCallback?: () => void; } +type ProductFormData = { + "Product Name": string; + Description: string; + Price: string; + Currency: CurrencyType; + Location: string; + "Shipping Option": ShippingOptionsType; + "Shipping Cost": number; + Categories: Set; + "Content Warning": boolean; +}; + export default function NewForm({ showModal, handleModalToggle, @@ -76,22 +101,18 @@ export default function NewForm({ control, reset, watch, - } = useForm({ - defaultValues: oldValues - ? { - "Product Name": oldValues.title, - Description: oldValues.summary, - Price: String(oldValues.price), - Currency: oldValues.currency, - Location: oldValues.location, - "Shipping Option": oldValues.shippingType, - "Shipping Cost": oldValues.shippingCost, - Category: oldValues.categories ? oldValues.categories.join(",") : "", - } - : { - Currency: "SATS", - "Shipping Option": "N/A", - }, + } = useForm({ + defaultValues: { + "Product Name": oldValues?.title || "", + Description: oldValues?.summary || "", + Price: String(oldValues?.price || 0), + Currency: oldValues?.currency || "SATS", + Location: oldValues?.location.displayName, + "Shipping Option": oldValues?.shippingType, + "Shipping Cost": oldValues?.shippingCost || 0, + Categories: oldValues?.categories || [], + "Content Warning": oldValues?.warning || false, + }, }); useEffect(() => { @@ -107,68 +128,105 @@ export default function NewForm({ setIsEdit(oldValues ? true : false); }, [showModal]); - const onSubmit = async (data: { [x: string]: string }) => { - setIsPostingOrUpdatingProduct(true); - const encoder = new TextEncoder(); - const dataEncoded = encoder.encode(data["Product Name"]); - const hashBuffer = await crypto.subtle.digest("SHA-256", dataEncoded); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - const hashHex = hashArray - .map((b) => b.toString(16).padStart(2, "0")) - .join(""); - - let tags: ProductFormValues = [ - ["d", oldValues?.d || hashHex], - ["alt", "Classified listing: " + data["Product Name"]], - [ - "client", - "Shopstr", - "31990:" + pubkey + ":" + (oldValues?.d || hashHex), - "wss://relay.damus.io", - ], - ["title", data["Product Name"]], - ["summary", data["Description"]], - ["price", data["Price"], data["Currency"]], - ["location", data["Location"]], - [ - "shipping", - data["Shipping Option"], - data["Shipping Cost"] ? data["Shipping Cost"] : "0", - data["Currency"], - ], - ]; - - images.forEach((image) => { - tags.push(["image", image]); - }); + const onSubmit = async (data: ProductFormData) => { + try { + setIsPostingOrUpdatingProduct(true); + const encoder = new TextEncoder(); + const dataEncoded = encoder.encode(data["Product Name"]); + const hashBuffer = await crypto.subtle.digest("SHA-256", dataEncoded); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); - data["Category"].split(",").forEach((category) => { - tags.push(["t", category]); - }); - let newListing = await PostListing(tags, passphrase); + let tags: ProductFormValues = [ + ["d", oldValues?.d || hashHex], + ["alt", "Classified listing: " + data["Product Name"]], + [ + "client", + "Shopstr", + "31990:" + pubkey + ":" + (oldValues?.d || hashHex), + "wss://relay.damus.io", + ], + ["title", data["Product Name"]], + ["summary", data["Description"]], + ["price", data["Price"], data["Currency"]], + ["location", data["Location"]], + [ + "shipping", + data["Shipping Option"].toString(), + data["Shipping Cost"] ? data["Shipping Cost"].toString() : "0", + data["Currency"], + ], + ]; + + const locationCode = getNameToCodeMap(data["Location"]); + tags.push(...buildListingGeotags({ iso3166: locationCode })); + + images.forEach((image) => { + tags.push(["image", image]); + }); - capturePostListingMetric(newListing.id, tags); + data["Categories"].forEach((category) => { + tags.push(["t", category, "category"]); + }); - if (isEdit) { - if (handleDelete && oldValues?.id) { - handleDelete(oldValues.id, passphrase); + // Relay search (NIP-50) not widespread enough, use tags instead for relay querying + getKeywords(data["Product Name"] + " " + data["Description"]).forEach( + (keyword) => { + tags.push(["t", keyword, "keyword"]); + }, + ); + + if (data["Content Warning"] === true) { + tags.push( + ["L", "content-warning"], + ["l", "n/a", "content-warning"], + ["content-warning", "n/a"], + ); + } + + console.log("generated tags for new listing:", tags); + + let newListing = await PostListing(tags, passphrase); + + capturePostListingMetric(newListing.id, tags); + + if (isEdit) { + if (handleDelete && oldValues?.id) { + handleDelete(oldValues.id, passphrase); + } } - } - clear(); - productEventContext.addNewlyCreatedProductEvent(newListing); - addProductToCache(newListing); - setIsPostingOrUpdatingProduct(false); - if (onSubmitCallback) { - onSubmitCallback(); + const productEvent = parseTags(newListing); + + clear(); + handleModalToggle(); + productEventContext.addNewlyCreatedProductEvents([productEvent]); + addProductToCache(productEvent); + setIsPostingOrUpdatingProduct(false); + if (onSubmitCallback) { + onSubmitCallback(); + } + } catch (err) { + console.log("Error submiting listing", err); } }; const clear = () => { - handleModalToggle(); setPassphrase(""); setImages([]); - reset(); + reset({ + "Product Name": "", + Description: "", + Price: "", + Currency: "SATS", + Location: "", + "Shipping Option": "N/A", + "Shipping Cost": 0, + Categories: new Set(), + "Content Warning": false, + }); }; const watchShippingOption = watch("Shipping Option"); // acts as state for shippingOption input. when shippingOption changes, this variable changes as well @@ -223,10 +281,24 @@ export default function NewForm({ > - Add New Product Listing + Add a New Product Listing
+ {signIn === "nsec" && ( + setPassphrase(e.target.value)} + value={passphrase} + /> + )} + { + isMultiple={true} + imgCallbackOnUpload={(imgUrls) => { setImages((prevValues) => { const updatedImages = [...prevValues]; - console.log("imgUrl", imgUrl); - if (imgUrl && imgUrl.length > 0) { - return [...updatedImages, imgUrl]; + console.log("imgUrl", imgUrls); + if (imgUrls && imgUrls.length > 0) { + return [...updatedImages, ...imgUrls]; } return [...updatedImages]; }); }} > - {isButtonDisabled ? "Enter your passphrase!" : "Upload Images"} + {isButtonDisabled + ? "Enter your passphrase to upload images!" + : "Upload Images"} - - - {/* */} + {CURRENCY_OPTIONS.map((currencyOption) => { + return ( + + ); + })}
); @@ -437,16 +515,14 @@ export default function NewForm({ let errorMessage: string = error?.message ? error.message : ""; return ( { + onChange(key); + }} isInvalid={isErrored} errorMessage={errorMessage} // controller props - onChange={onChange} // send value to hook form onBlur={onBlur} // notify when input is touched/blur - value={value} /> ); }} @@ -467,7 +543,6 @@ export default function NewForm({ return (
} @@ -542,7 +620,7 @@ export default function NewForm({ /> )} (value)} + // controller props isInvalid={isErrored} errorMessage={errorMessage} - // controller props - onChange={onChange} // send value to hook form - onBlur={onBlur} // notify when input is touched/blur - value={value} - defaultSelectedKeys={value ? value.split(",") : ""} - classNames={{ - base: "mt-4", - trigger: "min-h-unit-12 py-2", - }} - renderValue={(items) => { - return ( -
- {items.map((item) => ( - - {item.key - ? (item.key as string) - : "unknown category"} - - ))} -
- ); + onBlur={onBlur} + onChange={(event: { target: { value: string } }) => { + if (event.target.value === "") { + onChange(new Set([])); + } else { + onChange( + new Set(event.target.value.split(",")), + ); + } }} + > + ); + }} + /> + + { + return ( + - - {CATEGORIES.map((category) => ( - - {category} - - ))} - - +
+ {value === true + ? "Show Content Warning" + : "Don't Show Content Warning"} +
+
); }} /> - {signIn === "nsec" && ( - setPassphrase(e.target.value)} - value={passphrase} - /> - )} +

@@ -623,7 +689,7 @@ export default function NewForm({ Cashu {" "} - token that you can redeem for Bitcoin. + token that is redeemable to Bitcoin.

diff --git a/components/utility-components/compact-categories.tsx b/components/utility-components/compact-categories.tsx index 0348b93f..33a5fb35 100644 --- a/components/utility-components/compact-categories.tsx +++ b/components/utility-components/compact-categories.tsx @@ -2,12 +2,18 @@ import React from "react"; import { CATEGORIES } from "../utility/STATIC-VARIABLES"; import { Chip, Tooltip } from "@nextui-org/react"; -const CompactCategories = ({ categories }: { categories: string[] }) => { +const SHOPSTR_CATEGORIES = CATEGORIES.map(({ name }) => name); + +const CompactCategories = ({ categories }: { categories: Set }) => { const [isOpen, setIsOpen] = React.useState(false); - const validCategories = categories - ?.filter((category) => CATEGORIES.includes(category)) - .sort((a, b) => b.length - a.length); // sort by longest to shortest to avoid styling bugs of categories jumping around + const validCategories: string[] = []; + categories.forEach((category) => { + if (SHOPSTR_CATEGORIES.includes(category)) { + validCategories.push(category); + } + }); + validCategories.sort((a, b) => b.length - a.length); // sort by longest to shortest to avoid styling bugs of categories jumping around const categoryChips = validCategories?.map((category, index) => { return {category}; diff --git a/components/utility-components/dropdowns/category-dropdown.tsx b/components/utility-components/dropdowns/category-dropdown.tsx new file mode 100644 index 00000000..e2f0dc6e --- /dev/null +++ b/components/utility-components/dropdowns/category-dropdown.tsx @@ -0,0 +1,43 @@ +import React, { Dispatch, SetStateAction, useMemo } from "react"; +import { Select, SelectItem, SelectSection } from "@nextui-org/react"; +import { CATEGORIES } from "@/components/utility/STATIC-VARIABLES"; +import { Squares2X2Icon } from "@heroicons/react/24/outline"; + +export const CategoryDropdown = ({ + selectedCategories, + setSelectedCategories, + ...props +}: { + selectedCategories: Set; + setSelectedCategories?: Dispatch>>; +} & { [key: string]: any }) => { + return ( + + ); +}; + +export default CategoryDropdown; diff --git a/components/utility-components/dropdowns/location-dropdown.tsx b/components/utility-components/dropdowns/location-dropdown.tsx index 567b6487..8c361d93 100644 --- a/components/utility-components/dropdowns/location-dropdown.tsx +++ b/components/utility-components/dropdowns/location-dropdown.tsx @@ -1,101 +1,85 @@ -import React, { useMemo } from "react"; -import { Select, SelectItem, SelectSection, Avatar } from "@nextui-org/react"; +// @ts-nocheck +import React, { Dispatch, SetStateAction, useMemo } from "react"; +import { + Avatar, + Autocomplete, + AutocompleteItem, + AutocompleteSection, +} from "@nextui-org/react"; import locations from "../../../public/locationSelection.json"; +import { PiMapPinBold } from "react-icons/pi"; +import { getNameToCodeMap } from "@/utils/location/location"; -export const locationAvatar = (location: string) => { - const getLocationMap = () => { - let countries = locations.countries.map( - (country) => [country.country, country.iso3166] as const, - ); - let states = locations.states.map( - (state) => [state.state, state.iso3166] as const, - ); - return new Map([...countries, ...states]); - }; - const locationMap = getLocationMap(); - return locationMap.get(location) ? ( +export const LocationAvatar = ({ + name, + iso3166, +}: { + name?: string; + iso3166?: string; +}) => { + if (!name && !iso3166) { + return null; + } + const code = iso3166 || getNameToCodeMap(name as string); + if (code == null) { + return null; + } + + return ( - ) : null; + ); }; -const LocationDropdown = ({ value, ...props }: { [x: string]: any }) => { - const locationOptions = useMemo(() => { - const headingClasses = - "flex w-full sticky top-1 z-20 py-1.5 px-2 dark:bg-dark-bg bg-light-bg shadow-small rounded-small"; - - let countryOptions = ( - - {locations.countries.map((country, index) => { - return ( - - // } - > - {country.country} - - ); - })} - - ); - - let stateOptions = ( - - {locations.states.map((state, index) => { - return ( - - // } - > - {state.state} - - ); - })} - - ); - return [stateOptions, countryOptions]; - }, []); - +const LocationDropdown = ({ + selectedLocation, + setSelectedLocation, + ...props +}: { + selectedLocation: string | null; + setSelectedLocation?: Dispatch>; +} & { [key: string]: any }) => { + console.log("hmm selectedLocation ", selectedLocation); return ( - + {({ section, values }) => ( + + {({ name, iso3166 }) => ( + + } + key={name} + > + {name} + + )} + + )} + ); }; diff --git a/components/utility-components/file-uploader.tsx b/components/utility-components/file-uploader.tsx index c9880a51..704a2909 100644 --- a/components/utility-components/file-uploader.tsx +++ b/components/utility-components/file-uploader.tsx @@ -26,7 +26,7 @@ export const FileUploaderButton = ({ className: any; children: React.ReactNode; passphrase: string; - imgCallbackOnUpload: (imgUrl: string) => void; + imgCallbackOnUpload: (imgUrls: string[]) => void; isMultiple?: boolean; }) => { const [loading, setLoading] = useState(false); @@ -56,8 +56,8 @@ export const FileUploaderButton = ({ ); } const imageUrls = response?.map((i) => i.url); - if (imageUrls && imageUrls[0]) { - imgCallbackOnUpload(imageUrls[0]); + if (imageUrls && imageUrls.length > 0) { + imgCallbackOnUpload(imageUrls); } else { alert("Image upload failed to yield img URL"); } @@ -98,6 +98,7 @@ export const FileUploaderButton = ({
- - {location} + + {location.regionName || + location.displayName || + location.countryName}
@@ -97,6 +108,15 @@ export default function ProductCard({ const cardHoverStyle = "hover:shadow-lg hover:shadow-shopstr-purple dark:hover:shadow-shopstr-yellow"; + const getTrimmedLocationName = (location: ProductData["location"]) => { + const name = + location.regionName || location.displayName || location.countryName; + if (name.length > 20) { + return name.slice(0, 20) + "..."; + } + return name; + }; + return (
- - {location - ? location.length > 20 - ? location.slice(0, 20) + "..." - : location - : ""} + + {getTrimmedLocationName(location)}
diff --git a/components/utility-components/search.tsx b/components/utility-components/search.tsx new file mode 100644 index 00000000..8255d8f5 --- /dev/null +++ b/components/utility-components/search.tsx @@ -0,0 +1,38 @@ +import React, { Dispatch, SetStateAction } from "react"; +import { Input } from "@nextui-org/react"; +import { + MagnifyingGlassIcon, +} from "@heroicons/react/24/outline"; +import CategoryDropdown from "./dropdowns/category-dropdown"; + +export const Search = ({ + searchQuery, + setSearchQuery, + ...props +}: { + searchQuery: string; + setSearchQuery?: Dispatch>; +} & { [key: string]: any }) => { + return ( + } + onChange={(event) => { + if (!setSearchQuery) return; + const value = event.target.value; + setSearchQuery(value); + }} + onClear={() => { + if (!setSearchQuery) return; + setSearchQuery(""); + }} + {...props} + > + ); +}; + +export default CategoryDropdown; diff --git a/components/utility/STATIC-VARIABLES.ts b/components/utility/STATIC-VARIABLES.ts deleted file mode 100644 index d6581079..00000000 --- a/components/utility/STATIC-VARIABLES.ts +++ /dev/null @@ -1,40 +0,0 @@ -export const CATEGORIES = [ - "Digital", - "Physical", - "Services", - "Resale", - "Exchange/swap", - "Clothing", - "Shoes", - "Accessories", - "Electronics", - "Collectibles", - "Books", - "Pets", - "Sports", - "Fitness", - "Art", - "Crafts", - "Home", - "Office", - "Food", - "Miscellaneous", -]; - -export type ShippingOptionsType = - | "N/A" - | "Free" - | "Pickup" - | "Free/Pickup" - | "Added Cost"; - -export const SHIPPING_OPTIONS = [ - "N/A", - "Free", // free shipping you are going to ship it - "Pickup", // you are only going to have someone pick it up - "Free/Pickup", // you are open to do either - "Added Cost", // you are going to charge for shipping -]; - -export const SHOPSTRBUTTONCLASSNAMES = - "text-dark-text dark:text-light-text shadow-lg bg-gradient-to-tr from-shopstr-purple via-shopstr-purple-light to-shopstr-purple min-w-fit dark:from-shopstr-yellow dark:via-shopstr-yellow-light dark:to-shopstr-yellow"; diff --git a/components/utility/STATIC-VARIABLES.tsx b/components/utility/STATIC-VARIABLES.tsx new file mode 100644 index 00000000..fe5403eb --- /dev/null +++ b/components/utility/STATIC-VARIABLES.tsx @@ -0,0 +1,59 @@ +import { FaNetworkWired } from "react-icons/fa"; +import { MdOutlineMedicalServices } from "react-icons/md"; +import { LuShirt } from "react-icons/lu"; +import { MdOutlinePhoneIphone } from "react-icons/md"; +import { GrDiamond } from "react-icons/gr"; +import { PiBookOpen } from "react-icons/pi"; +import { FaPaw } from "react-icons/fa"; +import { MdOutlineSportsSoccer } from "react-icons/md"; +import { FaPaintBrush } from "react-icons/fa"; +import { GiStoneCrafting } from "react-icons/gi"; +import { RiHomeHeartLine } from "react-icons/ri"; +import { PiOfficeChairBold } from "react-icons/pi"; +import { CiWheat } from "react-icons/ci"; +import { FaTag } from "react-icons/fa"; +import { FaExchangeAlt } from "react-icons/fa"; +import { LuTag } from "react-icons/lu"; +import { FaPeopleArrows } from "react-icons/fa"; + +export const CATEGORIES = [ + { name: "Digital", icon: }, + { name: "Physical", icon: }, + { name: "Clothing", icon: }, + { name: "Electronics", icon: }, + { name: "Collectibles", icon: }, + { name: "Books", icon: }, + { name: "Pets", icon: }, + { name: "Sports", icon: }, + { name: "Art", icon: }, + { name: "Crafts", icon: }, + { name: "Home", icon: }, + { name: "Office", icon: }, + { name: "Food", icon: }, + { name: "Services", icon: }, + { name: "Resale", icon: }, + { name: "Exchange/swap", icon: }, + { name: "Miscellaneous", icon: }, +]; + +export type CurrencyType = "SATS" | "USD"; + +export const CURRENCY_OPTIONS: CurrencyType[] = ["SATS", "USD"]; + +export type ShippingOptionsType = + | "N/A" + | "Free" + | "Pickup" + | "Free/Pickup" + | "Added Cost"; + +export const SHIPPING_OPTIONS: ShippingOptionsType[] = [ + "N/A", + "Free", // free shipping you are going to ship it + "Pickup", // you are only going to have someone pick it up + "Free/Pickup", // you are open to do either + "Added Cost", // you are going to charge for shipping +]; + +export const SHOPSTRBUTTONCLASSNAMES = + "text-dark-text dark:text-light-text shadow-lg bg-gradient-to-tr from-shopstr-purple via-shopstr-purple-light to-shopstr-purple min-w-fit dark:from-shopstr-yellow dark:via-shopstr-yellow-light dark:to-shopstr-yellow"; diff --git a/components/utility/nostr-helper-functions.ts b/components/utility/nostr-helper-functions.ts index 8eea96e6..2b500551 100644 --- a/components/utility/nostr-helper-functions.ts +++ b/components/utility/nostr-helper-functions.ts @@ -14,7 +14,7 @@ import { ProductFormValues } from "@/pages/api/nostr/post-event"; export async function PostListing( values: ProductFormValues, passphrase: string, -) { +): Promise { const { signInMethod, userPubkey, relays } = getLocalStorageData(); const summary = values.find(([key]) => key === "summary")?.[1] || ""; @@ -94,6 +94,7 @@ export async function PostListing( kind: 30402, tags: updatedValues, content: summary, + sig: res.data.sig, }; } } diff --git a/components/utility/product-parser-functions.tsx b/components/utility/product-parser-functions.tsx index a60334f4..7746b5fa 100644 --- a/components/utility/product-parser-functions.tsx +++ b/components/utility/product-parser-functions.tsx @@ -1,4 +1,4 @@ -import { ShippingOptionsType } from "./STATIC-VARIABLES"; +import { CurrencyType, ShippingOptionsType } from "./STATIC-VARIABLES"; import { calculateTotalCost } from "../utility-components/display-monetary-info"; import { NostrEvent } from "@/utils/types/types"; @@ -10,14 +10,21 @@ export type ProductData = { summary: string; publishedAt: string; images: string[]; - categories: string[]; - location: string; + categories: Set; + location: { + displayName: string; + countryName: string; + countryCode: string; + regionName: string; + regionCode: string; + }; price: number; - currency: string; + currency: CurrencyType; shippingType?: ShippingOptionsType; shippingCost?: number; totalCost: number; d?: string; + warning: boolean; }; export const parseTags = (productEvent: NostrEvent) => { @@ -29,17 +36,23 @@ export const parseTags = (productEvent: NostrEvent) => { summary: "", publishedAt: "", images: [], - categories: [], - location: "", + categories: new Set(), + location: { + displayName: "", + countryName: "", + countryCode: "", + regionName: "", + regionCode: "", + }, price: 0, - currency: "", + currency: "SATS", totalCost: 0, + warning: false, }; parsedData.pubkey = productEvent.pubkey; parsedData.id = productEvent.id; parsedData.createdAt = productEvent.created_at; const tags = productEvent.tags; - if (tags === undefined) return; tags.forEach((tag) => { const [key, ...values] = tag; switch (key) { @@ -57,16 +70,28 @@ export const parseTags = (productEvent: NostrEvent) => { parsedData.images.push(values[0]); break; case "t": - if (parsedData.categories === undefined) parsedData.categories = []; - parsedData.categories.push(values[0]); + if (parsedData.categories === undefined) + parsedData.categories = new Set(); + parsedData.categories.add(values[0]); break; case "location": - parsedData.location = values[0]; + parsedData.location.displayName = values[0]; + break; + case "g": + if (values[1] === "countryName") { + parsedData.location.countryName = values[0]; + } else if (values[1] === "countryCode") { + parsedData.location.countryCode = values[0]; + } else if (values[1] === "regionName") { + parsedData.location.regionName = values[0]; + } else if (values[1] === "regionCode") { + parsedData.location.regionCode = values[0]; + } break; case "price": const [amount, currency] = values; parsedData.price = Number(amount); - parsedData.currency = currency; + parsedData.currency = currency as CurrencyType; break; case "shipping": if (values.length === 3) { @@ -93,6 +118,9 @@ export const parseTags = (productEvent: NostrEvent) => { case "d": parsedData.d = values[0]; break; + case "content-warning": + parsedData.warning = values[0].length > 0; + break; default: return; } diff --git a/package-lock.json b/package-lock.json index 772303f6..ece5102e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@getalby/lightning-tools": "^5.0.1", "@heroicons/react": "^2.0.18", "@itseasy21/react-elastic-carousel": "^0.12.3", - "@nextui-org/react": "^2.2.9", + "@nextui-org/react": "^2.2.10", "@tremor/react": "^3.13.4", "autoprefixer": "10.4.14", "axios": "^1.6.0", @@ -24,6 +24,7 @@ "eslint-config-next": "13.3.0", "fast-geoip": "^1.1.88", "framer-motion": "^10.16.4", + "keyword-extractor": "^0.0.28", "knex": "^3.1.0", "luxon": "^3.4.4", "next": "^13.5.4", @@ -36,6 +37,7 @@ "qrcode": "^1.5.3", "react": "18.2.0", "react-hook-form": "^7.47.0", + "react-icons": "^5.0.1", "react-responsive-carousel": "^3.2.23", "tailwindcss": "3.3.1", "uuid": "^9.0.0" @@ -2422,20 +2424,20 @@ } }, "node_modules/@nextui-org/autocomplete": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@nextui-org/autocomplete/-/autocomplete-2.0.9.tgz", - "integrity": "sha512-ViPXrZnP35k7LF+TBA4w8nqu0OEj9p1z9Rt7rwrACmY2VmDGY6h6a6nDCMjhuTVXptftRvzxfIPsIyzBYqxb0g==", + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@nextui-org/autocomplete/-/autocomplete-2.0.10.tgz", + "integrity": "sha512-nQr8VC5RtpjnPef1qXgjNxRAw8JbN6q5qIFtsHWOCzvvn5jGAtdxkAkNE4C7DTvlMWZkIlEuR4DyAmFfY8CChQ==", "dependencies": { "@nextui-org/aria-utils": "2.0.15", - "@nextui-org/button": "2.0.26", - "@nextui-org/input": "2.1.16", + "@nextui-org/button": "2.0.27", + "@nextui-org/input": "2.1.17", "@nextui-org/listbox": "2.1.16", - "@nextui-org/popover": "2.1.14", + "@nextui-org/popover": "2.1.15", "@nextui-org/react-utils": "2.0.10", - "@nextui-org/scroll-shadow": "2.1.12", + "@nextui-org/scroll-shadow": "2.1.13", "@nextui-org/shared-icons": "2.0.6", "@nextui-org/shared-utils": "2.0.4", - "@nextui-org/spinner": "2.0.24", + "@nextui-org/spinner": "2.0.25", "@nextui-org/use-aria-button": "2.0.6", "@react-aria/combobox": "^3.7.1", "@react-aria/focus": "^3.14.3", @@ -2511,14 +2513,14 @@ } }, "node_modules/@nextui-org/button": { - "version": "2.0.26", - "resolved": "https://registry.npmjs.org/@nextui-org/button/-/button-2.0.26.tgz", - "integrity": "sha512-mDrSII1oneY4omwDdxUhl5oLa3AhoWCchwV/jt7egunnAFie32HbTqfFYGpLGiJw3JMMh3WDUthrI1islVTRKA==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/@nextui-org/button/-/button-2.0.27.tgz", + "integrity": "sha512-oErzUr9KtE/qjUx4dSbalphxURssxGf9tv0mW++ZMkmVX1E6i887FwZb9xAVm9oBwYwR6+xpJaqjQLmt8aN/rQ==", "dependencies": { "@nextui-org/react-utils": "2.0.10", "@nextui-org/ripple": "2.0.24", "@nextui-org/shared-utils": "2.0.4", - "@nextui-org/spinner": "2.0.24", + "@nextui-org/spinner": "2.0.25", "@nextui-org/use-aria-button": "2.0.6", "@react-aria/button": "^3.8.4", "@react-aria/focus": "^3.14.3", @@ -2636,12 +2638,12 @@ } }, "node_modules/@nextui-org/dropdown": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@nextui-org/dropdown/-/dropdown-2.1.16.tgz", - "integrity": "sha512-3KINNvC7Cz+deQltCM8gaB7iJCfU4Qsp1fwnoy1wUEjeZhEtPOPR59oTyqT+gPaPIisP1+LLOfcqRl4jNQoVXw==", + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/@nextui-org/dropdown/-/dropdown-2.1.17.tgz", + "integrity": "sha512-Hxmz1Yf/LjjOLqWRF49Q5ZYJtae6ydDEk1mv8oMKNmSWHi92lrgmHlwkGvR3mjczbRuF+WkXHLEhVZH6/tZQ7A==", "dependencies": { "@nextui-org/menu": "2.0.17", - "@nextui-org/popover": "2.1.14", + "@nextui-org/popover": "2.1.15", "@nextui-org/react-utils": "2.0.10", "@nextui-org/shared-utils": "2.0.4", "@react-aria/focus": "^3.14.3", @@ -2689,9 +2691,9 @@ } }, "node_modules/@nextui-org/input": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@nextui-org/input/-/input-2.1.16.tgz", - "integrity": "sha512-nUTlAvsXj5t88ycvQdICxf78/pko6Wznx2OomvYjb3E45eb77twQcWUDhydkJCWIh3b4AhGHSMM6GYxwWUgMDA==", + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/@nextui-org/input/-/input-2.1.17.tgz", + "integrity": "sha512-3FW3NDDbQOa5IlUCpO2Ma/XEjGnx4TQLM8MvMbskc+GNbZ0mtzfV0hCeQkqxxJ2lP4Mkp4QhwGRRkRrDu1G0Wg==", "dependencies": { "@nextui-org/react-utils": "2.0.10", "@nextui-org/shared-icons": "2.0.6", @@ -2803,9 +2805,9 @@ } }, "node_modules/@nextui-org/modal": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/@nextui-org/modal/-/modal-2.0.28.tgz", - "integrity": "sha512-unfP0EMF3FDg5CkRqou03s4/BopWbaBTeVIMZeA2A1WF5teHUOmpLdp44Z1KOoWB1RVMDVd4JeoauNHNhJMp0g==", + "version": "2.0.29", + "resolved": "https://registry.npmjs.org/@nextui-org/modal/-/modal-2.0.29.tgz", + "integrity": "sha512-C/pvw0fAPWKbfMoGfIVZWhMRbe+DRGEg7GqPVY7EmW4FSSIK7Sfdn6Jxm+sSv+a7xHpDr86nirFbvN3S4jCaHw==", "dependencies": { "@nextui-org/framer-transitions": "2.0.15", "@nextui-org/react-utils": "2.0.10", @@ -2858,16 +2860,17 @@ } }, "node_modules/@nextui-org/pagination": { - "version": "2.0.26", - "resolved": "https://registry.npmjs.org/@nextui-org/pagination/-/pagination-2.0.26.tgz", - "integrity": "sha512-OVpkpXqUKRuMRIcYESBAL95d3pqZ17SKAyNINMiJ/DwWnrzJu/LXGmFwTuYRoBdqHFlm7guGqZbHmAkcS/Fgow==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/@nextui-org/pagination/-/pagination-2.0.27.tgz", + "integrity": "sha512-v1tSsb0Q863/gKVUxuN7FcE1TZWuvcbWZOrWjKe0/llRgfZ23/4KD1AmFyYuKo5RDFt+i1JWSfzAu08j0Hzzqg==", "dependencies": { "@nextui-org/react-utils": "2.0.10", "@nextui-org/shared-icons": "2.0.6", "@nextui-org/shared-utils": "2.0.4", "@nextui-org/use-aria-press": "2.0.1", - "@nextui-org/use-pagination": "2.0.4", + "@nextui-org/use-pagination": "2.0.5", "@react-aria/focus": "^3.14.3", + "@react-aria/i18n": "^3.8.4", "@react-aria/interactions": "^3.19.1", "@react-aria/utils": "^3.21.1", "scroll-into-view-if-needed": "3.0.10" @@ -2880,16 +2883,17 @@ } }, "node_modules/@nextui-org/popover": { - "version": "2.1.14", - "resolved": "https://registry.npmjs.org/@nextui-org/popover/-/popover-2.1.14.tgz", - "integrity": "sha512-fqqktFQ/chIBS9Y3MghL6KX6qAy3hodtXUDchnxLa1GL+oi6TCBLUjo+wgI5EMJrTTbqo/eFLui/Ks00JfCj+A==", + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@nextui-org/popover/-/popover-2.1.15.tgz", + "integrity": "sha512-FQ66y49sQvXvyDrEsEFAC0qfpl2X+5ZPGaVXdNd3Cjox/jxAxp93cSUkk0iOfYvdsbO5zVFjuM0L3Dqn4hsHMw==", "dependencies": { "@nextui-org/aria-utils": "2.0.15", - "@nextui-org/button": "2.0.26", + "@nextui-org/button": "2.0.27", "@nextui-org/framer-transitions": "2.0.15", "@nextui-org/react-utils": "2.0.10", "@nextui-org/shared-utils": "2.0.4", "@nextui-org/use-aria-button": "2.0.6", + "@nextui-org/use-safe-layout-effect": "2.0.4", "@react-aria/dialog": "^3.5.7", "@react-aria/focus": "^3.14.3", "@react-aria/interactions": "^3.19.1", @@ -2909,9 +2913,9 @@ } }, "node_modules/@nextui-org/progress": { - "version": "2.0.24", - "resolved": "https://registry.npmjs.org/@nextui-org/progress/-/progress-2.0.24.tgz", - "integrity": "sha512-RPVsFCF8COFClS/8PqEepzryhDFtIcJGQLu/P+qAr7jIDlXizXaBDrp0X34GVtQsapNeE9ExxX9Kt+QIspuHHQ==", + "version": "2.0.25", + "resolved": "https://registry.npmjs.org/@nextui-org/progress/-/progress-2.0.25.tgz", + "integrity": "sha512-EFVxwT0CXq+2scPLhKKRHkWb6xNa6Vjx+HdgSg3l4lgAxAUryvdfksjW8vjxn6x4I2rGbdzAYPEu27p2KaK7jg==", "dependencies": { "@nextui-org/react-utils": "2.0.10", "@nextui-org/shared-utils": "2.0.4", @@ -2953,48 +2957,48 @@ } }, "node_modules/@nextui-org/react": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/@nextui-org/react/-/react-2.2.9.tgz", - "integrity": "sha512-QHkUQTxI9sYoVjrvTpYm5K68pMDRqD13+DVzdsrkJuETGhbvE2c2CCGc4on9EwXC3JsOxuP/OyqaAmOIuHhYkA==", + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/@nextui-org/react/-/react-2.2.10.tgz", + "integrity": "sha512-YJhUIeLnO/FGDbZgfeWEz32RBrH2YFA1qsJQtMF7mza8rjspX/CkankvI7xs1o6sW/TYLSTq7sOF9RGMxLTIAA==", "dependencies": { "@nextui-org/accordion": "2.0.28", - "@nextui-org/autocomplete": "2.0.9", + "@nextui-org/autocomplete": "2.0.10", "@nextui-org/avatar": "2.0.24", "@nextui-org/badge": "2.0.24", "@nextui-org/breadcrumbs": "2.0.4", - "@nextui-org/button": "2.0.26", + "@nextui-org/button": "2.0.27", "@nextui-org/card": "2.0.24", "@nextui-org/checkbox": "2.0.25", "@nextui-org/chip": "2.0.25", "@nextui-org/code": "2.0.24", "@nextui-org/divider": "2.0.25", - "@nextui-org/dropdown": "2.1.16", + "@nextui-org/dropdown": "2.1.17", "@nextui-org/image": "2.0.24", - "@nextui-org/input": "2.1.16", + "@nextui-org/input": "2.1.17", "@nextui-org/kbd": "2.0.25", "@nextui-org/link": "2.0.26", "@nextui-org/listbox": "2.1.16", "@nextui-org/menu": "2.0.17", - "@nextui-org/modal": "2.0.28", + "@nextui-org/modal": "2.0.29", "@nextui-org/navbar": "2.0.27", - "@nextui-org/pagination": "2.0.26", - "@nextui-org/popover": "2.1.14", - "@nextui-org/progress": "2.0.24", + "@nextui-org/pagination": "2.0.27", + "@nextui-org/popover": "2.1.15", + "@nextui-org/progress": "2.0.25", "@nextui-org/radio": "2.0.25", "@nextui-org/ripple": "2.0.24", - "@nextui-org/scroll-shadow": "2.1.12", - "@nextui-org/select": "2.1.20", + "@nextui-org/scroll-shadow": "2.1.13", + "@nextui-org/select": "2.1.21", "@nextui-org/skeleton": "2.0.24", - "@nextui-org/slider": "2.2.5", - "@nextui-org/snippet": "2.0.30", + "@nextui-org/slider": "2.2.6", + "@nextui-org/snippet": "2.0.31", "@nextui-org/spacer": "2.0.24", - "@nextui-org/spinner": "2.0.24", + "@nextui-org/spinner": "2.0.25", "@nextui-org/switch": "2.0.25", "@nextui-org/system": "2.0.15", "@nextui-org/table": "2.0.28", "@nextui-org/tabs": "2.0.26", - "@nextui-org/theme": "2.1.17", - "@nextui-org/tooltip": "2.0.29", + "@nextui-org/theme": "2.1.18", + "@nextui-org/tooltip": "2.0.30", "@nextui-org/user": "2.0.25", "@react-aria/visually-hidden": "^3.8.6" }, @@ -3038,13 +3042,13 @@ } }, "node_modules/@nextui-org/scroll-shadow": { - "version": "2.1.12", - "resolved": "https://registry.npmjs.org/@nextui-org/scroll-shadow/-/scroll-shadow-2.1.12.tgz", - "integrity": "sha512-uxT8D+WCWeBy4xaFDfqVpBgjjHZUwydXsX5HhbzZCBir/1eRG5GMnUES3w98DSwcUVadG64gAVsyGW4HmSZw1Q==", + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@nextui-org/scroll-shadow/-/scroll-shadow-2.1.13.tgz", + "integrity": "sha512-hFoVGplGMWuE+KXRz9gtKRq3e0YYkxutrqjDD0BiDHk4WkiyOrTnNuE6wnJTnd6Hd+kavLPBDu2+yGauDb7/Qg==", "dependencies": { "@nextui-org/react-utils": "2.0.10", "@nextui-org/shared-utils": "2.0.4", - "@nextui-org/use-data-scroll-overflow": "2.1.2" + "@nextui-org/use-data-scroll-overflow": "2.1.3" }, "peerDependencies": { "@nextui-org/system": ">=2.0.0", @@ -3054,20 +3058,20 @@ } }, "node_modules/@nextui-org/select": { - "version": "2.1.20", - "resolved": "https://registry.npmjs.org/@nextui-org/select/-/select-2.1.20.tgz", - "integrity": "sha512-GCO9uzyYnFIdJTqIe6aDe2NnYlclcdYfZnECFAze/R2MW0jpoysk5ysGBDjVDmZis6tLu+BOFXJbIlYEi+LoUQ==", + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/@nextui-org/select/-/select-2.1.21.tgz", + "integrity": "sha512-BVfmxIsZTL6dBiZ1Q5RbAnqiNpVnaJgWi0M1QMV448FHMaDHLTWtNOJPMD0QyxHRNPfDgFrqEAq6a1+pA26ckQ==", "dependencies": { "@nextui-org/aria-utils": "2.0.15", "@nextui-org/listbox": "2.1.16", - "@nextui-org/popover": "2.1.14", + "@nextui-org/popover": "2.1.15", "@nextui-org/react-utils": "2.0.10", - "@nextui-org/scroll-shadow": "2.1.12", + "@nextui-org/scroll-shadow": "2.1.13", "@nextui-org/shared-icons": "2.0.6", "@nextui-org/shared-utils": "2.0.4", - "@nextui-org/spinner": "2.0.24", + "@nextui-org/spinner": "2.0.25", "@nextui-org/use-aria-button": "2.0.6", - "@nextui-org/use-aria-multiselect": "2.1.3", + "@nextui-org/use-aria-multiselect": "2.1.4", "@react-aria/focus": "^3.14.3", "@react-aria/interactions": "^3.19.1", "@react-aria/utils": "^3.21.1", @@ -3114,13 +3118,13 @@ } }, "node_modules/@nextui-org/slider": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@nextui-org/slider/-/slider-2.2.5.tgz", - "integrity": "sha512-dC6HHMmtn2WvxDmbY/Dq51XJjQ7cAnjZsuYVIvhwIiCLDG8QnEIhmYN0DQp/6oeZsCHnyMHC4DmtgOiJL0eXrQ==", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@nextui-org/slider/-/slider-2.2.6.tgz", + "integrity": "sha512-adCjQ8k4bUwWcvmOJUki3+UVsCz4ms+qLG4jnY2wClPdQAwISMbZzQsuv3km+1HIZE5Ja7jzeeT/dMd8l3n+bg==", "dependencies": { "@nextui-org/react-utils": "2.0.10", "@nextui-org/shared-utils": "2.0.4", - "@nextui-org/tooltip": "2.0.29", + "@nextui-org/tooltip": "2.0.30", "@nextui-org/use-aria-press": "2.0.1", "@react-aria/focus": "^3.14.3", "@react-aria/i18n": "^3.8.4", @@ -3138,15 +3142,15 @@ } }, "node_modules/@nextui-org/snippet": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/@nextui-org/snippet/-/snippet-2.0.30.tgz", - "integrity": "sha512-8hKxqKpbJIMqFVedzYj90T4td+TkWdOdyYD9+VjywMdezAjsWdr8tqQj7boaMFjVNVSG+Pnw55Pgg/vkpc21aw==", + "version": "2.0.31", + "resolved": "https://registry.npmjs.org/@nextui-org/snippet/-/snippet-2.0.31.tgz", + "integrity": "sha512-WooH5cqlHoa6SqUhzseKY7g1ah8kzSv382u95Or9kIgSirEZCrjygup3nFeKTMAe01NZoAz3OOYO7XNFWJ57vA==", "dependencies": { - "@nextui-org/button": "2.0.26", + "@nextui-org/button": "2.0.27", "@nextui-org/react-utils": "2.0.10", "@nextui-org/shared-icons": "2.0.6", "@nextui-org/shared-utils": "2.0.4", - "@nextui-org/tooltip": "2.0.29", + "@nextui-org/tooltip": "2.0.30", "@nextui-org/use-clipboard": "2.0.4", "@react-aria/focus": "^3.14.3", "@react-aria/utils": "^3.21.1" @@ -3175,9 +3179,9 @@ } }, "node_modules/@nextui-org/spinner": { - "version": "2.0.24", - "resolved": "https://registry.npmjs.org/@nextui-org/spinner/-/spinner-2.0.24.tgz", - "integrity": "sha512-s/q2FmxGPNEqA0ifWfc7xgs5a5D9c3xKkxL3n7jDoRnWo0NPlRsa6QRJGiSL5dHNoUqspRf/lNw2V94Bxk86Pg==", + "version": "2.0.25", + "resolved": "https://registry.npmjs.org/@nextui-org/spinner/-/spinner-2.0.25.tgz", + "integrity": "sha512-s2iqaB71sanRxglJtG4UZF+Rz/W6UxnYegbkhnkkljH20vhOcrhwm5jKGStq8jkata8UZ0ajS67H8KY8lHV8nw==", "dependencies": { "@nextui-org/react-utils": "2.0.10", "@nextui-org/shared-utils": "2.0.4", @@ -3297,9 +3301,9 @@ } }, "node_modules/@nextui-org/theme": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/@nextui-org/theme/-/theme-2.1.17.tgz", - "integrity": "sha512-/WeHcMrAcWPGsEVn9M9TnvxKkaYkCocBH9JrDYCEFQoJgleUzHd4nVk7MWtpSOYJXLUzUMY1M9AqAK3jBkw+5g==", + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/@nextui-org/theme/-/theme-2.1.18.tgz", + "integrity": "sha512-2ptDh350lVD0yejZTpGv4fkeoGKB8+B/Coblzpjijfofn/t6MQIRIRRLp04wCCa/IbeevjS2wyadWpMDtVh3CQ==", "dependencies": { "color": "^4.2.3", "color2k": "^2.0.2", @@ -3332,14 +3336,15 @@ } }, "node_modules/@nextui-org/tooltip": { - "version": "2.0.29", - "resolved": "https://registry.npmjs.org/@nextui-org/tooltip/-/tooltip-2.0.29.tgz", - "integrity": "sha512-LaFyS5bXhcZFXP9rnh6pTKsYX6siWjzEe5z72FIOyAV2yvv2yhkRiO/mEHKI8moo+/tScW/6muFXsvbEalPefg==", + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/@nextui-org/tooltip/-/tooltip-2.0.30.tgz", + "integrity": "sha512-V3N9o/oNU1Y11etiilrlqt5dF4/o9eJSttgN2CPo8eRAPc96+sRpdGPGX3XcLJZNFRcNx8BkD/bcEUcrDdjmRA==", "dependencies": { "@nextui-org/aria-utils": "2.0.15", "@nextui-org/framer-transitions": "2.0.15", "@nextui-org/react-utils": "2.0.10", "@nextui-org/shared-utils": "2.0.4", + "@nextui-org/use-safe-layout-effect": "2.0.4", "@react-aria/interactions": "^3.19.1", "@react-aria/overlays": "^3.18.1", "@react-aria/tooltip": "^3.6.4", @@ -3421,9 +3426,9 @@ } }, "node_modules/@nextui-org/use-aria-multiselect": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@nextui-org/use-aria-multiselect/-/use-aria-multiselect-2.1.3.tgz", - "integrity": "sha512-OM1lj2jdl0Q2Zme/ds6qyT4IIGsBJSGNjvkM6pEnpdyoej/HwTKsSEpEFTDGJ5t9J9DWWCEt3hz0uJxOPnZ66Q==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@nextui-org/use-aria-multiselect/-/use-aria-multiselect-2.1.4.tgz", + "integrity": "sha512-F95sF4eY5TLkom5tIMb+eoT4i0Cc4qygnQRqIosg8OryDbH62/MV4x88GjQsgDCY8dNeWCNVodHXxaWmVSAgyQ==", "dependencies": { "@react-aria/i18n": "^3.8.4", "@react-aria/interactions": "^3.19.1", @@ -3493,9 +3498,9 @@ } }, "node_modules/@nextui-org/use-data-scroll-overflow": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@nextui-org/use-data-scroll-overflow/-/use-data-scroll-overflow-2.1.2.tgz", - "integrity": "sha512-3h9QX+dWkfqnqciQc2KeeR67e77hobjefNHGBTDuB4LhJSJ180ToZH09SQNHaUmKRLTU/RABjGWXxdbORI0r6g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@nextui-org/use-data-scroll-overflow/-/use-data-scroll-overflow-2.1.3.tgz", + "integrity": "sha512-f4rDr4MHGQTyqTd6L4MpKAcKfPDiVeWfYXXXX6gdN8UVTk+PzW675Fe+l7ATBgmaVTn1AEPJwW9dDUJcDpn21g==", "dependencies": { "@nextui-org/shared-utils": "2.0.4" }, @@ -3547,11 +3552,12 @@ } }, "node_modules/@nextui-org/use-pagination": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@nextui-org/use-pagination/-/use-pagination-2.0.4.tgz", - "integrity": "sha512-EETHzhh+LW8u2bm93LkUABbu0pIoWBCeY8hmvgjhhNMkILuwZNGYnp9tdF2rcS2P4KDlHQkIQcoiOGrGMqBUaQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nextui-org/use-pagination/-/use-pagination-2.0.5.tgz", + "integrity": "sha512-wH0sC85XeTPPE4zRq0ycAVB+SpmPEiSmTEGxpBG2sqiJlsrNfEeXvTKf73INXM4IWfP53ONAQ7Nd1T7EVuYSkw==", "dependencies": { - "@nextui-org/shared-utils": "2.0.4" + "@nextui-org/shared-utils": "2.0.4", + "@react-aria/i18n": "^3.8.4" }, "peerDependencies": { "react": ">=18" @@ -8594,6 +8600,14 @@ "node": ">=4.0" } }, + "node_modules/keyword-extractor": { + "version": "0.0.28", + "resolved": "https://registry.npmjs.org/keyword-extractor/-/keyword-extractor-0.0.28.tgz", + "integrity": "sha512-oi7dSPpYtW/3fE0vZiqQgZ8mW3F1V9K4+rBJ0FcVrdXBEQuhZ0zKj7sX74eqGASuepLHf9aYdeonyKHWhYpHQA==", + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/knex": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/knex/-/knex-3.1.0.tgz", @@ -9993,6 +10007,14 @@ "react": "^16.8.0 || ^17 || ^18" } }, + "node_modules/react-icons": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.0.1.tgz", + "integrity": "sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -13853,20 +13875,20 @@ } }, "@nextui-org/autocomplete": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@nextui-org/autocomplete/-/autocomplete-2.0.9.tgz", - "integrity": "sha512-ViPXrZnP35k7LF+TBA4w8nqu0OEj9p1z9Rt7rwrACmY2VmDGY6h6a6nDCMjhuTVXptftRvzxfIPsIyzBYqxb0g==", + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@nextui-org/autocomplete/-/autocomplete-2.0.10.tgz", + "integrity": "sha512-nQr8VC5RtpjnPef1qXgjNxRAw8JbN6q5qIFtsHWOCzvvn5jGAtdxkAkNE4C7DTvlMWZkIlEuR4DyAmFfY8CChQ==", "requires": { "@nextui-org/aria-utils": "2.0.15", - "@nextui-org/button": "2.0.26", - "@nextui-org/input": "2.1.16", + "@nextui-org/button": "2.0.27", + "@nextui-org/input": "2.1.17", "@nextui-org/listbox": "2.1.16", - "@nextui-org/popover": "2.1.14", + "@nextui-org/popover": "2.1.15", "@nextui-org/react-utils": "2.0.10", - "@nextui-org/scroll-shadow": "2.1.12", + "@nextui-org/scroll-shadow": "2.1.13", "@nextui-org/shared-icons": "2.0.6", "@nextui-org/shared-utils": "2.0.4", - "@nextui-org/spinner": "2.0.24", + "@nextui-org/spinner": "2.0.25", "@nextui-org/use-aria-button": "2.0.6", "@react-aria/combobox": "^3.7.1", "@react-aria/focus": "^3.14.3", @@ -13918,14 +13940,14 @@ } }, "@nextui-org/button": { - "version": "2.0.26", - "resolved": "https://registry.npmjs.org/@nextui-org/button/-/button-2.0.26.tgz", - "integrity": "sha512-mDrSII1oneY4omwDdxUhl5oLa3AhoWCchwV/jt7egunnAFie32HbTqfFYGpLGiJw3JMMh3WDUthrI1islVTRKA==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/@nextui-org/button/-/button-2.0.27.tgz", + "integrity": "sha512-oErzUr9KtE/qjUx4dSbalphxURssxGf9tv0mW++ZMkmVX1E6i887FwZb9xAVm9oBwYwR6+xpJaqjQLmt8aN/rQ==", "requires": { "@nextui-org/react-utils": "2.0.10", "@nextui-org/ripple": "2.0.24", "@nextui-org/shared-utils": "2.0.4", - "@nextui-org/spinner": "2.0.24", + "@nextui-org/spinner": "2.0.25", "@nextui-org/use-aria-button": "2.0.6", "@react-aria/button": "^3.8.4", "@react-aria/focus": "^3.14.3", @@ -14007,12 +14029,12 @@ } }, "@nextui-org/dropdown": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@nextui-org/dropdown/-/dropdown-2.1.16.tgz", - "integrity": "sha512-3KINNvC7Cz+deQltCM8gaB7iJCfU4Qsp1fwnoy1wUEjeZhEtPOPR59oTyqT+gPaPIisP1+LLOfcqRl4jNQoVXw==", + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/@nextui-org/dropdown/-/dropdown-2.1.17.tgz", + "integrity": "sha512-Hxmz1Yf/LjjOLqWRF49Q5ZYJtae6ydDEk1mv8oMKNmSWHi92lrgmHlwkGvR3mjczbRuF+WkXHLEhVZH6/tZQ7A==", "requires": { "@nextui-org/menu": "2.0.17", - "@nextui-org/popover": "2.1.14", + "@nextui-org/popover": "2.1.15", "@nextui-org/react-utils": "2.0.10", "@nextui-org/shared-utils": "2.0.4", "@react-aria/focus": "^3.14.3", @@ -14042,9 +14064,9 @@ } }, "@nextui-org/input": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@nextui-org/input/-/input-2.1.16.tgz", - "integrity": "sha512-nUTlAvsXj5t88ycvQdICxf78/pko6Wznx2OomvYjb3E45eb77twQcWUDhydkJCWIh3b4AhGHSMM6GYxwWUgMDA==", + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/@nextui-org/input/-/input-2.1.17.tgz", + "integrity": "sha512-3FW3NDDbQOa5IlUCpO2Ma/XEjGnx4TQLM8MvMbskc+GNbZ0mtzfV0hCeQkqxxJ2lP4Mkp4QhwGRRkRrDu1G0Wg==", "requires": { "@nextui-org/react-utils": "2.0.10", "@nextui-org/shared-icons": "2.0.6", @@ -14127,9 +14149,9 @@ } }, "@nextui-org/modal": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/@nextui-org/modal/-/modal-2.0.28.tgz", - "integrity": "sha512-unfP0EMF3FDg5CkRqou03s4/BopWbaBTeVIMZeA2A1WF5teHUOmpLdp44Z1KOoWB1RVMDVd4JeoauNHNhJMp0g==", + "version": "2.0.29", + "resolved": "https://registry.npmjs.org/@nextui-org/modal/-/modal-2.0.29.tgz", + "integrity": "sha512-C/pvw0fAPWKbfMoGfIVZWhMRbe+DRGEg7GqPVY7EmW4FSSIK7Sfdn6Jxm+sSv+a7xHpDr86nirFbvN3S4jCaHw==", "requires": { "@nextui-org/framer-transitions": "2.0.15", "@nextui-org/react-utils": "2.0.10", @@ -14168,32 +14190,34 @@ } }, "@nextui-org/pagination": { - "version": "2.0.26", - "resolved": "https://registry.npmjs.org/@nextui-org/pagination/-/pagination-2.0.26.tgz", - "integrity": "sha512-OVpkpXqUKRuMRIcYESBAL95d3pqZ17SKAyNINMiJ/DwWnrzJu/LXGmFwTuYRoBdqHFlm7guGqZbHmAkcS/Fgow==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/@nextui-org/pagination/-/pagination-2.0.27.tgz", + "integrity": "sha512-v1tSsb0Q863/gKVUxuN7FcE1TZWuvcbWZOrWjKe0/llRgfZ23/4KD1AmFyYuKo5RDFt+i1JWSfzAu08j0Hzzqg==", "requires": { "@nextui-org/react-utils": "2.0.10", "@nextui-org/shared-icons": "2.0.6", "@nextui-org/shared-utils": "2.0.4", "@nextui-org/use-aria-press": "2.0.1", - "@nextui-org/use-pagination": "2.0.4", + "@nextui-org/use-pagination": "2.0.5", "@react-aria/focus": "^3.14.3", + "@react-aria/i18n": "^3.8.4", "@react-aria/interactions": "^3.19.1", "@react-aria/utils": "^3.21.1", "scroll-into-view-if-needed": "3.0.10" } }, "@nextui-org/popover": { - "version": "2.1.14", - "resolved": "https://registry.npmjs.org/@nextui-org/popover/-/popover-2.1.14.tgz", - "integrity": "sha512-fqqktFQ/chIBS9Y3MghL6KX6qAy3hodtXUDchnxLa1GL+oi6TCBLUjo+wgI5EMJrTTbqo/eFLui/Ks00JfCj+A==", + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@nextui-org/popover/-/popover-2.1.15.tgz", + "integrity": "sha512-FQ66y49sQvXvyDrEsEFAC0qfpl2X+5ZPGaVXdNd3Cjox/jxAxp93cSUkk0iOfYvdsbO5zVFjuM0L3Dqn4hsHMw==", "requires": { "@nextui-org/aria-utils": "2.0.15", - "@nextui-org/button": "2.0.26", + "@nextui-org/button": "2.0.27", "@nextui-org/framer-transitions": "2.0.15", "@nextui-org/react-utils": "2.0.10", "@nextui-org/shared-utils": "2.0.4", "@nextui-org/use-aria-button": "2.0.6", + "@nextui-org/use-safe-layout-effect": "2.0.4", "@react-aria/dialog": "^3.5.7", "@react-aria/focus": "^3.14.3", "@react-aria/interactions": "^3.19.1", @@ -14206,9 +14230,9 @@ } }, "@nextui-org/progress": { - "version": "2.0.24", - "resolved": "https://registry.npmjs.org/@nextui-org/progress/-/progress-2.0.24.tgz", - "integrity": "sha512-RPVsFCF8COFClS/8PqEepzryhDFtIcJGQLu/P+qAr7jIDlXizXaBDrp0X34GVtQsapNeE9ExxX9Kt+QIspuHHQ==", + "version": "2.0.25", + "resolved": "https://registry.npmjs.org/@nextui-org/progress/-/progress-2.0.25.tgz", + "integrity": "sha512-EFVxwT0CXq+2scPLhKKRHkWb6xNa6Vjx+HdgSg3l4lgAxAUryvdfksjW8vjxn6x4I2rGbdzAYPEu27p2KaK7jg==", "requires": { "@nextui-org/react-utils": "2.0.10", "@nextui-org/shared-utils": "2.0.4", @@ -14238,48 +14262,48 @@ } }, "@nextui-org/react": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/@nextui-org/react/-/react-2.2.9.tgz", - "integrity": "sha512-QHkUQTxI9sYoVjrvTpYm5K68pMDRqD13+DVzdsrkJuETGhbvE2c2CCGc4on9EwXC3JsOxuP/OyqaAmOIuHhYkA==", + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/@nextui-org/react/-/react-2.2.10.tgz", + "integrity": "sha512-YJhUIeLnO/FGDbZgfeWEz32RBrH2YFA1qsJQtMF7mza8rjspX/CkankvI7xs1o6sW/TYLSTq7sOF9RGMxLTIAA==", "requires": { "@nextui-org/accordion": "2.0.28", - "@nextui-org/autocomplete": "2.0.9", + "@nextui-org/autocomplete": "2.0.10", "@nextui-org/avatar": "2.0.24", "@nextui-org/badge": "2.0.24", "@nextui-org/breadcrumbs": "2.0.4", - "@nextui-org/button": "2.0.26", + "@nextui-org/button": "2.0.27", "@nextui-org/card": "2.0.24", "@nextui-org/checkbox": "2.0.25", "@nextui-org/chip": "2.0.25", "@nextui-org/code": "2.0.24", "@nextui-org/divider": "2.0.25", - "@nextui-org/dropdown": "2.1.16", + "@nextui-org/dropdown": "2.1.17", "@nextui-org/image": "2.0.24", - "@nextui-org/input": "2.1.16", + "@nextui-org/input": "2.1.17", "@nextui-org/kbd": "2.0.25", "@nextui-org/link": "2.0.26", "@nextui-org/listbox": "2.1.16", "@nextui-org/menu": "2.0.17", - "@nextui-org/modal": "2.0.28", + "@nextui-org/modal": "2.0.29", "@nextui-org/navbar": "2.0.27", - "@nextui-org/pagination": "2.0.26", - "@nextui-org/popover": "2.1.14", - "@nextui-org/progress": "2.0.24", + "@nextui-org/pagination": "2.0.27", + "@nextui-org/popover": "2.1.15", + "@nextui-org/progress": "2.0.25", "@nextui-org/radio": "2.0.25", "@nextui-org/ripple": "2.0.24", - "@nextui-org/scroll-shadow": "2.1.12", - "@nextui-org/select": "2.1.20", + "@nextui-org/scroll-shadow": "2.1.13", + "@nextui-org/select": "2.1.21", "@nextui-org/skeleton": "2.0.24", - "@nextui-org/slider": "2.2.5", - "@nextui-org/snippet": "2.0.30", + "@nextui-org/slider": "2.2.6", + "@nextui-org/snippet": "2.0.31", "@nextui-org/spacer": "2.0.24", - "@nextui-org/spinner": "2.0.24", + "@nextui-org/spinner": "2.0.25", "@nextui-org/switch": "2.0.25", "@nextui-org/system": "2.0.15", "@nextui-org/table": "2.0.28", "@nextui-org/tabs": "2.0.26", - "@nextui-org/theme": "2.1.17", - "@nextui-org/tooltip": "2.0.29", + "@nextui-org/theme": "2.1.18", + "@nextui-org/tooltip": "2.0.30", "@nextui-org/user": "2.0.25", "@react-aria/visually-hidden": "^3.8.6" } @@ -14308,30 +14332,30 @@ } }, "@nextui-org/scroll-shadow": { - "version": "2.1.12", - "resolved": "https://registry.npmjs.org/@nextui-org/scroll-shadow/-/scroll-shadow-2.1.12.tgz", - "integrity": "sha512-uxT8D+WCWeBy4xaFDfqVpBgjjHZUwydXsX5HhbzZCBir/1eRG5GMnUES3w98DSwcUVadG64gAVsyGW4HmSZw1Q==", + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@nextui-org/scroll-shadow/-/scroll-shadow-2.1.13.tgz", + "integrity": "sha512-hFoVGplGMWuE+KXRz9gtKRq3e0YYkxutrqjDD0BiDHk4WkiyOrTnNuE6wnJTnd6Hd+kavLPBDu2+yGauDb7/Qg==", "requires": { "@nextui-org/react-utils": "2.0.10", "@nextui-org/shared-utils": "2.0.4", - "@nextui-org/use-data-scroll-overflow": "2.1.2" + "@nextui-org/use-data-scroll-overflow": "2.1.3" } }, "@nextui-org/select": { - "version": "2.1.20", - "resolved": "https://registry.npmjs.org/@nextui-org/select/-/select-2.1.20.tgz", - "integrity": "sha512-GCO9uzyYnFIdJTqIe6aDe2NnYlclcdYfZnECFAze/R2MW0jpoysk5ysGBDjVDmZis6tLu+BOFXJbIlYEi+LoUQ==", + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/@nextui-org/select/-/select-2.1.21.tgz", + "integrity": "sha512-BVfmxIsZTL6dBiZ1Q5RbAnqiNpVnaJgWi0M1QMV448FHMaDHLTWtNOJPMD0QyxHRNPfDgFrqEAq6a1+pA26ckQ==", "requires": { "@nextui-org/aria-utils": "2.0.15", "@nextui-org/listbox": "2.1.16", - "@nextui-org/popover": "2.1.14", + "@nextui-org/popover": "2.1.15", "@nextui-org/react-utils": "2.0.10", - "@nextui-org/scroll-shadow": "2.1.12", + "@nextui-org/scroll-shadow": "2.1.13", "@nextui-org/shared-icons": "2.0.6", "@nextui-org/shared-utils": "2.0.4", - "@nextui-org/spinner": "2.0.24", + "@nextui-org/spinner": "2.0.25", "@nextui-org/use-aria-button": "2.0.6", - "@nextui-org/use-aria-multiselect": "2.1.3", + "@nextui-org/use-aria-multiselect": "2.1.4", "@react-aria/focus": "^3.14.3", "@react-aria/interactions": "^3.19.1", "@react-aria/utils": "^3.21.1", @@ -14362,13 +14386,13 @@ } }, "@nextui-org/slider": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@nextui-org/slider/-/slider-2.2.5.tgz", - "integrity": "sha512-dC6HHMmtn2WvxDmbY/Dq51XJjQ7cAnjZsuYVIvhwIiCLDG8QnEIhmYN0DQp/6oeZsCHnyMHC4DmtgOiJL0eXrQ==", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@nextui-org/slider/-/slider-2.2.6.tgz", + "integrity": "sha512-adCjQ8k4bUwWcvmOJUki3+UVsCz4ms+qLG4jnY2wClPdQAwISMbZzQsuv3km+1HIZE5Ja7jzeeT/dMd8l3n+bg==", "requires": { "@nextui-org/react-utils": "2.0.10", "@nextui-org/shared-utils": "2.0.4", - "@nextui-org/tooltip": "2.0.29", + "@nextui-org/tooltip": "2.0.30", "@nextui-org/use-aria-press": "2.0.1", "@react-aria/focus": "^3.14.3", "@react-aria/i18n": "^3.8.4", @@ -14380,15 +14404,15 @@ } }, "@nextui-org/snippet": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/@nextui-org/snippet/-/snippet-2.0.30.tgz", - "integrity": "sha512-8hKxqKpbJIMqFVedzYj90T4td+TkWdOdyYD9+VjywMdezAjsWdr8tqQj7boaMFjVNVSG+Pnw55Pgg/vkpc21aw==", + "version": "2.0.31", + "resolved": "https://registry.npmjs.org/@nextui-org/snippet/-/snippet-2.0.31.tgz", + "integrity": "sha512-WooH5cqlHoa6SqUhzseKY7g1ah8kzSv382u95Or9kIgSirEZCrjygup3nFeKTMAe01NZoAz3OOYO7XNFWJ57vA==", "requires": { - "@nextui-org/button": "2.0.26", + "@nextui-org/button": "2.0.27", "@nextui-org/react-utils": "2.0.10", "@nextui-org/shared-icons": "2.0.6", "@nextui-org/shared-utils": "2.0.4", - "@nextui-org/tooltip": "2.0.29", + "@nextui-org/tooltip": "2.0.30", "@nextui-org/use-clipboard": "2.0.4", "@react-aria/focus": "^3.14.3", "@react-aria/utils": "^3.21.1" @@ -14405,9 +14429,9 @@ } }, "@nextui-org/spinner": { - "version": "2.0.24", - "resolved": "https://registry.npmjs.org/@nextui-org/spinner/-/spinner-2.0.24.tgz", - "integrity": "sha512-s/q2FmxGPNEqA0ifWfc7xgs5a5D9c3xKkxL3n7jDoRnWo0NPlRsa6QRJGiSL5dHNoUqspRf/lNw2V94Bxk86Pg==", + "version": "2.0.25", + "resolved": "https://registry.npmjs.org/@nextui-org/spinner/-/spinner-2.0.25.tgz", + "integrity": "sha512-s2iqaB71sanRxglJtG4UZF+Rz/W6UxnYegbkhnkkljH20vhOcrhwm5jKGStq8jkata8UZ0ajS67H8KY8lHV8nw==", "requires": { "@nextui-org/react-utils": "2.0.10", "@nextui-org/shared-utils": "2.0.4", @@ -14494,9 +14518,9 @@ } }, "@nextui-org/theme": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/@nextui-org/theme/-/theme-2.1.17.tgz", - "integrity": "sha512-/WeHcMrAcWPGsEVn9M9TnvxKkaYkCocBH9JrDYCEFQoJgleUzHd4nVk7MWtpSOYJXLUzUMY1M9AqAK3jBkw+5g==", + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/@nextui-org/theme/-/theme-2.1.18.tgz", + "integrity": "sha512-2ptDh350lVD0yejZTpGv4fkeoGKB8+B/Coblzpjijfofn/t6MQIRIRRLp04wCCa/IbeevjS2wyadWpMDtVh3CQ==", "requires": { "color": "^4.2.3", "color2k": "^2.0.2", @@ -14521,14 +14545,15 @@ } }, "@nextui-org/tooltip": { - "version": "2.0.29", - "resolved": "https://registry.npmjs.org/@nextui-org/tooltip/-/tooltip-2.0.29.tgz", - "integrity": "sha512-LaFyS5bXhcZFXP9rnh6pTKsYX6siWjzEe5z72FIOyAV2yvv2yhkRiO/mEHKI8moo+/tScW/6muFXsvbEalPefg==", + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/@nextui-org/tooltip/-/tooltip-2.0.30.tgz", + "integrity": "sha512-V3N9o/oNU1Y11etiilrlqt5dF4/o9eJSttgN2CPo8eRAPc96+sRpdGPGX3XcLJZNFRcNx8BkD/bcEUcrDdjmRA==", "requires": { "@nextui-org/aria-utils": "2.0.15", "@nextui-org/framer-transitions": "2.0.15", "@nextui-org/react-utils": "2.0.10", "@nextui-org/shared-utils": "2.0.4", + "@nextui-org/use-safe-layout-effect": "2.0.4", "@react-aria/interactions": "^3.19.1", "@react-aria/overlays": "^3.18.1", "@react-aria/tooltip": "^3.6.4", @@ -14590,9 +14615,9 @@ } }, "@nextui-org/use-aria-multiselect": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@nextui-org/use-aria-multiselect/-/use-aria-multiselect-2.1.3.tgz", - "integrity": "sha512-OM1lj2jdl0Q2Zme/ds6qyT4IIGsBJSGNjvkM6pEnpdyoej/HwTKsSEpEFTDGJ5t9J9DWWCEt3hz0uJxOPnZ66Q==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@nextui-org/use-aria-multiselect/-/use-aria-multiselect-2.1.4.tgz", + "integrity": "sha512-F95sF4eY5TLkom5tIMb+eoT4i0Cc4qygnQRqIosg8OryDbH62/MV4x88GjQsgDCY8dNeWCNVodHXxaWmVSAgyQ==", "requires": { "@react-aria/i18n": "^3.8.4", "@react-aria/interactions": "^3.19.1", @@ -14647,9 +14672,9 @@ "requires": {} }, "@nextui-org/use-data-scroll-overflow": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@nextui-org/use-data-scroll-overflow/-/use-data-scroll-overflow-2.1.2.tgz", - "integrity": "sha512-3h9QX+dWkfqnqciQc2KeeR67e77hobjefNHGBTDuB4LhJSJ180ToZH09SQNHaUmKRLTU/RABjGWXxdbORI0r6g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@nextui-org/use-data-scroll-overflow/-/use-data-scroll-overflow-2.1.3.tgz", + "integrity": "sha512-f4rDr4MHGQTyqTd6L4MpKAcKfPDiVeWfYXXXX6gdN8UVTk+PzW675Fe+l7ATBgmaVTn1AEPJwW9dDUJcDpn21g==", "requires": { "@nextui-org/shared-utils": "2.0.4" } @@ -14687,11 +14712,12 @@ "requires": {} }, "@nextui-org/use-pagination": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@nextui-org/use-pagination/-/use-pagination-2.0.4.tgz", - "integrity": "sha512-EETHzhh+LW8u2bm93LkUABbu0pIoWBCeY8hmvgjhhNMkILuwZNGYnp9tdF2rcS2P4KDlHQkIQcoiOGrGMqBUaQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nextui-org/use-pagination/-/use-pagination-2.0.5.tgz", + "integrity": "sha512-wH0sC85XeTPPE4zRq0ycAVB+SpmPEiSmTEGxpBG2sqiJlsrNfEeXvTKf73INXM4IWfP53ONAQ7Nd1T7EVuYSkw==", "requires": { - "@nextui-org/shared-utils": "2.0.4" + "@nextui-org/shared-utils": "2.0.4", + "@react-aria/i18n": "^3.8.4" } }, "@nextui-org/use-safe-layout-effect": { @@ -18438,6 +18464,11 @@ "object.assign": "^4.1.3" } }, + "keyword-extractor": { + "version": "0.0.28", + "resolved": "https://registry.npmjs.org/keyword-extractor/-/keyword-extractor-0.0.28.tgz", + "integrity": "sha512-oi7dSPpYtW/3fE0vZiqQgZ8mW3F1V9K4+rBJ0FcVrdXBEQuhZ0zKj7sX74eqGASuepLHf9aYdeonyKHWhYpHQA==" + }, "knex": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/knex/-/knex-3.1.0.tgz", @@ -19338,6 +19369,12 @@ "integrity": "sha512-F/TroLjTICipmHeFlMrLtNLceO2xr1jU3CyiNla5zdwsGUGu2UOxxR4UyJgLlhMwLW/Wzp4cpJ7CPfgJIeKdSg==", "requires": {} }, + "react-icons": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.0.1.tgz", + "integrity": "sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw==", + "requires": {} + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index 7155c913..0b8fa0de 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@getalby/lightning-tools": "^5.0.1", "@heroicons/react": "^2.0.18", "@itseasy21/react-elastic-carousel": "^0.12.3", - "@nextui-org/react": "^2.2.9", + "@nextui-org/react": "^2.2.10", "@tremor/react": "^3.13.4", "autoprefixer": "10.4.14", "axios": "^1.6.0", @@ -27,6 +27,7 @@ "eslint-config-next": "13.3.0", "fast-geoip": "^1.1.88", "framer-motion": "^10.16.4", + "keyword-extractor": "^0.0.28", "knex": "^3.1.0", "luxon": "^3.4.4", "next": "^13.5.4", @@ -39,6 +40,7 @@ "qrcode": "^1.5.3", "react": "18.2.0", "react-hook-form": "^7.47.0", + "react-icons": "^5.0.1", "react-responsive-carousel": "^3.2.23", "tailwindcss": "3.3.1", "uuid": "^9.0.0" diff --git a/pages/_app.tsx b/pages/_app.tsx index 38e23503..05a36928 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -3,7 +3,6 @@ import type { AppProps } from "next/app"; import Head from "next/head"; import "../styles/globals.css"; import { useState, useEffect } from "react"; -import { useRouter } from "next/router"; import { ProfileMapContext, ProfileContextInterface, @@ -11,7 +10,7 @@ import { ProductContextInterface, ChatsContextInterface, ChatsContext, - ChatsMap, + MyListingsContext, } from "../utils/context/context"; import { getLocalStorageData, @@ -27,6 +26,9 @@ import { import { NostrEvent, ProfileData } from "../utils/types/types"; import BottomNav from "@/components/nav-bottom"; import SideNav from "@/components/nav-side"; +import parseTags, { + ProductData, +} from "@/components/utility/product-parser-functions"; function App({ Component, pageProps }: AppProps) { const [localStorageValues, setLocalStorageValues] = @@ -35,47 +37,107 @@ function App({ Component, pageProps }: AppProps) { { productEvents: [], isLoading: true, - addNewlyCreatedProductEvent: (productEvent: any) => { + setIsLoading: (isLoading) => { setProductContext((productContext) => { - let productEvents = [...productContext.productEvents, productEvent]; - return { - productEvents: productEvents, - isLoading: false, - addNewlyCreatedProductEvent: - productContext.addNewlyCreatedProductEvent, - removeDeletedProductEvent: productContext.removeDeletedProductEvent, - }; + return { ...productContext, isLoading }; + }); + }, + filters: { + searchQuery: "", + categories: new Set([]), + location: null, + }, + setFilters: (filters) => { + setProductContext((productContext) => { + return { ...productContext, filters }; + }); + }, + addNewlyCreatedProductEvents: (products: ProductData[]) => { + setProductContext((productContext) => { + const productEvents = [ + ...productContext.productEvents, + ...products, + ].sort((a, b) => b.createdAt - a.createdAt); + return { ...productContext, productEvents }; }); }, removeDeletedProductEvent: (productId: string) => { + // remove from both setProductContext((productContext) => { - let productEvents = [...productContext.productEvents].filter( + const productEvents = [...productContext.productEvents].filter( + (event) => event.id !== productId, + ); + return { ...productContext, productEvents }; + }); + setMyListingsContext((myListingsContext) => { + const productEvents = [...myListingsContext.productEvents].filter( (event) => event.id !== productId, ); - return { - productEvents: productEvents, - isLoading: false, - addNewlyCreatedProductEvent: - productContext.addNewlyCreatedProductEvent, - removeDeletedProductEvent: productContext.removeDeletedProductEvent, - }; + return { ...myListingsContext, productEvents }; }); }, }, ); + const [myListingsContext, setMyListingsContext] = + useState({ + productEvents: [], + isLoading: true, + setIsLoading: (isLoading) => { + setMyListingsContext((productContext) => { + return { ...productContext, isLoading }; + }); + }, + filters: { + searchQuery: "", + categories: new Set([]), + location: null, + }, + setFilters: (filters) => { + setMyListingsContext((productContext) => { + return { ...productContext, filters }; + }); + }, + addNewlyCreatedProductEvents: ( + products: ProductData[], + replace?: boolean, + ) => { + setMyListingsContext((productContext) => { + const productEvents = [ + ...(replace ? [] : [...productContext.productEvents]), + ...products, + ].sort((a, b) => b.createdAt - a.createdAt); + if (replace) { + return { ...productContext, productEvents }; + } else { + return { ...productContext, productEvents }; + } + }); + }, + removeDeletedProductEvent: (productId: string) => { + // remove from both + setProductContext((productContext) => { + const productEvents = [...productContext.productEvents].filter( + (event) => event.id !== productId, + ); + return { ...productContext, productEvents }; + }); + setMyListingsContext((myListingsContext) => { + const productEvents = [...myListingsContext.productEvents].filter( + (event) => event.id !== productId, + ); + return { ...myListingsContext, productEvents }; + }); + }, + }); const [profileContext, setProfileContext] = useState( { profileData: new Map(), isLoading: true, updateProfileData: (profileData: ProfileData) => { setProfileContext((profileContext) => { - let newProfileData = new Map(profileContext.profileData); + const newProfileData = new Map(profileContext.profileData); newProfileData.set(profileData.pubkey, profileData); - return { - profileData: newProfileData, - isLoading: false, - updateProfileData: profileContext.updateProfileData, - }; + return { ...profileContext, profileData: newProfileData }; }); }, }, @@ -85,37 +147,6 @@ function App({ Component, pageProps }: AppProps) { isLoading: true, }); - const editProductContext = ( - productEvents: NostrEvent[], - isLoading: boolean, - ) => { - setProductContext((productContext) => { - return { - productEvents: productEvents, - isLoading: isLoading, - addNewlyCreatedProductEvent: productContext.addNewlyCreatedProductEvent, - removeDeletedProductEvent: productContext.removeDeletedProductEvent, - }; - }); - }; - - const editProfileContext = ( - profileData: Map, - isLoading: boolean, - ) => { - setProfileContext((profileContext) => { - return { - profileData, - isLoading, - updateProfileData: profileContext.updateProfileData, - }; - }); - }; - - const editChatContext = (chatsMap: ChatsMap, isLoading: boolean) => { - setChatsContext({ chatsMap, isLoading }); - }; - /** FETCH initial PRODUCTS and PROFILES **/ useEffect(() => { async function fetchData() { @@ -123,34 +154,53 @@ function App({ Component, pageProps }: AppProps) { const userPubkey = getLocalStorageData().userPubkey; try { let pubkeysToFetchProfilesFor: string[] = []; - let { profileSetFromProducts } = await fetchAllPosts( - relays, - editProductContext, - ); + setProductContext({ ...productContext, isLoading: true }); + let { profileSetFromProducts, productArrayFromRelay } = + await fetchAllPosts(relays, productContext.filters); + const productEvents = productArrayFromRelay + .reduce((curr, event) => { + const productEvent = parseTags(event); + return productEvent ? [...curr, productEvent] : curr; + }, [] as ProductData[]) + .sort((a, b) => b.createdAt - a.createdAt); + setProductContext({ + ...productContext, + productEvents: [...productEvents], + isLoading: false, + }); pubkeysToFetchProfilesFor = [...profileSetFromProducts]; - let { profileSetFromChats } = await fetchChatsAndMessages( + setChatsContext({ ...chatsContext, isLoading: true }); + let { profileSetFromChats, chatsData } = await fetchChatsAndMessages( relays, userPubkey, - editChatContext, ); + setChatsContext({ + ...chatsContext, + chatsMap: chatsData, + isLoading: false, + }); pubkeysToFetchProfilesFor = [ userPubkey as string, ...pubkeysToFetchProfilesFor, ...profileSetFromChats, ]; - let { profileMap } = await fetchProfile( + setProfileContext({ ...profileContext, isLoading: true }); + let { profileData } = await fetchProfile( relays, pubkeysToFetchProfilesFor, - editProfileContext, ); + setProfileContext({ ...profileContext, profileData, isLoading: false }); } catch (error) { console.error("Error fetching data:", error); + setProductContext({ ...productContext, isLoading: false }); + setChatsContext({ ...chatsContext, isLoading: false }); + setProfileContext({ ...profileContext, isLoading: false }); } } fetchData(); window.addEventListener("storage", fetchData); return () => window.removeEventListener("storage", fetchData); - }, [localStorageValues.relays]); + }, [localStorageValues.relays, productContext.filters]); useEffect(() => { if ("serviceWorker" in navigator) { @@ -179,21 +229,23 @@ function App({ Component, pageProps }: AppProps) { /> - - - - -
- -
- -
-
- -
-
-
-
+ + + + + +
+ +
+ +
+
+ +
+
+
+
+
); diff --git a/pages/api/nostr/cache-service.ts b/pages/api/nostr/cache-service.ts index b708bcb5..ec0d2e85 100644 --- a/pages/api/nostr/cache-service.ts +++ b/pages/api/nostr/cache-service.ts @@ -1,10 +1,15 @@ -import { NostrEvent } from "../../../utils/types/types"; +import { ProductData } from "@/components/utility/product-parser-functions"; +import { + ItemType, + NostrEvent, + NostrMessageEvent, + ProfileData, +} from "../../../utils/types/types"; import Dexie, { Table } from "dexie"; -import { ItemType, NostrMessageEvent } from "../../../utils/types/types"; class ItemsFetchedFromRelays extends Dexie { - public products!: Table<{ id: string; product: NostrEvent }>; - public profiles!: Table<{ id: string; profile: {} }>; + public products!: Table<{ id: string; product: ProductData }>; + public profiles!: Table<{ id: string; profile: ProfileData }>; public chatMessages!: Table<{ id: string; message: NostrMessageEvent }>; public lastFetchedTime!: Table<{ itemType: string; time: number }>; @@ -48,18 +53,18 @@ export const didXMinutesElapseSinceLastFetch = async ( return timelapsedInMinutes > minutes; }; -export const addProductToCache = async (product: NostrEvent) => { +export const addProductToCache = async (product: ProductData) => { await products.put({ id: product.id, product }); }; -export const addProductsToCache = async (productsArray: NostrEvent[]) => { +export const addProductsToCache = async (productsArray: ProductData[]) => { productsArray.forEach(async (product) => { await addProductToCache(product); }); await lastFetchedTime.put({ itemType: "products", time: Date.now() }); }; -export const addProfilesToCache = async (profileMap: Map) => { +export const addProfilesToCache = async (profileMap: Map) => { Array.from(profileMap.entries()).forEach(async ([pubkey, profile]) => { if (profile === null) return; await profiles.put({ id: pubkey, profile }); @@ -84,7 +89,7 @@ export const removeProductFromCache = async (productIds: string[]) => { export const fetchAllProductsFromCache = async () => { let productsFromCache = await products.toArray(); let productsArray = productsFromCache.map( - (productFromCache: { id: string; product: NostrEvent }) => + (productFromCache: { id: string; product: ProductData }) => productFromCache.product, ); return productsArray; @@ -92,11 +97,11 @@ export const fetchAllProductsFromCache = async () => { export const fetchProfileDataFromCache = async () => { let cache = await profiles.toArray(); - let productMap = new Map(); + let profileMap = new Map(); cache.forEach(({ id, profile }) => { - productMap.set(id, profile); + profileMap.set(id, profile); }); - return productMap; + return profileMap; }; export const fetchAllChatsFromCache = async () => { diff --git a/pages/api/nostr/fetch-service.ts b/pages/api/nostr/fetch-service.ts index aa2ac865..780f6d89 100644 --- a/pages/api/nostr/fetch-service.ts +++ b/pages/api/nostr/fetch-service.ts @@ -1,4 +1,4 @@ -import { Filter, Nostr, SimplePool } from "nostr-tools"; +import { Filter, SimplePool } from "nostr-tools"; import { addChatMessageToCache, addProductToCache, @@ -8,27 +8,37 @@ import { fetchProfileDataFromCache, removeProductFromCache, } from "./cache-service"; -import { NostrEvent, NostrMessageEvent } from "@/utils/types/types"; -import { ChatsMap } from "@/utils/context/context"; +import { + NostrEvent, + NostrMessageEvent, + ProfileData, +} from "@/utils/types/types"; +import { ChatsMap, ProductContextInterface } from "@/utils/context/context"; import { DateTime } from "luxon"; +import { getNameToCodeMap } from "@/utils/location/location"; +import parseTags, { + ProductData, +} from "@/components/utility/product-parser-functions"; +import keyword_extractor from "keyword-extractor"; +import { getKeywords } from "@/utils/text"; export const fetchAllPosts = async ( relays: string[], - editProductContext: (productEvents: NostrEvent[], isLoading: boolean) => void, + filters: ProductContextInterface["filters"], since?: number, until?: number, ): Promise<{ profileSetFromProducts: Set; + productArrayFromRelay: NostrEvent[]; }> => { return new Promise(async function (resolve, reject) { try { - let deletedProductsInCacheSet: Set = new Set(); // used to remove deleted items from cache + let deletedProductsInCacheSet: Set = new Set(); // used to remove deleted items from cache try { let productArrayFromCache = await fetchAllProductsFromCache(); deletedProductsInCacheSet = new Set( - productArrayFromCache.map((product: NostrEvent) => product.id), + productArrayFromCache.map((product: ProductData) => product.id), ); - editProductContext(productArrayFromCache, false); } catch (error) { console.log("Error: ", error); } @@ -42,17 +52,39 @@ export const fetchAllPosts = async ( until = Math.trunc(DateTime.now().toSeconds()); } + const buildTagsFilters: string[] = []; + if (filters.categories.size > 0) { + buildTagsFilters.push(...Array.from(filters.categories)); + } + if (filters.searchQuery.length > 0) { + buildTagsFilters.push( + ...getKeywords(filters.searchQuery), + ); + } const filter: Filter = { kinds: [30402], since, until, + // No relays support NIP-50 for 30402 kinds, yet... + // ...(filters.searchQuery.length > 0 && { + // search: filters.searchQuery, + // }), + ...(filters.location && { + "#g": [getNameToCodeMap(filters.location)], + }), + ...(buildTagsFilters.length > 0 && { + "#t": buildTagsFilters, + }), }; let productArrayFromRelay: NostrEvent[] = []; let profileSetFromProducts: Set = new Set(); + console.log(relays); + console.log(filters); let h = pool.subscribeMany(relays, [filter], { onevent(event) { + console.log(event); productArrayFromRelay.push(event); if ( deletedProductsInCacheSet && @@ -60,7 +92,7 @@ export const fetchAllPosts = async ( ) { deletedProductsInCacheSet.delete(event.id); } - addProductToCache(event); + addProductToCache(parseTags(event)); profileSetFromProducts.add(event.pubkey); }, oneose() { @@ -71,8 +103,8 @@ export const fetchAllPosts = async ( const returnCall = () => { resolve({ profileSetFromProducts, + productArrayFromRelay, }); - editProductContext(productArrayFromRelay, false); removeProductFromCache(Array.from(deletedProductsInCacheSet)); }; } catch (error) { @@ -85,21 +117,12 @@ export const fetchAllPosts = async ( export const fetchProfile = async ( relays: string[], pubkeyProfilesToFetch: string[], - editProfileContext: ( - productEvents: Map, - isLoading: boolean, - ) => void, ): Promise<{ - profileMap: Map; + profileData: Map; }> => { return new Promise(async function (resolve, reject) { try { - try { - let profileData = await fetchProfileDataFromCache(); - editProfileContext(profileData, false); - } catch (error) { - console.log("Error: ", error); - } + let profileData = await fetchProfileDataFromCache(); const pool = new SimplePool(); let subParams: { kinds: number[]; authors?: string[] } = { @@ -107,20 +130,17 @@ export const fetchProfile = async ( authors: Array.from(pubkeyProfilesToFetch), }; - let profileMap: Map = new Map( - Array.from(pubkeyProfilesToFetch).map((pubkey) => [pubkey, null]), - ); - let h = pool.subscribeMany(relays, [subParams], { onevent(event) { if ( - profileMap.get(event.pubkey) === null || - profileMap.get(event.pubkey).created_at > event.created_at + !profileData.has(event.pubkey) || + (profileData.get(event.pubkey) as ProfileData).created_at > + event.created_at ) { // update only if the profile is not already set or the new event is newer try { const content = JSON.parse(event.content); - profileMap.set(event.pubkey, { + profileData.set(event.pubkey, { pubkey: event.pubkey, created_at: event.created_at, content: content, @@ -135,8 +155,8 @@ export const fetchProfile = async ( }, oneose() { h.close(); - resolve({ profileMap }); - addProfilesToCache(profileMap); + resolve({ profileData }); + addProfilesToCache(profileData); }, }); } catch (error) { @@ -148,15 +168,14 @@ export const fetchProfile = async ( export const fetchChatsAndMessages = async ( relays: string[], userPubkey: string, - editChatContext: (chatsMap: ChatsMap, isLoading: boolean) => void, ): Promise<{ profileSetFromChats: Set; + chatsData: ChatsMap; }> => { return new Promise(async function (resolve, reject) { // if no userPubkey, user is not signed in if (!userPubkey) { - editChatContext(new Map(), false); - resolve({ profileSetFromChats: new Set() }); + resolve({ profileSetFromChats: new Set(), chatsData: new Map() }); } let chatMessagesFromCache: Map = await fetchChatMessagesFromCache(); @@ -186,8 +205,10 @@ export const fetchChatsAndMessages = async ( a.created_at - b.created_at, ); }); - resolve({ profileSetFromChats: new Set(chatsMap.keys()) }); - editChatContext(chatsMap, false); + resolve({ + profileSetFromChats: new Set(chatsMap.keys()), + chatsData: chatsMap, + }); } }; @@ -223,7 +244,10 @@ export const fetchChatsAndMessages = async ( } addToChatsMap(receipientPubkey, chatMessage); if (incomingChatsReachedEOSE && outgoingChatsReachedEOSE) { - editChatContext(chatsMap, false); + resolve({ + profileSetFromChats: new Set(chatsMap.keys()), + chatsData: chatsMap, + }); } }, oneose() { @@ -253,7 +277,10 @@ export const fetchChatsAndMessages = async ( } addToChatsMap(senderPubkey, chatMessage); if (incomingChatsReachedEOSE && outgoingChatsReachedEOSE) { - editChatContext(chatsMap, false); + resolve({ + profileSetFromChats: new Set(chatsMap.keys()), + chatsData: chatsMap, + }); } }, async oneose() { diff --git a/pages/settings/user-profile.tsx b/pages/settings/user-profile.tsx index bc561d74..7cf087bf 100644 --- a/pages/settings/user-profile.tsx +++ b/pages/settings/user-profile.tsx @@ -140,7 +140,7 @@ const UserProfilePage = () => { isIconOnly={false} className={`absolute bottom-5 right-5 z-20 ${SHOPSTRBUTTONCLASSNAMES}`} passphrase={passphrase} - imgCallbackOnUpload={(imgUrl) => setValue("banner", imgUrl)} + imgCallbackOnUpload={(imgUrls) => setValue("banner", imgUrls[0])} > Upload Banner @@ -152,8 +152,8 @@ const UserProfilePage = () => { isIconOnly className={`absolute bottom-[-0.5rem] right-[-0.5rem] z-20 ${SHOPSTRBUTTONCLASSNAMES}`} passphrase={passphrase} - imgCallbackOnUpload={(imgUrl) => - setValue("picture", imgUrl) + imgCallbackOnUpload={(imgUrls) => + setValue("picture", imgUrls[0]) } > diff --git a/public/locationSelection.json b/public/locationSelection.json index 4be44aa7..5ee4e6fd 100644 --- a/public/locationSelection.json +++ b/public/locationSelection.json @@ -1,296 +1,1229 @@ -{ - "countries": [ - { "country": "Afghanistan", "iso3166": "af" }, - { "country": "Albania", "iso3166": "al" }, - { "country": "Algeria", "iso3166": "dz" }, - { "country": "American Samoa", "iso3166": "as" }, - { "country": "Andorra", "iso3166": "ad" }, - { "country": "Angola", "iso3166": "ao" }, - { "country": "Anguilla", "iso3166": "ai" }, - { "country": "Antigua & Barbuda", "iso3166": "ag" }, - { "country": "Argentina", "iso3166": "ar" }, - { "country": "Armenia", "iso3166": "am" }, - { "country": "Aruba", "iso3166": "aw" }, - { "country": "Australia", "iso3166": "au" }, - { "country": "Austria", "iso3166": "at" }, - { "country": "Azerbaijan", "iso3166": "az" }, - { "country": "Bahamas", "iso3166": "bs" }, - { "country": "Bahrain", "iso3166": "bh" }, - { "country": "Bangladesh", "iso3166": "bd" }, - { "country": "Barbados", "iso3166": "bb" }, - { "country": "Belarus", "iso3166": "by" }, - { "country": "Belgium", "iso3166": "be" }, - { "country": "Belize", "iso3166": "bz" }, - { "country": "Benin", "iso3166": "bj" }, - { "country": "Bermuda", "iso3166": "bm" }, - { "country": "Bhutan", "iso3166": "bt" }, - { "country": "Bolivia", "iso3166": "bo" }, - { "country": "Bonaire", "iso3166": "bq" }, - { "country": "Bosnia & Herzegovina", "iso3166": "ba" }, - { "country": "Botswana", "iso3166": "bw" }, - { "country": "Brazil", "iso3166": "br" }, - { "country": "British Indian Ocean Ter", "iso3166": "io" }, - { "country": "Brunei", "iso3166": "bn" }, - { "country": "Bulgaria", "iso3166": "bg" }, - { "country": "Burkina Faso", "iso3166": "bf" }, - { "country": "Burundi", "iso3166": "bi" }, - { "country": "Cambodia", "iso3166": "kh" }, - { "country": "Cameroon", "iso3166": "cm" }, - { "country": "Canada", "iso3166": "ca" }, - { "country": "Cape Verde", "iso3166": "cv" }, - { "country": "Cayman Islands", "iso3166": "ky" }, - { "country": "Central African Republic", "iso3166": "cf" }, - { "country": "Chad", "iso3166": "td" }, - { "country": "Channel Islands", "iso3166": "je" }, - { "country": "Chile", "iso3166": "cl" }, - { "country": "China", "iso3166": "cn" }, - { "country": "Christmas Island", "iso3166": "cx" }, - { "country": "Cocos Island", "iso3166": "cc" }, - { "country": "Colombia", "iso3166": "co" }, - { "country": "Comoros", "iso3166": "km" }, - { "country": "Congo", "iso3166": "cg" }, - { "country": "Cook Islands", "iso3166": "ck" }, - { "country": "Costa Rica", "iso3166": "cr" }, - { "country": "Cote D'Ivoire", "iso3166": "ci" }, - { "country": "Croatia", "iso3166": "hr" }, - { "country": "Cuba", "iso3166": "cu" }, - { "country": "Curacao", "iso3166": "cw" }, - { "country": "Cyprus", "iso3166": "cy" }, - { "country": "Czech Republic", "iso3166": "cz" }, - { "country": "Denmark", "iso3166": "dk" }, - { "country": "Djibouti", "iso3166": "dj" }, - { "country": "Dominica", "iso3166": "dm" }, - { "country": "Dominican Republic", "iso3166": "do" }, - { "country": "East Timor", "iso3166": "tm" }, - { "country": "Ecuador", "iso3166": "ec" }, - { "country": "Egypt", "iso3166": "eg" }, - { "country": "El Salvador", "iso3166": "sv" }, - { "country": "Equatorial Guinea", "iso3166": "gq" }, - { "country": "Eritrea", "iso3166": "er" }, - { "country": "Estonia", "iso3166": "ee" }, - { "country": "Ethiopia", "iso3166": "et" }, - { "country": "Falkland Islands", "iso3166": "fk" }, - { "country": "Faroe Islands", "iso3166": "fo" }, - { "country": "Fiji", "iso3166": "fj" }, - { "country": "Finland", "iso3166": "fi" }, - { "country": "France", "iso3166": "fr" }, - { "country": "French Guiana", "iso3166": "gf" }, - { "country": "French Polynesia", "iso3166": "pf" }, - { "country": "French Southern Ter", "iso3166": "tf" }, - { "country": "Gabon", "iso3166": "ga" }, - { "country": "Gambia", "iso3166": "gm" }, - { "country": "Georgia", "iso3166": "ge" }, - { "country": "Germany", "iso3166": "de" }, - { "country": "Ghana", "iso3166": "gh" }, - { "country": "Gibraltar", "iso3166": "gi" }, - { "country": "Great Britain", "iso3166": "gb" }, - { "country": "Greece", "iso3166": "gr" }, - { "country": "Greenland", "iso3166": "gl" }, - { "country": "Grenada", "iso3166": "gd" }, - { "country": "Guadeloupe", "iso3166": "gp" }, - { "country": "Guam", "iso3166": "gu" }, - { "country": "Guatemala", "iso3166": "gt" }, - { "country": "Guinea", "iso3166": "gn" }, - { "country": "Guyana", "iso3166": "gy" }, - { "country": "Haiti", "iso3166": "ht" }, - { "country": "Honduras", "iso3166": "hn" }, - { "country": "Hong Kong", "iso3166": "hk" }, - { "country": "Hungary", "iso3166": "hu" }, - { "country": "Iceland", "iso3166": "is" }, - { "country": "India", "iso3166": "in" }, - { "country": "Indonesia", "iso3166": "id" }, - { "country": "Iran", "iso3166": "ir" }, - { "country": "Iraq", "iso3166": "iq" }, - { "country": "Ireland", "iso3166": "ie" }, - { "country": "Isle of Man", "iso3166": "im" }, - { "country": "Israel", "iso3166": "il" }, - { "country": "Italy", "iso3166": "it" }, - { "country": "Jamaica", "iso3166": "jm" }, - { "country": "Japan", "iso3166": "jp" }, - { "country": "Jordan", "iso3166": "jo" }, - { "country": "Kazakhstan", "iso3166": "kz" }, - { "country": "Kenya", "iso3166": "ke" }, - { "country": "Kiribati", "iso3166": "ki" }, - { "country": "Korea North", "iso3166": "kp" }, - { "country": "Korea South", "iso3166": "kr" }, - { "country": "Kuwait", "iso3166": "kw" }, - { "country": "Kyrgyzstan", "iso3166": "kg" }, - { "country": "Laos", "iso3166": "la" }, - { "country": "Latvia", "iso3166": "lv" }, - { "country": "Lebanon", "iso3166": "lb" }, - { "country": "Lesotho", "iso3166": "ls" }, - { "country": "Liberia", "iso3166": "lr" }, - { "country": "Libya", "iso3166": "ly" }, - { "country": "Liechtenstein", "iso3166": "li" }, - { "country": "Lithuania", "iso3166": "lt" }, - { "country": "Luxembourg", "iso3166": "lu" }, - { "country": "Macau", "iso3166": "mo" }, - { "country": "Macedonia", "iso3166": "mk" }, - { "country": "Madagascar", "iso3166": "mg" }, - { "country": "Malaysia", "iso3166": "my" }, - { "country": "Malawi", "iso3166": "mw" }, - { "country": "Maldives", "iso3166": "mv" }, - { "country": "Mali", "iso3166": "ml" }, - { "country": "Malta", "iso3166": "mt" }, - { "country": "Marshall Islands", "iso3166": "mh" }, - { "country": "Martinique", "iso3166": "mq" }, - { "country": "Mauritania", "iso3166": "mr" }, - { "country": "Mauritius", "iso3166": "mu" }, - { "country": "Mayotte", "iso3166": "yt" }, - { "country": "Mexico", "iso3166": "mx" }, - { "country": "Moldova", "iso3166": "md" }, - { "country": "Monaco", "iso3166": "mc" }, - { "country": "Mongolia", "iso3166": "mn" }, - { "country": "Montserrat", "iso3166": "ms" }, - { "country": "Morocco", "iso3166": "ma" }, - { "country": "Mozambique", "iso3166": "mz" }, - { "country": "Myanmar", "iso3166": "mm" }, - { "country": "Namibia", "iso3166": "na" }, - { "country": "Nauru", "iso3166": "nr" }, - { "country": "Nepal", "iso3166": "np" }, - { "country": "Netherlands (Holland, Europe)", "iso3166": "nl" }, - { "country": "New Caledonia", "iso3166": "nc" }, - { "country": "New Zealand", "iso3166": "nz" }, - { "country": "Nicaragua", "iso3166": "ni" }, - { "country": "Niger", "iso3166": "ne" }, - { "country": "Nigeria", "iso3166": "ng" }, - { "country": "Niue", "iso3166": "nu" }, - { "country": "Norfolk Island", "iso3166": "nf" }, - { "country": "Norway", "iso3166": "no" }, - { "country": "Oman", "iso3166": "om" }, - { "country": "Pakistan", "iso3166": "pk" }, - { "country": "Palau Island", "iso3166": "pw" }, - { "country": "Palestine", "iso3166": "ps" }, - { "country": "Panama", "iso3166": "pa" }, - { "country": "Papua New Guinea", "iso3166": "pg" }, - { "country": "Paraguay", "iso3166": "py" }, - { "country": "Peru", "iso3166": "pe" }, - { "country": "Philippines", "iso3166": "ph" }, - { "country": "Pitcairn Island", "iso3166": "pn" }, - { "country": "Poland", "iso3166": "pl" }, - { "country": "Portugal", "iso3166": "pt" }, - { "country": "Puerto Rico", "iso3166": "pr" }, - { "country": "Qatar", "iso3166": "qa" }, - { "country": "Republic of Montenegro", "iso3166": "me" }, - { "country": "Republic of Serbia", "iso3166": "rs" }, - { "country": "Reunion", "iso3166": "re" }, - { "country": "Romania", "iso3166": "ro" }, - { "country": "Russia", "iso3166": "ru" }, - { "country": "Rwanda", "iso3166": "rw" }, - { "country": "St Barthelemy", "iso3166": "bl" }, - { "country": "St Eustatius", "iso3166": "bq" }, - { "country": "St Helena", "iso3166": "sh" }, - { "country": "St Kitts-Nevis", "iso3166": "kn" }, - { "country": "St Lucia", "iso3166": "lc" }, - { "country": "St Maarten", "iso3166": "sx" }, - { "country": "St Pierre & Miquelon", "iso3166": "pm" }, - { "country": "St Vincent & Grenadines", "iso3166": "vc" }, - { "country": "Saipan", "iso3166": "mp" }, - { "country": "Samoa", "iso3166": "ws" }, - { "country": "Samoa American", "iso3166": "as" }, - { "country": "San Marino", "iso3166": "sm" }, - { "country": "Sao Tome & Principe", "iso3166": "st" }, - { "country": "Saudi Arabia", "iso3166": "sa" }, - { "country": "Senegal", "iso3166": "sn" }, - { "country": "Seychelles", "iso3166": "sc" }, - { "country": "Sierra Leone", "iso3166": "sl" }, - { "country": "Singapore", "iso3166": "sg" }, - { "country": "Slovakia", "iso3166": "sk" }, - { "country": "Slovenia", "iso3166": "si" }, - { "country": "Solomon Islands", "iso3166": "sb" }, - { "country": "Somalia", "iso3166": "so" }, - { "country": "South Africa", "iso3166": "za" }, - { "country": "Spain", "iso3166": "es" }, - { "country": "Sri Lanka", "iso3166": "lk" }, - { "country": "Sudan", "iso3166": "sd" }, - { "country": "Suriname", "iso3166": "sr" }, - { "country": "Swaziland", "iso3166": "sz" }, - { "country": "Sweden", "iso3166": "se" }, - { "country": "Switzerland", "iso3166": "ch" }, - { "country": "Syria", "iso3166": "sy" }, - { "country": "Tahiti", "iso3166": "pf" }, - { "country": "Taiwan", "iso3166": "tw" }, - { "country": "Tajikistan", "iso3166": "tj" }, - { "country": "Tanzania", "iso3166": "tz" }, - { "country": "Thailand", "iso3166": "th" }, - { "country": "Togo", "iso3166": "tg" }, - { "country": "Tokelau", "iso3166": "tk" }, - { "country": "Tonga", "iso3166": "to" }, - { "country": "Trinidad & Tobago", "iso3166": "tt" }, - { "country": "Tunisia", "iso3166": "tn" }, - { "country": "Turkey", "iso3166": "tr" }, - { "country": "Turkmenistan", "iso3166": "tm" }, - { "country": "Turks & Caicos Is", "iso3166": "tc" }, - { "country": "Tuvalu", "iso3166": "tv" }, - { "country": "Uganda", "iso3166": "ug" }, - { "country": "Ukraine", "iso3166": "ua" }, - { "country": "United Arab Emirates", "iso3166": "ae" }, - { "country": "United Kingdom", "iso3166": "gb" }, - { "country": "United States of America", "iso3166": "us" }, - { "country": "Uruguay", "iso3166": "uy" }, - { "country": "Uzbekistan", "iso3166": "uz" }, - { "country": "Vanuatu", "iso3166": "vu" }, - { "country": "Vatican City State", "iso3166": "va" }, - { "country": "Venezuela", "iso3166": "ve" }, - { "country": "Vietnam", "iso3166": "vn" }, - { "country": "Virgin Islands (Brit)", "iso3166": "vg" }, - { "country": "Virgin Islands (USA)", "iso3166": "vi" }, - { "country": "Wake Island", "iso3166": "um" }, - { "country": "Wallis & Futana Is", "iso3166": "wf" }, - { "country": "Yemen", "iso3166": "ye" }, - { "country": "Zambia", "iso3166": "zm" }, - { "country": "Zimbabwe", "iso3166": "zw" } - ], - "states": [ - { "state": "Alabama", "iso3166": "us-al" }, - { "state": "Alaska", "iso3166": "us-ak" }, - { "state": "Arizona", "iso3166": "us-az" }, - { "state": "Arkansas", "iso3166": "us-ar" }, - { "state": "California", "iso3166": "us-ca" }, - { "state": "Colorado", "iso3166": "us-co" }, - { "state": "Connecticut", "iso3166": "us-ct" }, - { "state": "Delaware", "iso3166": "us-de" }, - { "state": "Florida", "iso3166": "us-fl" }, - { "state": "Georgia", "iso3166": "us-ga" }, - { "state": "Hawaii", "iso3166": "us-hi" }, - { "state": "Idaho", "iso3166": "us-id" }, - { "state": "Illinois", "iso3166": "us-il" }, - { "state": "Indiana", "iso3166": "us-in" }, - { "state": "Iowa", "iso3166": "us-ia" }, - { "state": "Kansas", "iso3166": "us-ks" }, - { "state": "Kentucky", "iso3166": "us-ky" }, - { "state": "Louisiana", "iso3166": "us-la" }, - { "state": "Maine", "iso3166": "us-me" }, - { "state": "Maryland", "iso3166": "us-md" }, - { "state": "Massachusetts", "iso3166": "us-ma" }, - { "state": "Michigan", "iso3166": "us-mi" }, - { "state": "Minnesota", "iso3166": "us-mn" }, - { "state": "Mississippi", "iso3166": "us-ms" }, - { "state": "Missouri", "iso3166": "us-mo" }, - { "state": "Montana", "iso3166": "us-mt" }, - { "state": "Nebraska", "iso3166": "us-ne" }, - { "state": "Nevada", "iso3166": "us-nv" }, - { "state": "New Hampshire", "iso3166": "us-nh" }, - { "state": "New Jersey", "iso3166": "us-nj" }, - { "state": "New Mexico", "iso3166": "us-nm" }, - { "state": "New York", "iso3166": "us-ny" }, - { "state": "North Carolina", "iso3166": "us-nc" }, - { "state": "North Dakota", "iso3166": "us-nd" }, - { "state": "Ohio", "iso3166": "us-oh" }, - { "state": "Oklahoma", "iso3166": "us-ok" }, - { "state": "Oregon", "iso3166": "us-or" }, - { "state": "Pennsylvania", "iso3166": "us-pa" }, - { "state": "Rhode Island", "iso3166": "us-ri" }, - { "state": "South Carolina", "iso3166": "us-sc" }, - { "state": "South Dakota", "iso3166": "us-sd" }, - { "state": "Tennessee", "iso3166": "us-tn" }, - { "state": "Texas", "iso3166": "us-tx" }, - { "state": "Utah", "iso3166": "us-ut" }, - { "state": "Vermont", "iso3166": "us-vt" }, - { "state": "Virginia", "iso3166": "us-va" }, - { "state": "Washington", "iso3166": "us-wa" }, - { "state": "West Virginia", "iso3166": "us-wv" }, - { "state": "Wisconsin", "iso3166": "us-wi" }, - { "state": "Wyoming", "iso3166": "us-wy" } - ] -} +[ + { + "section": "Countries", + "values": [ + { + "name": "Afghanistan", + "iso3166": "af" + }, + { + "name": "Albania", + "iso3166": "al" + }, + { + "name": "Algeria", + "iso3166": "dz" + }, + { + "name": "American Samoa", + "iso3166": "as" + }, + { + "name": "Andorra", + "iso3166": "ad" + }, + { + "name": "Angola", + "iso3166": "ao" + }, + { + "name": "Anguilla", + "iso3166": "ai" + }, + { + "name": "Antigua & Barbuda", + "iso3166": "ag" + }, + { + "name": "Argentina", + "iso3166": "ar" + }, + { + "name": "Armenia", + "iso3166": "am" + }, + { + "name": "Aruba", + "iso3166": "aw" + }, + { + "name": "Australia", + "iso3166": "au" + }, + { + "name": "Austria", + "iso3166": "at" + }, + { + "name": "Azerbaijan", + "iso3166": "az" + }, + { + "name": "Bahamas", + "iso3166": "bs" + }, + { + "name": "Bahrain", + "iso3166": "bh" + }, + { + "name": "Bangladesh", + "iso3166": "bd" + }, + { + "name": "Barbados", + "iso3166": "bb" + }, + { + "name": "Belarus", + "iso3166": "by" + }, + { + "name": "Belgium", + "iso3166": "be" + }, + { + "name": "Belize", + "iso3166": "bz" + }, + { + "name": "Benin", + "iso3166": "bj" + }, + { + "name": "Bermuda", + "iso3166": "bm" + }, + { + "name": "Bhutan", + "iso3166": "bt" + }, + { + "name": "Bolivia", + "iso3166": "bo" + }, + { + "name": "Bonaire", + "iso3166": "bq" + }, + { + "name": "Bosnia & Herzegovina", + "iso3166": "ba" + }, + { + "name": "Botswana", + "iso3166": "bw" + }, + { + "name": "Brazil", + "iso3166": "br" + }, + { + "name": "British Indian Ocean Ter", + "iso3166": "io" + }, + { + "name": "Brunei", + "iso3166": "bn" + }, + { + "name": "Bulgaria", + "iso3166": "bg" + }, + { + "name": "Burkina Faso", + "iso3166": "bf" + }, + { + "name": "Burundi", + "iso3166": "bi" + }, + { + "name": "Cambodia", + "iso3166": "kh" + }, + { + "name": "Cameroon", + "iso3166": "cm" + }, + { + "name": "Canada", + "iso3166": "ca" + }, + { + "name": "Cape Verde", + "iso3166": "cv" + }, + { + "name": "Cayman Islands", + "iso3166": "ky" + }, + { + "name": "Central African Republic", + "iso3166": "cf" + }, + { + "name": "Chad", + "iso3166": "td" + }, + { + "name": "Channel Islands", + "iso3166": "je" + }, + { + "name": "Chile", + "iso3166": "cl" + }, + { + "name": "China", + "iso3166": "cn" + }, + { + "name": "Christmas Island", + "iso3166": "cx" + }, + { + "name": "Cocos Island", + "iso3166": "cc" + }, + { + "name": "Colombia", + "iso3166": "co" + }, + { + "name": "Comoros", + "iso3166": "km" + }, + { + "name": "Congo", + "iso3166": "cg" + }, + { + "name": "Cook Islands", + "iso3166": "ck" + }, + { + "name": "Costa Rica", + "iso3166": "cr" + }, + { + "name": "Cote D'Ivoire", + "iso3166": "ci" + }, + { + "name": "Croatia", + "iso3166": "hr" + }, + { + "name": "Cuba", + "iso3166": "cu" + }, + { + "name": "Curacao", + "iso3166": "cw" + }, + { + "name": "Cyprus", + "iso3166": "cy" + }, + { + "name": "Czech Republic", + "iso3166": "cz" + }, + { + "name": "Denmark", + "iso3166": "dk" + }, + { + "name": "Djibouti", + "iso3166": "dj" + }, + { + "name": "Dominica", + "iso3166": "dm" + }, + { + "name": "Dominican Republic", + "iso3166": "do" + }, + { + "name": "East Timor", + "iso3166": "tm" + }, + { + "name": "Ecuador", + "iso3166": "ec" + }, + { + "name": "Egypt", + "iso3166": "eg" + }, + { + "name": "El Salvador", + "iso3166": "sv" + }, + { + "name": "Equatorial Guinea", + "iso3166": "gq" + }, + { + "name": "Eritrea", + "iso3166": "er" + }, + { + "name": "Estonia", + "iso3166": "ee" + }, + { + "name": "Ethiopia", + "iso3166": "et" + }, + { + "name": "Falkland Islands", + "iso3166": "fk" + }, + { + "name": "Faroe Islands", + "iso3166": "fo" + }, + { + "name": "Fiji", + "iso3166": "fj" + }, + { + "name": "Finland", + "iso3166": "fi" + }, + { + "name": "France", + "iso3166": "fr" + }, + { + "name": "French Guiana", + "iso3166": "gf" + }, + { + "name": "French Polynesia", + "iso3166": "pf" + }, + { + "name": "French Southern Ter", + "iso3166": "tf" + }, + { + "name": "Gabon", + "iso3166": "ga" + }, + { + "name": "Gambia", + "iso3166": "gm" + }, + { + "name": "Georgia", + "iso3166": "ge" + }, + { + "name": "Germany", + "iso3166": "de" + }, + { + "name": "Ghana", + "iso3166": "gh" + }, + { + "name": "Gibraltar", + "iso3166": "gi" + }, + { + "name": "Great Britain", + "iso3166": "gb" + }, + { + "name": "Greece", + "iso3166": "gr" + }, + { + "name": "Greenland", + "iso3166": "gl" + }, + { + "name": "Grenada", + "iso3166": "gd" + }, + { + "name": "Guadeloupe", + "iso3166": "gp" + }, + { + "name": "Guam", + "iso3166": "gu" + }, + { + "name": "Guatemala", + "iso3166": "gt" + }, + { + "name": "Guinea", + "iso3166": "gn" + }, + { + "name": "Guyana", + "iso3166": "gy" + }, + { + "name": "Haiti", + "iso3166": "ht" + }, + { + "name": "Honduras", + "iso3166": "hn" + }, + { + "name": "Hong Kong", + "iso3166": "hk" + }, + { + "name": "Hungary", + "iso3166": "hu" + }, + { + "name": "Iceland", + "iso3166": "is" + }, + { + "name": "India", + "iso3166": "in" + }, + { + "name": "Indonesia", + "iso3166": "id" + }, + { + "name": "Iran", + "iso3166": "ir" + }, + { + "name": "Iraq", + "iso3166": "iq" + }, + { + "name": "Ireland", + "iso3166": "ie" + }, + { + "name": "Isle of Man", + "iso3166": "im" + }, + { + "name": "Israel", + "iso3166": "il" + }, + { + "name": "Italy", + "iso3166": "it" + }, + { + "name": "Jamaica", + "iso3166": "jm" + }, + { + "name": "Japan", + "iso3166": "jp" + }, + { + "name": "Jordan", + "iso3166": "jo" + }, + { + "name": "Kazakhstan", + "iso3166": "kz" + }, + { + "name": "Kenya", + "iso3166": "ke" + }, + { + "name": "Kiribati", + "iso3166": "ki" + }, + { + "name": "Korea North", + "iso3166": "kp" + }, + { + "name": "Korea South", + "iso3166": "kr" + }, + { + "name": "Kuwait", + "iso3166": "kw" + }, + { + "name": "Kyrgyzstan", + "iso3166": "kg" + }, + { + "name": "Laos", + "iso3166": "la" + }, + { + "name": "Latvia", + "iso3166": "lv" + }, + { + "name": "Lebanon", + "iso3166": "lb" + }, + { + "name": "Lesotho", + "iso3166": "ls" + }, + { + "name": "Liberia", + "iso3166": "lr" + }, + { + "name": "Libya", + "iso3166": "ly" + }, + { + "name": "Liechtenstein", + "iso3166": "li" + }, + { + "name": "Lithuania", + "iso3166": "lt" + }, + { + "name": "Luxembourg", + "iso3166": "lu" + }, + { + "name": "Macau", + "iso3166": "mo" + }, + { + "name": "Macedonia", + "iso3166": "mk" + }, + { + "name": "Madagascar", + "iso3166": "mg" + }, + { + "name": "Malaysia", + "iso3166": "my" + }, + { + "name": "Malawi", + "iso3166": "mw" + }, + { + "name": "Maldives", + "iso3166": "mv" + }, + { + "name": "Mali", + "iso3166": "ml" + }, + { + "name": "Malta", + "iso3166": "mt" + }, + { + "name": "Marshall Islands", + "iso3166": "mh" + }, + { + "name": "Martinique", + "iso3166": "mq" + }, + { + "name": "Mauritania", + "iso3166": "mr" + }, + { + "name": "Mauritius", + "iso3166": "mu" + }, + { + "name": "Mayotte", + "iso3166": "yt" + }, + { + "name": "Mexico", + "iso3166": "mx" + }, + { + "name": "Moldova", + "iso3166": "md" + }, + { + "name": "Monaco", + "iso3166": "mc" + }, + { + "name": "Mongolia", + "iso3166": "mn" + }, + { + "name": "Montserrat", + "iso3166": "ms" + }, + { + "name": "Morocco", + "iso3166": "ma" + }, + { + "name": "Mozambique", + "iso3166": "mz" + }, + { + "name": "Myanmar", + "iso3166": "mm" + }, + { + "name": "Namibia", + "iso3166": "na" + }, + { + "name": "Nauru", + "iso3166": "nr" + }, + { + "name": "Nepal", + "iso3166": "np" + }, + { + "name": "Netherlands", + "iso3166": "nl" + }, + { + "name": "New Caledonia", + "iso3166": "nc" + }, + { + "name": "New Zealand", + "iso3166": "nz" + }, + { + "name": "Nicaragua", + "iso3166": "ni" + }, + { + "name": "Niger", + "iso3166": "ne" + }, + { + "name": "Nigeria", + "iso3166": "ng" + }, + { + "name": "Niue", + "iso3166": "nu" + }, + { + "name": "Norfolk Island", + "iso3166": "nf" + }, + { + "name": "Norway", + "iso3166": "no" + }, + { + "name": "Oman", + "iso3166": "om" + }, + { + "name": "Pakistan", + "iso3166": "pk" + }, + { + "name": "Palau Island", + "iso3166": "pw" + }, + { + "name": "Palestine", + "iso3166": "ps" + }, + { + "name": "Panama", + "iso3166": "pa" + }, + { + "name": "Papua New Guinea", + "iso3166": "pg" + }, + { + "name": "Paraguay", + "iso3166": "py" + }, + { + "name": "Peru", + "iso3166": "pe" + }, + { + "name": "Philippines", + "iso3166": "ph" + }, + { + "name": "Pitcairn Island", + "iso3166": "pn" + }, + { + "name": "Poland", + "iso3166": "pl" + }, + { + "name": "Portugal", + "iso3166": "pt" + }, + { + "name": "Puerto Rico", + "iso3166": "pr" + }, + { + "name": "Qatar", + "iso3166": "qa" + }, + { + "name": "Republic of Montenegro", + "iso3166": "me" + }, + { + "name": "Republic of Serbia", + "iso3166": "rs" + }, + { + "name": "Reunion", + "iso3166": "re" + }, + { + "name": "Romania", + "iso3166": "ro" + }, + { + "name": "Russia", + "iso3166": "ru" + }, + { + "name": "Rwanda", + "iso3166": "rw" + }, + { + "name": "St Barthelemy", + "iso3166": "bl" + }, + { + "name": "St Eustatius", + "iso3166": "bq" + }, + { + "name": "St Helena", + "iso3166": "sh" + }, + { + "name": "St Kitts-Nevis", + "iso3166": "kn" + }, + { + "name": "St Lucia", + "iso3166": "lc" + }, + { + "name": "St Maarten", + "iso3166": "sx" + }, + { + "name": "St Pierre & Miquelon", + "iso3166": "pm" + }, + { + "name": "St Vincent & Grenadines", + "iso3166": "vc" + }, + { + "name": "Saipan", + "iso3166": "mp" + }, + { + "name": "Samoa", + "iso3166": "ws" + }, + { + "name": "Samoa American", + "iso3166": "as" + }, + { + "name": "San Marino", + "iso3166": "sm" + }, + { + "name": "Sao Tome & Principe", + "iso3166": "st" + }, + { + "name": "Saudi Arabia", + "iso3166": "sa" + }, + { + "name": "Senegal", + "iso3166": "sn" + }, + { + "name": "Seychelles", + "iso3166": "sc" + }, + { + "name": "Sierra Leone", + "iso3166": "sl" + }, + { + "name": "Singapore", + "iso3166": "sg" + }, + { + "name": "Slovakia", + "iso3166": "sk" + }, + { + "name": "Slovenia", + "iso3166": "si" + }, + { + "name": "Solomon Islands", + "iso3166": "sb" + }, + { + "name": "Somalia", + "iso3166": "so" + }, + { + "name": "South Africa", + "iso3166": "za" + }, + { + "name": "Spain", + "iso3166": "es" + }, + { + "name": "Sri Lanka", + "iso3166": "lk" + }, + { + "name": "Sudan", + "iso3166": "sd" + }, + { + "name": "Suriname", + "iso3166": "sr" + }, + { + "name": "Swaziland", + "iso3166": "sz" + }, + { + "name": "Sweden", + "iso3166": "se" + }, + { + "name": "Switzerland", + "iso3166": "ch" + }, + { + "name": "Syria", + "iso3166": "sy" + }, + { + "name": "Tahiti", + "iso3166": "pf" + }, + { + "name": "Taiwan", + "iso3166": "tw" + }, + { + "name": "Tajikistan", + "iso3166": "tj" + }, + { + "name": "Tanzania", + "iso3166": "tz" + }, + { + "name": "Thailand", + "iso3166": "th" + }, + { + "name": "Togo", + "iso3166": "tg" + }, + { + "name": "Tokelau", + "iso3166": "tk" + }, + { + "name": "Tonga", + "iso3166": "to" + }, + { + "name": "Trinidad & Tobago", + "iso3166": "tt" + }, + { + "name": "Tunisia", + "iso3166": "tn" + }, + { + "name": "Turkey", + "iso3166": "tr" + }, + { + "name": "Turkmenistan", + "iso3166": "tm" + }, + { + "name": "Turks & Caicos Is", + "iso3166": "tc" + }, + { + "name": "Tuvalu", + "iso3166": "tv" + }, + { + "name": "Uganda", + "iso3166": "ug" + }, + { + "name": "Ukraine", + "iso3166": "ua" + }, + { + "name": "United Arab Emirates", + "iso3166": "ae" + }, + { + "name": "United Kingdom", + "iso3166": "gb" + }, + { + "name": "United States of America", + "iso3166": "us" + }, + { + "name": "Uruguay", + "iso3166": "uy" + }, + { + "name": "Uzbekistan", + "iso3166": "uz" + }, + { + "name": "Vanuatu", + "iso3166": "vu" + }, + { + "name": "Vatican City State", + "iso3166": "va" + }, + { + "name": "Venezuela", + "iso3166": "ve" + }, + { + "name": "Vietnam", + "iso3166": "vn" + }, + { + "name": "Virgin Islands (Brit)", + "iso3166": "vg" + }, + { + "name": "Virgin Islands (USA)", + "iso3166": "vi" + }, + { + "name": "Wake Island", + "iso3166": "um" + }, + { + "name": "Wallis & Futana Is", + "iso3166": "wf" + }, + { + "name": "Yemen", + "iso3166": "ye" + }, + { + "name": "Zambia", + "iso3166": "zm" + }, + { + "name": "Zimbabwe", + "iso3166": "zw" + } + ] + }, + { + "section": "U.S. States", + "values": [ + { + "name": "Alabama", + "iso3166": "us-al" + }, + { + "name": "Alaska", + "iso3166": "us-ak" + }, + { + "name": "Arizona", + "iso3166": "us-az" + }, + { + "name": "Arkansas", + "iso3166": "us-ar" + }, + { + "name": "California", + "iso3166": "us-ca" + }, + { + "name": "Colorado", + "iso3166": "us-co" + }, + { + "name": "Connecticut", + "iso3166": "us-ct" + }, + { + "name": "Delaware", + "iso3166": "us-de" + }, + { + "name": "Florida", + "iso3166": "us-fl" + }, + { + "name": "Georgia", + "iso3166": "us-ga" + }, + { + "name": "Hawaii", + "iso3166": "us-hi" + }, + { + "name": "Idaho", + "iso3166": "us-id" + }, + { + "name": "Illinois", + "iso3166": "us-il" + }, + { + "name": "Indiana", + "iso3166": "us-in" + }, + { + "name": "Iowa", + "iso3166": "us-ia" + }, + { + "name": "Kansas", + "iso3166": "us-ks" + }, + { + "name": "Kentucky", + "iso3166": "us-ky" + }, + { + "name": "Louisiana", + "iso3166": "us-la" + }, + { + "name": "Maine", + "iso3166": "us-me" + }, + { + "name": "Maryland", + "iso3166": "us-md" + }, + { + "name": "Massachusetts", + "iso3166": "us-ma" + }, + { + "name": "Michigan", + "iso3166": "us-mi" + }, + { + "name": "Minnesota", + "iso3166": "us-mn" + }, + { + "name": "Mississippi", + "iso3166": "us-ms" + }, + { + "name": "Missouri", + "iso3166": "us-mo" + }, + { + "name": "Montana", + "iso3166": "us-mt" + }, + { + "name": "Nebraska", + "iso3166": "us-ne" + }, + { + "name": "Nevada", + "iso3166": "us-nv" + }, + { + "name": "New Hampshire", + "iso3166": "us-nh" + }, + { + "name": "New Jersey", + "iso3166": "us-nj" + }, + { + "name": "New Mexico", + "iso3166": "us-nm" + }, + { + "name": "New York", + "iso3166": "us-ny" + }, + { + "name": "North Carolina", + "iso3166": "us-nc" + }, + { + "name": "North Dakota", + "iso3166": "us-nd" + }, + { + "name": "Ohio", + "iso3166": "us-oh" + }, + { + "name": "Oklahoma", + "iso3166": "us-ok" + }, + { + "name": "Oregon", + "iso3166": "us-or" + }, + { + "name": "Pennsylvania", + "iso3166": "us-pa" + }, + { + "name": "Rhode Island", + "iso3166": "us-ri" + }, + { + "name": "South Carolina", + "iso3166": "us-sc" + }, + { + "name": "South Dakota", + "iso3166": "us-sd" + }, + { + "name": "Tennessee", + "iso3166": "us-tn" + }, + { + "name": "Texas", + "iso3166": "us-tx" + }, + { + "name": "Utah", + "iso3166": "us-ut" + }, + { + "name": "Vermont", + "iso3166": "us-vt" + }, + { + "name": "Virginia", + "iso3166": "us-va" + }, + { + "name": "Washington", + "iso3166": "us-wa" + }, + { + "name": "West Virginia", + "iso3166": "us-wv" + }, + { + "name": "Wisconsin", + "iso3166": "us-wi" + }, + { + "name": "Wyoming", + "iso3166": "us-wy" + } + ] + }, + { + "section": "Canada Provinces/Territories", + "values": [ + { + "name": "Alberta", + "iso3166": "ca" + }, + { + "name": "British Columbia", + "iso3166": "ca" + }, + { + "name": "Manitoba", + "iso3166": "ca" + }, + { + "name": "New Brunswick", + "iso3166": "ca" + }, + { + "name": "Newfoundland and Labrador", + "iso3166": "ca" + }, + { + "name": "Northwest Territories", + "iso3166": "ca" + }, + { + "name": "Nova Scotia", + "iso3166": "ca" + }, + { + "name": "Nunavut", + "iso3166": "ca" + }, + { + "name": "Ontario", + "iso3166": "ca" + }, + { + "name": "Prince Edward Island", + "iso3166": "ca" + }, + { + "name": "Québec", + "iso3166": "ca" + }, + { + "name": "Saskatchewan", + "iso3166": "ca" + }, + { + "name": "Yukon", + "iso3166": "ca" + } + ] + } +] diff --git a/tailwind.config.ts b/tailwind.config.ts index f7b4c74e..113b1560 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -169,6 +169,27 @@ const config: Config = { }, ], darkMode: "class", - plugins: [nextui()], + plugins: [ + nextui({ + themes: { + light: { + colors: { + focus: "#a655f7", + primary: { + DEFAULT: "#a655f7", + }, + }, + }, + dark: { + colors: { + focus: "#fcd34d", + primary: { + DEFAULT: "#fcd34d", + }, + }, + }, + }, + }), + ], }; export default config; diff --git a/utils/context/context.ts b/utils/context/context.ts index bd2ff540..07884e94 100644 --- a/utils/context/context.ts +++ b/utils/context/context.ts @@ -1,30 +1,60 @@ import { createContext } from "react"; -import { NostrMessageEvent, ProfileData } from "../types/types"; +import { NostrEvent, NostrMessageEvent, ProfileData } from "../types/types"; +import { ProductData } from "@/components/utility/product-parser-functions"; export interface ProfileContextInterface { - profileData: Map; + profileData: Map; isLoading: boolean; updateProfileData: (profileData: ProfileData) => void; } -export const ProfileMapContext = createContext({ - profileData: new Map(), +export const ProfileMapContext = createContext({ + profileData: new Map(), isLoading: true, -} as ProfileContextInterface); + updateProfileData: () => {}, +}); export interface ProductContextInterface { - productEvents: any; + productEvents: ProductData[]; isLoading: boolean; - addNewlyCreatedProductEvent: (productEvent: any) => void; + setIsLoading: (isLoading: boolean) => void; + filters: { + searchQuery: string; + categories: Set; + location: string | null; + }; + setFilters: (filters: ProductContextInterface["filters"]) => void; + addNewlyCreatedProductEvents: (productEvents: ProductData[], replace?: boolean) => void; removeDeletedProductEvent: (productId: string) => void; } -export const ProductContext = createContext({ - productEvents: {}, +export const ProductContext = createContext({ + productEvents: [], isLoading: true, - addNewlyCreatedProductEvent: (productEvent: any) => {}, - removeDeletedProductEvent: (productId: string) => {}, -} as ProductContextInterface); + setIsLoading: () => {}, + filters: { + searchQuery: "", + categories: new Set([]), + location: null, + }, + setFilters: () => {}, + addNewlyCreatedProductEvents: () => {}, + removeDeletedProductEvent: () => {}, +}); + +export const MyListingsContext = createContext({ + productEvents: [], + isLoading: true, + setIsLoading: () => {}, + filters: { + searchQuery: "", + categories: new Set([]), + location: null, + }, + setFilters: () => {}, + addNewlyCreatedProductEvents: () => {}, + removeDeletedProductEvent: () => {}, +}); export type ChatsMap = Map; @@ -33,7 +63,7 @@ export interface ChatsContextInterface { isLoading: boolean; } -export const ChatsContext = createContext({ - chatsMap: new Map(), +export const ChatsContext = createContext({ + chatsMap: new Map(), isLoading: true, -} as ChatsContextInterface); +}); diff --git a/utils/location/location.ts b/utils/location/location.ts new file mode 100644 index 00000000..44cb120c --- /dev/null +++ b/utils/location/location.ts @@ -0,0 +1,62 @@ +import { ProductFormValues } from "@/pages/api/nostr/post-event"; +import locations from "../../public/locationSelection.json"; + +let codeToNameMap: { [key: string]: string }; +const buildCodeToNameMap = () => { + const map = {}; + locations.forEach(l => l.values.forEach(v => Object.assign(map, { [v.iso3166.toLowerCase()]: v.name }))) + return map; +} +export const getCodeToNameMap = (code: string) => { + if (!codeToNameMap) { + codeToNameMap = buildCodeToNameMap(); + } + return codeToNameMap[code.toLowerCase()]; +} + +let nameToCodeMap: { [key: string]: string }; +const buildNameToCodeMap = () => { + const map = {}; + locations.forEach(l => l.values.forEach(v => Object.assign(map, { [v.name.toUpperCase()]: v.iso3166 }))) + return map; +} +export const getNameToCodeMap = (name: string) => { + if (!nameToCodeMap) { + nameToCodeMap = buildNameToCodeMap(); + } + return nameToCodeMap[name.toUpperCase()]; +} + +export const buildListingGeotags = ({ + iso3166, +}: { + iso3166: string; +}) => { + let countryCode: string; + let countryName: string; + let regionCode: string; + let regionName: string; + + if (iso3166.includes("-")) { + countryCode = iso3166.split("-")[0]; + regionCode = iso3166; + countryName = getCodeToNameMap(countryCode); + regionName = getCodeToNameMap(regionCode); + } else { + countryCode = iso3166; + regionCode = ""; + countryName = getCodeToNameMap(countryCode); + regionName = ""; + } + return [ + ["G", "countryCode"], + ["g", countryCode, "countryCode"], + ["G", "regionCode"], + ["g", regionCode, "regionCode"], + ["G", "countryName"], + ["g", countryName, "countryName"], + ["G", "regionName"], + ["g", regionName, "regionName"], + ] as ProductFormValues; +}; + diff --git a/utils/text.ts b/utils/text.ts new file mode 100644 index 00000000..cbd2a823 --- /dev/null +++ b/utils/text.ts @@ -0,0 +1,13 @@ +import keyword_extractor from "keyword-extractor"; + +export const REMOVE_URL_REGEX = + /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/g; + +export const getKeywords = (text: string) => { + return keyword_extractor.extract(text.replace(REMOVE_URL_REGEX, ""), { + language: "en", + remove_digits: true, + remove_duplicates: true, + return_changed_case: true, + }); +}; From 9c27f6c278e3bcda87da261a9076211078d043ca Mon Sep 17 00:00:00 2001 From: ericspaghetti <12552123+ericspaghetti@users.noreply.github.com> Date: Mon, 11 Mar 2024 11:20:48 -0700 Subject: [PATCH 2/2] use s tags for search index --- components/display-products.tsx | 18 +++++------------- components/product-form.tsx | 4 ++-- pages/api/nostr/cache-service.ts | 1 - pages/api/nostr/fetch-service.ts | 20 +++++--------------- utils/context/context.ts | 7 +++++-- utils/text.ts | 4 +--- 6 files changed, 18 insertions(+), 36 deletions(-) diff --git a/components/display-products.tsx b/components/display-products.tsx index f8169a58..06022e67 100644 --- a/components/display-products.tsx +++ b/components/display-products.tsx @@ -109,26 +109,18 @@ const DisplayEvents = ({ const pool = new SimplePool(); - const buildTagsFilters: string[] = []; - if (productEventContext.filters.categories.size > 0) { - buildTagsFilters.push( - ...Array.from(productEventContext.filters.categories), - ); - } - if (productEventContext.filters.searchQuery.length > 0) { - buildTagsFilters.push( - ...getKeywords(productEventContext.filters.searchQuery), - ); - } const filter: Filter = { kinds: [30402], since, until: oldestListingCreatedAt, + ...(productEventContext.filters.searchQuery.length > 0 && { + "#s": getKeywords(productEventContext.filters.searchQuery), + }), ...(productEventContext.filters.location && { "#g": [getNameToCodeMap(productEventContext.filters.location)], }), - ...(buildTagsFilters.length > 0 && { - "#t": buildTagsFilters, + ...(productEventContext.filters.categories.size > 0 && { + "#t": Array.from(productEventContext.filters.categories), }), }; const events = await pool.querySync(getLocalStorageData().relays, filter); diff --git a/components/product-form.tsx b/components/product-form.tsx index c7174169..5600e510 100644 --- a/components/product-form.tsx +++ b/components/product-form.tsx @@ -168,13 +168,13 @@ export default function NewForm({ }); data["Categories"].forEach((category) => { - tags.push(["t", category, "category"]); + tags.push(["t", category]); }); // Relay search (NIP-50) not widespread enough, use tags instead for relay querying getKeywords(data["Product Name"] + " " + data["Description"]).forEach( (keyword) => { - tags.push(["t", keyword, "keyword"]); + tags.push(["s", keyword]); }, ); diff --git a/pages/api/nostr/cache-service.ts b/pages/api/nostr/cache-service.ts index ec0d2e85..b27918ad 100644 --- a/pages/api/nostr/cache-service.ts +++ b/pages/api/nostr/cache-service.ts @@ -1,7 +1,6 @@ import { ProductData } from "@/components/utility/product-parser-functions"; import { ItemType, - NostrEvent, NostrMessageEvent, ProfileData, } from "../../../utils/types/types"; diff --git a/pages/api/nostr/fetch-service.ts b/pages/api/nostr/fetch-service.ts index 780f6d89..f28e33d6 100644 --- a/pages/api/nostr/fetch-service.ts +++ b/pages/api/nostr/fetch-service.ts @@ -19,7 +19,6 @@ import { getNameToCodeMap } from "@/utils/location/location"; import parseTags, { ProductData, } from "@/components/utility/product-parser-functions"; -import keyword_extractor from "keyword-extractor"; import { getKeywords } from "@/utils/text"; export const fetchAllPosts = async ( @@ -52,15 +51,6 @@ export const fetchAllPosts = async ( until = Math.trunc(DateTime.now().toSeconds()); } - const buildTagsFilters: string[] = []; - if (filters.categories.size > 0) { - buildTagsFilters.push(...Array.from(filters.categories)); - } - if (filters.searchQuery.length > 0) { - buildTagsFilters.push( - ...getKeywords(filters.searchQuery), - ); - } const filter: Filter = { kinds: [30402], since, @@ -69,22 +59,22 @@ export const fetchAllPosts = async ( // ...(filters.searchQuery.length > 0 && { // search: filters.searchQuery, // }), + ...(filters.searchQuery.length > 0 && { + "#s": getKeywords(filters.searchQuery), + }), ...(filters.location && { "#g": [getNameToCodeMap(filters.location)], }), - ...(buildTagsFilters.length > 0 && { - "#t": buildTagsFilters, + ...(filters.categories.size > 0 && { + "#t": Array.from(filters.categories), }), }; let productArrayFromRelay: NostrEvent[] = []; let profileSetFromProducts: Set = new Set(); - console.log(relays); - console.log(filters); let h = pool.subscribeMany(relays, [filter], { onevent(event) { - console.log(event); productArrayFromRelay.push(event); if ( deletedProductsInCacheSet && diff --git a/utils/context/context.ts b/utils/context/context.ts index 07884e94..0ae4bb76 100644 --- a/utils/context/context.ts +++ b/utils/context/context.ts @@ -1,5 +1,5 @@ import { createContext } from "react"; -import { NostrEvent, NostrMessageEvent, ProfileData } from "../types/types"; +import { NostrMessageEvent, ProfileData } from "../types/types"; import { ProductData } from "@/components/utility/product-parser-functions"; export interface ProfileContextInterface { @@ -24,7 +24,10 @@ export interface ProductContextInterface { location: string | null; }; setFilters: (filters: ProductContextInterface["filters"]) => void; - addNewlyCreatedProductEvents: (productEvents: ProductData[], replace?: boolean) => void; + addNewlyCreatedProductEvents: ( + productEvents: ProductData[], + replace?: boolean, + ) => void; removeDeletedProductEvent: (productId: string) => void; } diff --git a/utils/text.ts b/utils/text.ts index cbd2a823..bd25a18b 100644 --- a/utils/text.ts +++ b/utils/text.ts @@ -1,8 +1,6 @@ import keyword_extractor from "keyword-extractor"; -export const REMOVE_URL_REGEX = - /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/g; - +export const REMOVE_URL_REGEX = /https?.*?(?= |$)/g; export const getKeywords = (text: string) => { return keyword_extractor.extract(text.replace(REMOVE_URL_REGEX, ""), { language: "en",