Compare commits

...

7 Commits

Author SHA1 Message Date
Keith Irwin a7c5fd2489
Revert "Replaced all instances of sid and pid"
This reverts commit d7cb83ec70.
2023-05-06 14:01:08 -06:00
Keith Irwin 79da1842c8
Moved products from files to db 2023-05-06 07:45:48 -06:00
Keith Irwin 31fb820e9c
Replaced all instances of sid and pid 2023-05-02 15:07:52 -06:00
Keith Irwin 7c35fc4b31
Created new models 2023-05-02 14:49:49 -06:00
Keith Irwin 6cec722430
Moved orders to DB 2023-04-28 12:22:47 -06:00
Keith Irwin a168d4b9f8
Added db persistence 2023-04-24 14:18:01 -06:00
Keith Irwin 2d86de1fc8
Installed mongo
npm update
npm install mongoose
2023-04-24 13:45:59 -06:00
116 changed files with 1081 additions and 2794 deletions

View File

@ -6,6 +6,14 @@ STRIPE_LISTENER_PORT="8082"
# Domains
SITE_DOMAIN="https://www.example.com"
API_DOMAIN="https://api.example.com"
API_DOMAIN_INTERNAL="http://my_api"
# Database
DB_HOST="my_db"
DB_NAME="myapp"
DB_PORT="27017"
DB_USER="myapp"
DB_PASS="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
# Email recipients
ADMIN_EMAIL="sysadmin@example.com"
@ -25,7 +33,7 @@ NTFY_DOMAIN="https://ntfy.example.com"
NTFY_TOPIC="orders-paid_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
# Monero
MONEROPAY_URL="http://localhost:5000"
MONEROPAY_URL="http://my_moneropay:5000"
MONEROD_URL="node.supportxmr.com:18081"
MONERO_PRICE_LEEWAY="0.02"
MONERO_PRICECHECK_SEC="600"

2
.gitignore vendored
View File

