Compare commits
7
Commits
8729098f20
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08a0bdcdb8 | ||
|
|
93c132b0df | ||
|
|
771d95d1cd | ||
|
|
254736356b | ||
|
|
738e406725 | ||
|
|
de4630635d | ||
|
|
6b358011b0 |
@@ -100,7 +100,7 @@ export class StorefrontCartController {
|
|||||||
text: 'Added to cart.'
|
text: 'Added to cart.'
|
||||||
});
|
});
|
||||||
|
|
||||||
res.redirect(HttpStatus.FOUND, safeInternalShopRedirectPath(req));
|
res.redirect(HttpStatus.FOUND, this.buildAddToCartRedirectPath(req, variantId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('shop/cart/product/update')
|
@Post('shop/cart/product/update')
|
||||||
@@ -189,4 +189,18 @@ export class StorefrontCartController {
|
|||||||
this.cartCookieService.setCart(req, res, cartMutation);
|
this.cartCookieService.setCart(req, res, cartMutation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildAddToCartRedirectPath(req: Request, variantId: string): string {
|
||||||
|
const redirectPath = safeInternalShopRedirectPath(req);
|
||||||
|
const pathname = redirectPath.split('?')[0] ?? redirectPath;
|
||||||
|
const isProductsIndexPath = pathname === '/' || pathname.startsWith('/shop/categories/');
|
||||||
|
|
||||||
|
if (!isProductsIndexPath) {
|
||||||
|
return redirectPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const anchor = `product-card-${variantId}`;
|
||||||
|
|
||||||
|
return `${redirectPath}#${anchor}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
--sf-split-sidebar: minmax(220px, 320px);
|
--sf-split-sidebar: minmax(220px, 320px);
|
||||||
--sf-product-card-min: 11rem;
|
--sf-product-card-min: 11rem;
|
||||||
--sf-product-card-max: 22rem;
|
--sf-product-card-max: 22rem;
|
||||||
|
--sf-sticky-header-scroll-padding: 9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
@@ -158,6 +159,10 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-padding-top: var(--sf-sticky-header-scroll-padding);
|
||||||
|
}
|
||||||
|
|
||||||
body.sf-body {
|
body.sf-body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
@@ -288,9 +293,13 @@ a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sf-header {
|
.sf-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
margin-bottom: var(--sf-space-4);
|
margin-bottom: var(--sf-space-4);
|
||||||
padding: var(--sf-space-3) 0;
|
padding: var(--sf-space-3) 0;
|
||||||
border-bottom: 1px solid var(--sf-border);
|
border-bottom: 1px solid var(--sf-border);
|
||||||
|
background: var(--sf-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-header__inner {
|
.sf-header__inner {
|
||||||
@@ -371,6 +380,26 @@ a:hover {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sf-header__inner {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-header__brand-group {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-nav,
|
||||||
|
.sf-rates {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sf-crypto-icon {
|
.sf-crypto-icon {
|
||||||
width: 1.25rem;
|
width: 1.25rem;
|
||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
@@ -510,6 +539,76 @@ a:hover {
|
|||||||
border-color: var(--sf-warning-border);
|
border-color: var(--sf-warning-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sf-alert--toast {
|
||||||
|
position: fixed;
|
||||||
|
top: var(--sf-space-5);
|
||||||
|
left: 50%;
|
||||||
|
z-index: 20;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sf-space-2);
|
||||||
|
width: max-content;
|
||||||
|
max-width: min(24rem, calc(100% - 2 * var(--sf-space-4)));
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.6875rem 0.9375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-alert--toast.sf-alert--success {
|
||||||
|
animation: sf-toast-dismiss 3s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-alert--toast.sf-alert--error {
|
||||||
|
animation: sf-toast-dismiss 6s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-alert--toast::before {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border-radius: var(--sf-radius-full);
|
||||||
|
color: var(--sf-on-accent);
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-alert--toast.sf-alert--success::before {
|
||||||
|
content: '✓';
|
||||||
|
background: var(--sf-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-alert--toast.sf-alert--error::before {
|
||||||
|
content: '×';
|
||||||
|
background: var(--sf-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sf-toast-dismiss {
|
||||||
|
0%,
|
||||||
|
70% {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.sf-alert--toast {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sf-btn {
|
.sf-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -789,7 +888,7 @@ a:hover {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-line-item__body > .sf-discount-error {
|
.sf-line-item__body > .sf-cart-issue {
|
||||||
margin: 0.125rem 0 0;
|
margin: 0.125rem 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -866,8 +965,8 @@ a:hover {
|
|||||||
color: var(--sf-text-muted);
|
color: var(--sf-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-discount-error {
|
.sf-cart-issue {
|
||||||
margin: 0 0 var(--sf-space-2);
|
margin: var(--sf-space-2) 0;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--sf-error);
|
color: var(--sf-error);
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
|
|||||||
@@ -12,10 +12,10 @@
|
|||||||
{{> shop-nav}}
|
{{> shop-nav}}
|
||||||
{{> category-nav}}
|
{{> category-nav}}
|
||||||
<main class='sf-main'>
|
<main class='sf-main'>
|
||||||
{{> feedback}}
|
|
||||||
{{{body}}}
|
{{{body}}}
|
||||||
</main>
|
</main>
|
||||||
{{> shop-footer}}
|
{{> shop-footer}}
|
||||||
</div>
|
</div>
|
||||||
|
{{> feedback}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
{{@root.shopFiatCurrency}}</div>
|
{{@root.shopFiatCurrency}}</div>
|
||||||
<div class='sf-line-item__note'>{{> product-delivery-note deliveryMode=deliveryMode}}</div>
|
<div class='sf-line-item__note'>{{> product-delivery-note deliveryMode=deliveryMode}}</div>
|
||||||
{{#if stockIssueMessage}}
|
{{#if stockIssueMessage}}
|
||||||
<p class='sf-discount-error'>{{stockIssueMessage}}</p>
|
<p class='sf-cart-issue'>{{stockIssueMessage}}</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#unless stockForSession}}
|
{{#unless stockForSession}}
|
||||||
<p class='sf-text-subtle'>No more in stock beside your order</p>
|
<p class='sf-text-subtle'>No more in stock beside your order</p>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
{{#each @root.discounts}}
|
{{#each @root.discounts}}
|
||||||
{{#if (includes ineligibleVariantIds ../id)}}
|
{{#if (includes ineligibleVariantIds ../id)}}
|
||||||
<p class='sf-discount-error'>Not eligible for code {{code}}</p>
|
<p class='sf-cart-issue'>Not eligible for code {{code}}</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
<form class='sf-form-row' method='post' action='/shop/cart/product/update'>
|
<form class='sf-form-row' method='post' action='/shop/cart/product/update'>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
}}
|
}}
|
||||||
<span class='sf-discount-row__code sf-text-error'>{{code}}</span>
|
<span class='sf-discount-row__code sf-text-error'>{{code}}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class='sf-discount-error'>{{issueMessage}}</p>
|
<p class='sf-cart-issue'>{{issueMessage}}</p>
|
||||||
{{else if amount}}
|
{{else if amount}}
|
||||||
<div class='sf-discount-row sf-discount-item'>
|
<div class='sf-discount-row sf-discount-item'>
|
||||||
{{> dismiss-button
|
{{> dismiss-button
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
{{#if cartTotalIssueMessage}}
|
{{#if cartTotalIssueMessage}}
|
||||||
<p class='sf-discount-error'>{{cartTotalIssueMessage}}</p>
|
<p class='sf-cart-issue'>{{cartTotalIssueMessage}}</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<div class='sf-stack sf-mt-4'>
|
<div class='sf-stack sf-mt-4'>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{{#if feedback}}
|
{{#if feedback}}
|
||||||
<p class='sf-alert sf-alert--{{feedback.type}}' role='status'>
|
<p class='sf-alert sf-alert--{{feedback.type}} sf-alert--toast' role='status'>
|
||||||
{{feedback.text}}
|
{{feedback.text}}
|
||||||
</p>
|
</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<li class='sf-card sf-product-card'>
|
<li class='sf-card sf-product-card' id='product-card-{{selectedVariant.id}}'>
|
||||||
{{#with selectedVariant}}
|
{{#with selectedVariant}}
|
||||||
{{#if thumbnailUrl}}
|
{{#if thumbnailUrl}}
|
||||||
<a class='sf-product-card__media-link' href='{{detailHref}}'>
|
<a class='sf-product-card__media-link' href='{{detailHref}}'>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class='sf-footer__row'>
|
<div class='sf-footer__row'>
|
||||||
<div class='sf-footer__start'>
|
<div class='sf-footer__start'>
|
||||||
{{#if simplexLink}}
|
{{#if simplexLink}}
|
||||||
<a href='{{simplexLink}}' target='_blank' rel='noopener noreferrer'>Contact via Simplex</a>
|
<a href='{{simplexLink}}' target='_blank' rel='noopener noreferrer'>Contact via SimpleX</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<a href='https://nullcart.net/' target='_blank' rel='noopener noreferrer'>Powered by NullCart</a>
|
<a href='https://nullcart.net/' target='_blank' rel='noopener noreferrer'>Powered by NullCart</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
## 1. Requirements
|
## 1. Requirements
|
||||||
|
|
||||||
- Ubuntu 22.04+ or similar Linux with Docker Engine and the Compose plugin — follow [Install Docker Engine on Ubuntu](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository). Tested and recommended on Ubuntu 22.04 LTS.
|
- Ubuntu 22.04+ or similar Linux with Docker Engine and the Compose plugin — follow [Install Docker Engine on Ubuntu](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository). Tested and recommended on Ubuntu 22.04 LTS.
|
||||||
- A domain name pointing at your server (A record for clearnet HTTPS)
|
- A domain name pointing at your server (A records for apex and `www`)
|
||||||
|
|
||||||
## 2. Server setup
|
## 2. Server setup
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ Remove the temporary bootstrap certificates under `deploy/certs/live/` (Certbot
|
|||||||
rm -rf deploy/certs/live/*
|
rm -rf deploy/certs/live/*
|
||||||
```
|
```
|
||||||
|
|
||||||
Request the real certificate:
|
Request the real certificate (apex + www):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./deploy/scripts/issue-certs.sh --email you@example.com
|
./deploy/scripts/issue-certs.sh --email you@example.com
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ usage() {
|
|||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: $(basename "$0") --email you@example.com
|
Usage: $(basename "$0") --email you@example.com
|
||||||
|
|
||||||
Obtain or renew Let's Encrypt certificates for CLEARNET_DOMAIN using the webroot
|
Obtain Let's Encrypt certificates for CLEARNET_DOMAIN and www.CLEARNET_DOMAIN using
|
||||||
challenge. Nginx must be running and serving /.well-known/acme-challenge/ from
|
the webroot challenge. Nginx must be running and serving /.well-known/acme-challenge/
|
||||||
deploy/certbot/www.
|
from deploy/certbot/www.
|
||||||
|
|
||||||
Environment is read from .env.prod (CLEARNET_DOMAIN).
|
Environment is read from .env.prod (CLEARNET_DOMAIN).
|
||||||
EOF
|
EOF
|
||||||
@@ -65,6 +65,7 @@ docker run --rm \
|
|||||||
--webroot \
|
--webroot \
|
||||||
-w /var/www/certbot \
|
-w /var/www/certbot \
|
||||||
-d "$CLEARNET_DOMAIN" \
|
-d "$CLEARNET_DOMAIN" \
|
||||||
|
-d "www.${CLEARNET_DOMAIN}" \
|
||||||
--email "$CERTBOT_EMAIL" \
|
--email "$CERTBOT_EMAIL" \
|
||||||
--agree-tos \
|
--agree-tos \
|
||||||
--non-interactive
|
--non-interactive
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name ${CLEARNET_DOMAIN};
|
server_name ${CLEARNET_DOMAIN} www.${CLEARNET_DOMAIN};
|
||||||
|
|
||||||
location /.well-known/acme-challenge/ {
|
location /.well-known/acme-challenge/ {
|
||||||
root /var/www/certbot;
|
root /var/www/certbot;
|
||||||
@@ -13,7 +13,7 @@ server {
|
|||||||
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
server_name ${CLEARNET_DOMAIN};
|
server_name ${CLEARNET_DOMAIN} www.${CLEARNET_DOMAIN};
|
||||||
|
|
||||||
ssl_certificate /etc/nginx/certs/live/${CLEARNET_DOMAIN}/fullchain.pem;
|
ssl_certificate /etc/nginx/certs/live/${CLEARNET_DOMAIN}/fullchain.pem;
|
||||||
ssl_certificate_key /etc/nginx/certs/live/${CLEARNET_DOMAIN}/privkey.pem;
|
ssl_certificate_key /etc/nginx/certs/live/${CLEARNET_DOMAIN}/privkey.pem;
|
||||||
|
|||||||
Reference in New Issue
Block a user