@ -3,8 +3,8 @@ node_modules/
# environment and data
.env
db/
docker-compose.yml
orders/*
sold/*
view-wallet/*
moneropay-db/*

View File

@ -1,3 +1,4 @@
'use strict'
require('dotenv').config()
module.exports = {
STRIPE_PUB: process.env.STRIPE_PUB,

67
_src/_data/listings.js Normal file
View File

@ -0,0 +1,67 @@
'use strict'
require('dotenv').config()
const mongoose = require('mongoose')
mongoose.connect('mongodb://'
+ process.env.DB_USER + ':'
+ process.env.DB_PASS + '@'
+ process.env.DB_HOST + ':'
+ process.env.DB_PORT + '/'
+ process.env.DB_NAME )
const models = require('../../api/models')
module.exports = async () => {
let listings; try {
listings = await Promise.all([
new Promise(async (resolve, reject) => {
let units; try {
units = await models.Unit.find({
listed: true,
soldDate: null,
}).populate('product')
} catch (err) { reject(err) }
resolve(units.map((unit) => { return {
title: unit.title || unit.product.title,
category: unit.product.category,
description: unit.description || unit.product.description,
slug: unit.slug,
content: unit.content || unit.product.content,
sid: unit._id,
pid: unit.product._id,
imgs: [...unit.imgs, ...unit.product.imgs],
price: unit.price || unit.product.price,
specs: unit.specs,
emoji: unit.product.emoji,
dims: unit.product.dims,
weight: unit.product.weight,
leadTime: unit.leadTime || unit.product.leadTime,
refundPolicy: unit.refundPolicy || unit.product.refundPolicy,
} }))
}),
new Promise(async (resolve, reject) => {
let products; try {
products = await models.Product.find({
listed: true,
})
} catch (err) { reject(err) }
resolve(products.map((product) => { return {
title: product.title,
category: product.category,
description: product.description,
slug: product.slug,
content: product.content,
sid: '',
pid: product._id,
imgs: product.imgs,
price: product.price,
emoji: product.emoji,
dims: product.dims,
weight: product.weight,
leadTime: product.leadTime,
refundPolicy: product.refundPolicy,
} }))
}),
])
} catch (err) { return console.error(err) }
return [...listings[0], ...listings[1]]
}

View File

@ -0,0 +1,38 @@
---
layout: layouts/base.njk
---
<h1>{% if listing.emoji %}{{listing.emoji}} {% endif %}{{listing.title}}</h1>
<p>{{listing.description}}</p>
<link rel="stylesheet" href="/assets/shop.css">
<div id="product_container">
<script src="/assets/scripts/cart-add.js" integrity="{{'/assets/scripts/cart-add.js'|srintegrity}}"></script>
<aside id="product_sidebar">
{% if listing.imgs.length -%}
<a href="/shop/{{listing.imgs[0]}}"><img src="/shop/{{listing.imgs[0]}}" alt="{{listing.title}}"/></a>
{%- else -%}
<p>No image available.</p>
{%- endif %}
<p><b>$<span data-bind>{{listing.price.usd}}</b></p>
<button class="add-to-cart" id="add-to-cart_{{listing.pid}}_{{listing.sid}}" onclick="addToCart('{{listing.pid}}','{{listing.sid}}','{{listing.price.usd}}','{% if listing.emoji %}{{listing.emoji}} {% endif %}{{listing.title|noApostrophes}}','{{listing.weight.lbs}}', '{{listing.dims.in.height*listing.dims.in.depth*listing.dims.in.width}}','/shop/{% if listing.slug %}{{listing.slug}}{% else %}{{listing.category}}/{% if listing.sid %}{{listing.sid}}/{% endif %}{{listing.title|slugify}}{% endif %}')"><b>🛒 Add to cart</b></button>
<div class="qty-wrap" id="qty-select-wrap_{{listing.pid}}_{{listing.sid}}">
<div style="display:flex">
<button id="qty-minus_{{listing.pid}}_{{listing.sid}}" class="qty-adjust" onclick="qtyMinus('{{listing.pid}}','{{listing.sid}}')">-</button>
<input class="qty" id="qty-select_{{listing.pid}}_{{listing.sid}}" type="number" name="qty-select_{{listing.pid}}/{{listing.sid}}" onchange="qtyChanged('{{listing.pid}}','{{listing.sid}}',{{listing.price.usd}},'{% if listing.emoji %}{{listing.emoji}} {% endif %}{{listing.title|noApostrophes}}','{{listing.weight.lbs}}', '{{listing.dims.in.height*listing.dims.in.depth*listing.dims.in.width}}','/shop/{% if listing.slug %}{{listing.slug}}{% else %}{{listing.category}}/{% if listing.sid %}{{listing.sid}}/{% endif %}{{listing.title|slugify}}{% endif %}')" min="0" max="{% if listing.sid %}1{% elif listing.inStock %}{{listing.inStock}}{% else %}99{% endif %}"/>
<button id="qty-plus_{{listing.pid}}_{{listing.sid}}" class="qty-adjust" onclick="qtyPlus('{{listing.pid}}','{{listing.sid}}')">+</button>
</div><span style="margin-left:1rem">in cart</span>
</div>
<script>productLoaded('{{listing.pid}}','{{listing.sid}}')</script>
<hr>
<p><b>Shipping:</b> Get it in {% if listing.leadTime %}{{listing.leadTime}}{% else %}10-30 days{% endif %}.
<p><b>Refund policy:</b> {% if listing.refundPolicy %}{{listing.refundPolicy}}{% else %}Return within 60 days for a refund (shipping not refunded).{% endif %}</p>
<hr>
{% markdown %}{% include 'product-specs.md' %}{% endmarkdown %}
</aside>
<section id="product_content">
{{listing.content|safe}}
</section>
</div>

View File

@ -1,44 +0,0 @@
---
layout: layouts/base.njk
templateClass: tmpl-product
---
{% set stripe_backend = 'https://stripe.blue.slvit.us/' %}
<p><a href="/shop/"><b>&lt; Back to all products</b></a><p>
<h1>{% if emoji %}{{emoji}} {% endif %}{{title}}</h1>
<p>{{description}}</p>
<link rel="stylesheet" href="/assets/shop.css">
<div id="product_container">
<script src="/assets/scripts/cart-add.js" integrity="{{'/assets/scripts/cart-add.js'|srintegrity}}"></script>
<aside id="product_sidebar">
{% if img -%}
<a href="/shop/{{img}}"><img src="/shop/{{img}}" alt="{{title}}"/></a>
{%- else -%}
<p>No image available.</p>
{%- endif %}
<p><b>$<span data-bind>{{price.usd}}</b></p>
<button class="add-to-cart" id="add-to-cart_{{pid}}_{{sid}}" onclick="addToCart('{{pid}}','{{sid}}','{{price.usd}}','{% if emoji %}{{emoji}} {% endif %}{{title|noApostrophes}}','{{specs.weight.lbs}}', '{{specs.dimensions.in.height*specs.dimensions.in.depth*specs.dimensions.in.width}}','{{page.url}}')"><b>🛒 Add to cart</b></button>
<div class="qty-wrap" id="qty-select-wrap_{{pid}}_{{sid}}">
<div style="display:flex">
<button id="qty-minus_{{pid}}_{{sid}}" class="qty-adjust" onclick="qtyMinus('{{pid}}','{{sid}}')">-</button>
<input class="qty" id="qty-select_{{pid}}_{{sid}}" type="number" name="qty-select_{{pid}}/{{sid}}" onchange="qtyChanged('{{pid}}','{{sid}}',{{price.usd}},'{% if emoji %}{{emoji}} {% endif %}{{title|noApostrophes}}','{{specs.weight.lbs}}', '{{specs.dimensions.in.height*specs.dimensions.in.depth*specs.dimensions.in.width}}','{{page.url}}')" min="0" max="99"/>
<button id="qty-plus_{{pid}}_{{sid}}" class="qty-adjust" onclick="qtyPlus('{{pid}}','{{sid}}')">+</button>
</div><span style="margin-left:1rem">in cart</span>
</div>
<script>productLoaded('{{pid}}','{{sid}}')</script>
<hr>
<p><b>Shipping:</b> Get it in {% if leadTime %}{{leadTime}}{% else %}10-30 days{% endif %}.
<p><b>Refund policy:</b> {% if refundPolicy %}{{refundPolicy}}{% else %}Return within 60 days for a refund (shipping not refunded).{% endif %}</p>
<hr>
{% md %}{% include 'product-specs.md' %}{% endmd %}
</aside>
<section id="product_content">
{{content|safe}}
</section>
</div>
<p><a href="/shop/"><b>&lt; Back to all products</b></a><p>

View File

@ -0,0 +1,34 @@
<link rel="stylesheet" href="/assets/shop.css">
{% if listings.length === 0 %}
<p>Nothing in stock.</p>
{% else %}<section class="productlist">
<script src="/assets/scripts/cart-add.js" integrity="{{'/assets/scripts/cart-add.js'|srintegrity}}"></script>
{% for listing in listings -%}
{% if listing.slug %}
{% set url = '/shop/' + listing.slug %}
{% else %}
{% if listing.sid %}
{% set url = '/shop/' + listing.category + '/' + listing.title|slugify + '/' + listing.sid + '/' %}
{% else %}
{% set url = '/shop/' + listing.category + '/' + listing.title|slugify + '/' %}
{% endif %}
{% endif %}
<div class="productlist-item">
<a href="{{url}}" style="text-decoration:none">
<h4>{{listing.title|noApostrophes}}</h4>
{% if listing.imgs.length -%}
<img src="/shop/{{listing.imgs[0]}}" width="100%"/>
{%- else %}<p>No image available.</p>{% endif %}
</a>
<p><b>${{listing.price.usd}}</b></p>
<button class="add-to-cart" id="add-to-cart_{{listing.pid}}_{{listing.sid}}" onclick="addToCart('{{listing.pid}}','{{listing.sid}}','{{listing.price.usd}}','{% if listing.emoji %}{{listing.emoji}} {% endif %}{{listing.title|noApostrophes}}','{{listing.weight.lbs}}', '{{listing.dims.in.height*listing.dims.in.depth*listing.dims.in.width}}','/shop/{% if listing.slug %}{{listing.slug}}{% else %}{{listing.category}}/{% if listing.sid %}{{listing.sid}}/{% endif %}{{listing.title|slugify}}{% endif %}')"><b>🛒 Add to cart</b></button>
<div class="qty-wrap" id="qty-select-wrap_{{listing.pid}}_{{listing.sid}}">
<div style="display:flex">
<button id="qty-minus_{{listing.pid}}_{{listing.sid}}" class="qty-adjust" onclick="qtyMinus('{{listing.pid}}','{{listing.sid}}')">-</button>
<input class="qty" id="qty-select_{{listing.pid}}_{{listing.sid}}" type="number" name="qty_{{listing.pid}}/{{listing.sid}}" onchange="qtyChanged('{{listing.pid}}','{{listing.sid}}',{{listing.price.usd}},'{% if listing.emoji %}{{listing.emoji}} {% endif %}{{listing.title|noApostrophes}}','{{listing.weight.lbs}}', '{{listing.dims.in.height*listing.dims.in.depth*listing.dims.in.width}}','/shop/{% if listing.slug %}{{listing.slug}}{% else %}{{listing.category}}/{% if listing.sid %}{{listing.sid}}/{% endif %}{{listing.title|slugify}}{% endif %}')" min="0" max="{% if listing.sid %}1{% elif listing.inStock %}{{listing.inStock}}{% else %}99{% endif %}"/>
<button id="qty-plus_{{listing.pid}}_{{listing.sid}}" class="qty-adjust" onclick="qtyPlus('{{listing.pid}}','{{listing.sid}}')">+</button>
</div><span style="margin-left:1rem">in cart</span>
</div>
<script>productLoaded('{{listing.pid}}','{{listing.sid}}')</script>
</div>{% endfor %}
</section>{% endif %}

View File

@ -1,27 +0,0 @@
<link rel="stylesheet" href="/assets/shop.css">
{% if productslist.length === 0 %}
<p>Nothing in stock.</p>
{% else %}<section class="productlist">
<script src="/assets/scripts/cart-add.js" integrity="{{'/assets/scripts/cart-add.js'|srintegrity}}"></script>
{% for product in productslist -%}
<div class="productlist-item">
<a href="{{product.url}}" style="text-decoration:none">
<h4>{{product.data.title|noApostrophes}}</h4>
{% if product.data.img -%}
<img src="/shop/{{product.data.img}}" width="100%"/>
{%- else -%}
<p>No image available.</p>
{%- endif %}
</a>
<p><b>${{product.data.price.usd}}</b></p>
<button class="add-to-cart" id="add-to-cart_{{product.data.pid}}_{{product.data.sid}}" onclick="addToCart('{{product.data.pid}}','{{product.data.sid}}','{{product.data.price.usd}}','{% if product.data.emoji %}{{product.data.emoji}} {% endif %}{{product.data.title|noApostrophes}}','{{product.data.specs.weight.lbs}}', '{{product.data.specs.dimensions.in.height*product.data.specs.dimensions.in.depth*product.data.specs.dimensions.in.width}}','{{product.url}}')"><b>🛒 Add to cart</b></button>
<div class="qty-wrap" id="qty-select-wrap_{{product.data.pid}}_{{product.data.sid}}">
<div style="display:flex">
<button id="qty-minus_{{product.data.pid}}_{{product.data.sid}}" class="qty-adjust" onclick="qtyMinus('{{product.data.pid}}','{{product.data.sid}}')">-</button>
<input class="qty" id="qty-select_{{product.data.pid}}_{{product.data.sid}}" type="number" name="qty_{{product.data.pid}}/{{product.data.sid}}" onchange="qtyChanged('{{product.data.pid}}','{{product.data.sid}}',{{product.data.price.usd}},'{% if product.data.emoji %}{{product.data.emoji}} {% endif %}{{product.data.title|noApostrophes}}','{{product.data.specs.weight.lbs}}', '{{product.data.specs.dimensions.in.height*product.data.specs.dimensions.in.depth*product.data.specs.dimensions.in.width}}','{{product.url}}')" min="0" max="99"/>
<button id="qty-plus_{{product.data.pid}}_{{product.data.sid}}" class="qty-adjust" onclick="qtyPlus('{{product.data.pid}}','{{product.data.sid}}')">+</button>
</div><span style="margin-left:1rem">in cart</span>
</div>
<script>productLoaded('{{product.data.pid}}','{{product.data.sid}}')</script>
</div>{% endfor %}
</section>{% endif %}

View File

@ -21,9 +21,12 @@ class CartItem { constructor(data) {
self.qty = ko.observable(data.qty)
self.maxQty = (self.sid)? 1:99
self.subtotal = ko.pureComputed(() => {
return formatUSD(self.price * self.qty())
})
self.subtotal = ko.pureComputed(() =>
self.price * self.qty()
)
self.subtotal_pretty = ko.pureComputed(() =>
formatUSD(self.subtotal())
)
// Subscribe to changes in qty
self.qty.subscribe((qty) => {
@ -287,7 +290,7 @@ class Cart { constructor() {
console.error(err)
} finally { self.isLoading(false) }
localStorage.setItem('cartOrder', parsedRes.id)
window.location = `/shop/order/?id=${parsedRes.id}&key=${parsedRes.key}`
window.location = `/shop/order/?id=${parsedRes.id}`
}

View File

@ -59,7 +59,7 @@ const emptyCart = () => {
const cancel = async () => {
try { await fetch(
`${API_DOMAIN}/order/${qstr.id}?key=${qstr.key}`,
`${API_DOMAIN}/order/${qstr.id}`,
{ method:'DELETE' }
) } catch (err) { console.error(err) }
finally { window.location = '/shop/cart/' }
@ -84,12 +84,14 @@ class MoneroTransaction { constructor(data) {
self.locked = ko.observable(true)
} }
class Checkout { constructor(data) {
class Order { constructor(data) {
let self = this
self.orderId = ko.observable(qstr.id||qstr.order)
self.orderKey = ko.observable(qstr.key)
self.orderId = ko.observable(qstr.id)
self.orderId_short = ko.pureComputed(() =>
'ORD-' + self.orderId().slice(-8)
)
self.orderUrl = ko.pureComputed(() =>
`${SITE_DOMAIN}/order/?id=${self.orderId()}&key=${self.orderKey().replace(/ /g,'+')}`
`${SITE_DOMAIN}/shop/order/?id=${self.orderId()}`
)
self.items = ko.observableArray([])
self.shipname = ko.observable('')
@ -133,7 +135,7 @@ class Checkout { constructor(data) {
( self.submitted() >= self.totalxmr() )
)
self.paidDate = ko.observable(new Date())
self.paidDate_pretty = ko.pureComputed(() =>
self.paidDate_pretty = ko.pureComputed( () =>
self.paidDate().toDateString()
)
self.isPaidUSD = ko.observable(false)
@ -144,6 +146,9 @@ class Checkout { constructor(data) {
self.isPaid = ko.pureComputed(() =>
(self.isPaidUSD() || self.isPaidXMR())
)
self.titleStatus = ko.pureComputed(() =>
(self.isPaid())?'(paid)':'(unpaid)'
)
self.isOverpaid = ko.pureComputed( () =>
( self.submitted() > self.totalxmr() )
)
@ -155,7 +160,7 @@ class Checkout { constructor(data) {
)
self.transactions = ko.observableArray([])
self.xmr_uri = ko.pureComputed(() =>
`monero:${self.xmr_address()}?tx_amount=${self.unsubmitted()}&tx_description=sales@slvit.us%20order%20${self.orderId()}`
`monero:${self.xmr_address()}?tx_amount=${self.unsubmitted()}&tx_description=sales@slvit.us%20${self.orderId_short()}`
)
let xmr_qr = new QRCode(
document.getElementById('xmr_qr'),
@ -171,7 +176,6 @@ class Checkout { constructor(data) {
`Place order for ${self.total_pretty()}`
)
self.checkingStatus = ko.observable('')
self.titleStatus = ko.observable('')
self.stripeText = ko.observable('')
self.submitStripePayment= ko.observable(()=>{})
self.shipDate = ko.observable(null)
@ -201,36 +205,31 @@ class Checkout { constructor(data) {
if (paymentIntent.last_payment_error !== null) {
if (paymentIntent.last_payment_error.type==='StripeCardError') {
self.titleStatus(' (declined)')
self.stripeText(`Your payment was declined. Our payment processor, Stripe, sent back this error: <code>${paymentIntent.last_payment_error.code||'generic_decline'}</code>. Here's what they think that means: </p>
<blockquote>${DECLINE_CODES[paymentIntent.last_payment_error.code||'generic_decline']||DECLINE_CODES['generic_decline']}</blockquote>
<p>What you do now is up to you. Maybe you want to <a href="/shop/checkout/stripe/">go back to the checkout page</a> and try again.`)
}
else {
self.titleStatus(' (failed)')
self.stripeText(`The payment failed due to an ${paymentIntent.last_payment_error.type} error.`)
}
self.isPaidUSD(false)
} else {
if (paymentIntent.status==='succeeded') {
self.titleStatus(' (paid)')
self.stripeText(`A ${self.total_pretty()} credit card payment for this order was processed ${(self.paidDate())?'on '+self.paidDate_pretty():''}. It will appear on your statement as "slvit.us ${self.orderId()}".`)
self.stripeText(`A ${self.total_pretty()} credit card payment for this order was processed ${(self.paidDate()!=null)?'on '+self.paidDate_pretty():''}. It will appear on your statement as "slvit.us ${self.orderId_short()}".`)
self.isPaidUSD(true)
if (!self.paidDate()) self.paidDate(new Date())
if (self.paidDate()==null) self.paidDate(new Date())
if (localStorage.getItem('cartOrder')==self.orderId())
emptyCart()
}
else if (paymentIntent.status==='processing') {
self.titleStatus(' (processing)')
self.stripeText(`Your payment wasn't processed <i>yet</i>. Wait a few minutes and then try <a href="javascript:window.location.reload(true)">refreshing this page</a>. `)
self.isPaidUSD(false)
}
else {
self.titleStatus(' (failed)')
self.stripeText('The payment failed due to an unknown error.')
self.isPaidUSD(false)
}
@ -244,7 +243,7 @@ class Checkout { constructor(data) {
// Fetch order from API
let res; try {
res = await fetch(`${API_DOMAIN}/order/${qstr.id}?key=${qstr.key}`, {
res = await fetch(`${API_DOMAIN}/order/${qstr.id}`, {
headers: {'Accept':'application/json'},
})
if (!res.ok) throw res.statusText
@ -257,16 +256,17 @@ class Checkout { constructor(data) {
}
// Populate view model with order data
self.paidDate(new Date(order.paidDate))
self.subtotal(formatUSD(order.subtotal))
self.shipping(formatUSD(order.shipping.amount))
self.processing(order.processing)
if (order.payment.date!=null)
self.paidDate(new Date(order.payment.date))
self.subtotal(formatUSD(order.amount.subtotal))
self.shipping(formatUSD(order.amount.shipping))
self.processing(order.amount.processing)
self.processing_pretty = ko.pureComputed(() =>
self.processing()
)
self.tax(formatUSD(order.taxAmount))
self.total(formatUSD(order.total))
self.paymentMethod(order.paymentMethod)
self.tax(formatUSD(order.amount.tax))
self.total(formatUSD(order.amount.total))
self.paymentMethod(order.payment.method)
order.items.forEach((item) =>
self.items.push({
name: item.name,
@ -276,8 +276,8 @@ class Checkout { constructor(data) {
})
)
self.shipname(order.shipping.address.name)
self.addr1(order.shipping.address.addr1)
self.addr2(order.shipping.address.addr2)
self.addr1(order.shipping.address.line1)
self.addr2(order.shipping.address.line2)
self.city(order.shipping.address.city)
self.zip(order.shipping.address.zip)
self.state(order.shipping.address.state)
@ -290,19 +290,19 @@ class Checkout { constructor(data) {
self.trackingNumbers(order.shipping.tracking)
// USD payment
if (order.paymentMethod==='USD') {
self.isPaidUSD((qstr.redirect_status=='succeeded'||order.paidDate!=null))
if (order.payment.method==='USD') {
self.isPaidUSD((qstr.redirect_status=='succeeded'||order.payment.date!=null))
// Try to get payment status if we didn't already
if (self.isPaidUSD()) {
if (qstr.payment_intent_client_secret==null)
getStripeStatus(order.stripe_secret)
getStripeStatus(order.payment.stripeSecret)
}
// Create stripe payment form
else {
const elements = stripe.elements({
clientSecret: order.stripe_secret,
clientSecret: order.payment.stripeSecret,
appearance: {
theme: 'night',
variables: {
@ -323,7 +323,7 @@ class Checkout { constructor(data) {
let payment_intent; try {
payment_intent = await stripe.confirmPayment({
elements,
confirmParams: { return_url: `${SUCCESS_URL}?id=${order.id}&key=${order.key}` },
confirmParams: { return_url: `${SUCCESS_URL}?id=${order._id}` },
})
if (payment_intent.error) throw payment_intent.error
} catch (err) { alert(err.message) }
@ -334,9 +334,9 @@ class Checkout { constructor(data) {
}
// Monero payment
else if (order.paymentMethod==='XMR') {
self.xmr_address(order.xmr_address)
self.totalxmr(order.totalxmr)
else if (order.payment.method==='XMR') {
self.xmr_address(order.payment.xmrAddress)
self.totalxmr(order.amount.totalxmr)
// Price checking
const getXmrPrice = async () => {
@ -360,9 +360,9 @@ class Checkout { constructor(data) {
const getTransactions = async () => {
isChecking = true
let res; try {
res = await fetch(`${API_DOMAIN}/xmr-receive/${order.xmr_address}`)
res = await fetch(`${API_DOMAIN}/xmr-receive/${order.payment.xmrAddress}`)
} catch (err) {
return console.error(`Failed to get update about xmr address ${order.xmr_address}\n${err}`)
return console.error(`Failed to get update about xmr address ${order.payment.xmrAddress}\n${err}`)
} if (!res.ok)
return console.error(`Got a bad response when requesting update on this xmr address: ${res.status}\n${res}`)
let resData; try {
@ -430,9 +430,9 @@ class Checkout { constructor(data) {
}
else alert(
`Invalid payment method in order: ${order.paymentMethod}`
`Invalid payment method in order: ${order.payment.method}`
)
} )()
} }; ko.applyBindings(new Checkout())
} }; ko.applyBindings(new Order())

View File

@ -3,8 +3,10 @@ layout: layouts/base.njk
eleventyNavigation:
key: About
order: 1
eleventyImport:
collections: ['featured']
pagination:
data: listings
size: 3
alias: listings
---
**{{metadata.title}}** is a web development, digital marketing, and information technologies company. As a managed service provider (MSP), SLVIT offers [websites](/services/websites/), [hosting](/services/hosting), [email](/services/hosting/email/), and [tech support](/services/support/). We also sell new and used [computers](/shop/computers/), [phones](/shop/phones/), [routers](/shop/routers/), and [accessories](/shop/accessories). We're based in Antonito, CO and operate all over southern Colorado and northern New Mexico.
@ -15,8 +17,7 @@ eleventyImport:
## 💻 Featured products
{% set productslist = collections.featured | head(-3) -%}
{% include 'productslist.njk' %}
{% include 'listings.njk' %}
> [**See all products >**](/shop/)

View File

@ -1,6 +1,6 @@
---
tags: ['product', 'computer', 'all-in-one']
layout: layouts/product.njk
layout: layouts/listing.njk
year: 2013
make: Dell
model: Inspiron One 2330

View File

@ -1,7 +1,7 @@
---
sid: d8578682
tags: ['product', 'computer', 'desktop']
layout: layouts/product.njk
layout: layouts/listing.njk
specs: {
year: 2016,
make: Dell,

View File

Before

Width:  |  Height:  |  Size: 2.9 MiB

After

Width:  |  Height:  |  Size: 2.9 MiB

View File

Before

Width:  |  Height:  |  Size: 3.5 MiB

After

Width:  |  Height:  |  Size: 3.5 MiB

View File

Before

Width:  |  Height:  |  Size: 805 KiB

After

Width:  |  Height:  |  Size: 805 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 MiB

After

Width:  |  Height:  |  Size: 6.6 MiB

View File

@ -1,7 +1,7 @@
---
sid: d9079897
tags: ['product', 'computer', 'desktop']
layout: layouts/product.njk
layout: layouts/listing.njk
specs: {
year: 2016,
make: Dell,

View File

Before

Width:  |  Height:  |  Size: 3.0 MiB

After

Width:  |  Height:  |  Size: 3.0 MiB

View File

Before

Width:  |  Height:  |  Size: 3.3 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

View File

Before

Width:  |  Height:  |  Size: 476 KiB

After

Width:  |  Height:  |  Size: 476 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 MiB

After

Width:  |  Height:  |  Size: 4.3 MiB

View File

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -1,7 +1,7 @@
---
sid: d2646191
tags: ['product', 'computer', 'desktop']
layout: layouts/product.njk
layout: layouts/listing.njk
specs: {
year: 2010,
make: HP,

View File

Before

Width:  |  Height:  |  Size: 3.8 MiB

After

Width:  |  Height:  |  Size: 3.8 MiB

View File

Before

Width:  |  Height:  |  Size: 2.8 MiB

After

Width:  |  Height:  |  Size: 2.8 MiB

View File

Before

Width:  |  Height:  |  Size: 3.7 MiB

After

Width:  |  Height:  |  Size: 3.7 MiB

View File

Before

Width:  |  Height:  |  Size: 5.0 MiB

After

Width:  |  Height:  |  Size: 5.0 MiB

View File

Before

Width:  |  Height:  |  Size: 735 KiB

After

Width:  |  Height:  |  Size: 735 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

View File

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -1,7 +1,7 @@
---
sid: d0664022
tags: ['product', 'computer', 'desktop']
layout: layouts/product.njk
layout: layouts/listing.njk
specs: {
year: 2016,
make: HP,

View File

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

View File

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

Before

Width:  |  Height:  |  Size: 2.9 MiB

After

Width:  |  Height:  |  Size: 2.9 MiB

View File

Before

Width:  |  Height:  |  Size: 5.0 MiB

After

Width:  |  Height:  |  Size: 5.0 MiB

View File

Before

Width:  |  Height:  |  Size: 614 KiB

After

Width:  |  Height:  |  Size: 614 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@ -1,7 +1,7 @@
---
sid: d1674407
tags: ['product', 'computer', 'desktop']
layout: layouts/product.njk
layout: layouts/listing.njk
specs: {
year: 2016,
make: HP,

View File

Before

Width:  |  Height:  |  Size: 4.7 MiB

After

Width:  |  Height:  |  Size: 4.7 MiB

View File

Before

Width:  |  Height:  |  Size: 4.6 MiB

After

Width:  |  Height:  |  Size: 4.6 MiB

View File

Before

Width:  |  Height:  |  Size: 2.8 MiB

After

Width:  |  Height:  |  Size: 2.8 MiB

View File

Before

Width:  |  Height:  |  Size: 5.6 MiB

After

Width:  |  Height:  |  Size: 5.6 MiB

View File

Before

Width:  |  Height:  |  Size: 658 KiB

After

Width:  |  Height:  |  Size: 658 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

@ -1,7 +1,7 @@
---
sid: d4531351
tags: ['product', 'computer', 'desktop', 'featured']
layout: layouts/product.njk
layout: layouts/listing.njk
specs: {
year: 2016,
make: HP,

View File

Before

Width:  |  Height:  |  Size: 5.8 MiB

After

Width:  |  Height:  |  Size: 5.8 MiB

View File

Before

Width:  |  Height:  |  Size: 6.7 MiB

After

Width:  |  Height:  |  Size: 6.7 MiB

View File

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

Before

Width:  |  Height:  |  Size: 5.3 MiB

After

Width:  |  Height:  |  Size: 5.3 MiB

View File

Before

Width:  |  Height:  |  Size: 6.5 MiB

After

Width:  |  Height:  |  Size: 6.5 MiB

View File

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

Before

Width:  |  Height:  |  Size: 647 KiB

After

Width:  |  Height:  |  Size: 647 KiB

View File

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

@ -1,7 +1,7 @@
---
sid: l7278286
tags: ['product', 'computer', 'laptop', 'featured']
layout: layouts/product.njk
layout: layouts/listing.njk
title: 2015 Dell Latitude E7240 (Linux Fedora 37) Laptop
emoji: 💻
specs: {

View File

Before

Width:  |  Height:  |  Size: 835 KiB

After

Width:  |  Height:  |  Size: 835 KiB

View File

Before

Width:  |  Height:  |  Size: 737 KiB

After

Width:  |  Height:  |  Size: 737 KiB

View File

Before

Width:  |  Height:  |  Size: 632 KiB

After

Width:  |  Height:  |  Size: 632 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

Before

Width:  |  Height:  |  Size: 990 KiB

After

Width:  |  Height:  |  Size: 990 KiB

View File

@ -1,7 +1,7 @@
---
sid: l8575077
tags: ['product', 'computer', 'laptop']
layout: layouts/product.njk
layout: layouts/listing.njk
title: 2018 Lenovo IdeaPad 330-17 (Linux Mint Cinnamon) Laptop
emoji: 💻
specs: {

View File

Before

Width:  |  Height:  |  Size: 4.3 MiB

After

Width:  |  Height:  |  Size: 4.3 MiB

View File

Before

Width:  |  Height:  |  Size: 245 KiB

After

Width:  |  Height:  |  Size: 245 KiB

View File

@ -1,7 +1,7 @@
---
pid: fc7422
tags: ['product', 'phone', 'featured']
layout: layouts/product.njk
layout: layouts/listing.njk
emoji: 📱
title: Privacy Pixel 6a Phone with CalyxOS
specs: {

View File

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 173 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,7 +1,7 @@
---
pid: fc2144
tags: ['product', 'phone']
layout: layouts/product.njk
layout: layouts/listing.njk
title: Privacy Pixel 6a Phone with GrapheneOS
emoji: 📱
specs: {

View File

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 244 KiB

View File

@ -28,7 +28,7 @@ title: Cart
<input class="qty" type="number" data-bind="value:qty,attr:{max:maxQty}" min="0">
<button class="qty-adjust" data-bind="click:$parent.itemPlus,enable:(qty()<maxQty)">+</button>
</div>
<div class="tar" data-bind="text:subtotal()"></div>
<div class="tar" data-bind="text:subtotal_pretty()"></div>
</div></td>
</tr></tbody></table><hr>
@ -68,7 +68,7 @@ title: Cart
</tr><tr data-bind="visible:payment_method()==='XMR'">
<td colspan="2" style="text-align:right"><b data-bind="text:totalxmr_pretty"></b></td>
</tr><tr data-bind="visible:payment_method()==='XMR'"><td colspan="2">
<i>You will pay a ~$0.01 transaction fee in your wallet app.</i>
<i>(You will pay a ½¢ transaction fee in your wallet app)</i>
</td></tr>
</tbody></table>
</div><hr>

View File

@ -4,13 +4,14 @@ title: Shop
eleventyNavigation:
key: Shop
order: 2
eleventyImport:
collections: ['products']
pagination:
data: listings
size: 20
alias: listings
---
# 🛍️ {{title}}
These are the gadgets we have in stock right now. When you've picked out what you want, visit your [cart](/shop/cart/) to check out.
{% set productslist = collections.products -%}
{% include 'productslist.njk' %}
{% include 'listings.njk' %}

13
_src/shop/listing.md Normal file
View File

@ -0,0 +1,13 @@
---
layout: layouts/listing.njk
pagination:
data: listings
size: 1
alias: listing
permalink: "shop/{% if listing.slug %}{{listing.slug}}/{% else %}{{listing.category}}/{{listing.title|slugify}}/{% if listing.sid %}{{listing.sid}}/{% endif %}{% endif %}"
---
# {{listing.title}}
{% set content %}{{listing.content}}{% endset%}
{% md content %}

Some files were not shown because too many files have changed in this diff Show More