Skip to content

Commit

Permalink
Merge branch 'award-afrobitcoin-experiment' into main-to-awardABC
Browse files Browse the repository at this point in the history
  • Loading branch information
Tirodem authored Dec 6, 2024
2 parents 0f6968f + e8626ba commit 5adb716
Show file tree
Hide file tree
Showing 39 changed files with 1,029 additions and 37 deletions.
3 changes: 3 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ BITCOIN_RPC_PASSWORD=
# NOTE: Use it only if you have a bitcoin core node, for nodeless setups, you can set it directly in the UI
#Example: xpub...
BIP84_XPUB=
# The inbound liquidity threshold determines when the system considers a channel's liquidity to be low and triggers an
# email notification to the site administrator.
LIGHTNING_INBOUND_LIQUIDITY_WARNING_THRESHOLD=0.3
# Set to true in .env.local to enable link preload headers, see https://nitropack.io/blog/post/link-rel-preload-explained
# Note that it will send large link headers for all pages, which may break nginx on default settings
# Add this to your nginx config to fix it:
Expand Down
39 changes: 38 additions & 1 deletion src/lib/components/CmsDesign.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
CmsTokens,
CmsContactForm,
CmsCountdown,
CmsGallery
CmsGallery,
CmsLeaderboard
} from '$lib/server/cms';
import SpecificationWidget from './SpecificationWidget.svelte';
import ContactForm from './ContactForm.svelte';
Expand All @@ -28,6 +29,7 @@
import GalleryWidget from './GalleryWidget/GalleryWidget.svelte';
import { page } from '$app/stores';
import CurrencyCalculator from './CurrencyCalculator.svelte';
import LeaderBoardWidget from './LeaderBoardWidget.svelte';
export let products: CmsProduct[];
export let pictures: CmsPicture[];
Expand All @@ -45,6 +47,7 @@
export let websiteLink: string | undefined;
export let brandName: string | undefined;
export let galleries: CmsGallery[];
export let leaderboards: CmsLeaderboard[];
let classNames = '';
export { classNames as class };
Expand All @@ -64,6 +67,10 @@
digitalFiles.map((digitalFile) => [digitalFile.productId, digitalFile])
);
$: challengeById = Object.fromEntries(challenges.map((challenge) => [challenge._id, challenge]));
$: leaderboardById = Object.fromEntries(
leaderboards.map((leaderboard) => [leaderboard._id, leaderboard])
);
$: sliderById = Object.fromEntries(sliders.map((slider) => [slider._id, slider]));
$: tagById = Object.fromEntries(tags.map((tag) => [tag._id, tag]));
$: picturesByTag = groupBy(
Expand Down Expand Up @@ -184,6 +191,19 @@
<PictureComponent picture={pictureById[token.slug]} class="my-5 lg:hidden block" />
{:else if token.type === 'currencyCalculatorWidget'}
<CurrencyCalculator />
{:else if token.type === 'qrCode'}
{#if token.slug === 'Bolt12'}
<a href="lightning:{$page.data.bolt12Address}">
<img src="{$page.url.origin}/phoenixd/bolt12/qrcode" class="w-96 h-96" alt="QR code" />
</a>
{/if}
{:else if token.type === 'leaderboardWidget'}
<LeaderBoardWidget
leaderboard={leaderboardById[token.slug]}
{pictures}
{products}
class="not-prose"
/>
{:else if token.type === 'html'}
<div class="my-5">
<!-- eslint-disable svelte/no-at-html-tags -->
Expand Down Expand Up @@ -260,6 +280,23 @@
<PictureComponent picture={pictureById[token.slug]} class="my-5" />
{:else if token.type === 'currencyCalculatorWidget'}
<CurrencyCalculator />
{:else if token.type === 'qrCode'}
{#if token.slug === 'Bolt12'}
<a href="lightning:{$page.data.bolt12Address}">
<img
src="{$page.url.origin}/phoenixd/bolt12/qrcode"
class="w-96 h-96"
alt="QR code"
/>
</a>
{/if}
{:else if token.type === 'leaderboardWidget'}
<LeaderBoardWidget
leaderboard={leaderboardById[token.slug]}
{pictures}
{products}
class="not-prose"
/>
{:else if token.type === 'html'}
<div class="my-5">
<!-- eslint-disable svelte/no-at-html-tags -->
Expand Down
7 changes: 6 additions & 1 deletion src/lib/components/CmsPage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
CmsSpecification,
CmsContactForm,
CmsCountdown,
CmsGallery
CmsGallery,
CmsLeaderboard
} from '$lib/server/cms';
import CmsDesign from './CmsDesign.svelte';
Expand Down Expand Up @@ -43,6 +44,8 @@
export let contactForms: CmsContactForm[];
export let countdowns: CmsCountdown[];
export let galleries: CmsGallery[];
export let leaderboards: CmsLeaderboard[];
const { t } = useI18n();
</script>

Expand Down Expand Up @@ -76,6 +79,7 @@
{brandName}
{countdowns}
{galleries}
{leaderboards}
class="body body-mainPlan"
/>
{:else}
Expand All @@ -102,6 +106,7 @@
{brandName}
{countdowns}
{galleries}
{leaderboards}
class="body"
/>
</main>
Expand Down
9 changes: 6 additions & 3 deletions src/lib/components/GoalProgress.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
export let text: string;
export let goal: number;
export let progress: number;
export let leaderboard = false;
let className = '';
export { className as class };
$: percentage = (progress * 100) / goal;
$: percentage = goal !== 0 ? (progress * 100) / goal : 0;
$: newPercentage = (goal * 100) / progress;
</script>

Expand All @@ -15,7 +16,7 @@
? 'bg-green-500'
: 'bg-gradient-to-r from-red-500 to-green-500 via-yellow-500'}"
>
{#if percentage < 100}
{#if percentage <= 100}
<div
data-text={text}
style="background-position: {percentage}% 0%"
Expand All @@ -33,7 +34,9 @@
</div>
<div
class="absolute inset-0 rounded-[3px] flex justify-end {percentage >= 100
? 'bg-green-500'
? leaderboard
? 'bg-gradient-to-r from-red-500 to-green-500 via-yellow-500'
: 'bg-green-500'
: ''}"
style={percentage >= 100 ? `width: calc(${Math.round(newPercentage)}%);` : ''}
>
Expand Down
60 changes: 60 additions & 0 deletions src/lib/components/LeaderBoardWidget.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<script lang="ts">
import GoalProgress from './GoalProgress.svelte';
import { useI18n } from '$lib/i18n';
import type { Leaderboard } from '$lib/types/Leaderboard';
import type { Product } from '$lib/types/Product';
import type { Picture } from '$lib/types/Picture';
import { groupBy } from 'lodash-es';
import type { SetRequired } from 'type-fest';
import PictureComponent from './Picture.svelte';
let className = '';
export { className as class };
export let leaderboard: Pick<
Leaderboard,
'_id' | 'name' | 'progress' | 'endsAt' | 'mode' | 'beginsAt'
>;
export let products: Pick<Product, '_id' | 'name' | 'shortDescription'>[];
export let pictures: Picture[];
const { locale } = useI18n();
$: productById = Object.fromEntries(products.map((product) => [product._id, product]));
$: picturesByProduct = groupBy(
pictures.filter((picture): picture is SetRequired<Picture, 'productId'> => !!picture.productId),
'productId'
);
leaderboard.progress.sort((a, b) => b.amount - a.amount);
</script>

{#each leaderboard.progress as progress}
<div class="flex items-center {className}">
<div class="flex items-center flex-row gap-4">
<a href="/product/{progress.product}">
<PictureComponent
picture={picturesByProduct[productById[progress.product]._id][0]}
class="max-w-[68px] max-h-[68px]"
/>
</a>
<div class="flex flex-col">
<a href="/product/{progress.product}" class="font-medium text-[22px] body-title">
{productById[progress.product].name}
</a>
<p class="hidden lg:contents">{productById[progress.product].shortDescription}</p>
</div>
</div>
</div>
<GoalProgress
class="font-bold body-title"
text={leaderboard.mode === 'moneyAmount'
? Number(Math.max(0, progress.amount))
.toLocaleString($locale, {
style: 'currency',
currency: progress.currency,
minimumFractionDigits: 0
})
.toString()
: Math.max(progress.amount, 0).toString()}
goal={leaderboard.progress[0].amount}
progress={progress.amount}
leaderboard={true}
/>
{/each}
50 changes: 48 additions & 2 deletions src/lib/server/cms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { Tag } from '$lib/types/Tag';
import type { ContactForm } from '$lib/types/ContactForm';
import type { Countdown } from '$lib/types/Countdown';
import type { Gallery } from '$lib/types/Gallery';
import type { Leaderboard } from '$lib/types/Leaderboard';

const window = new JSDOM('').window;

Expand Down Expand Up @@ -79,13 +80,20 @@ type TokenObject =
}
| { type: 'currencyCalculatorWidget'; slug: string; raw: string };

| { type: 'qrCode'; slug: string; raw: string }
| {
type: 'leaderboardWidget';
slug: string;
raw: string;
};
export async function cmsFromContent(
{ content, mobileContent }: { content: string; mobileContent?: string },
locals: Partial<PickDeep<App.Locals, 'user.roleId' | 'language' | 'email' | 'sso'>>
) {
const PRODUCT_WIDGET_REGEX =
/\[Product=(?<slug>[\p{L}\d_-]+)(?:[?\s]display=(?<display>[a-z0-9-]+))?\]/giu;
const CHALLENGE_WIDGET_REGEX = /\[Challenge=(?<slug>[a-z0-9-]+)\]/giu;
const LEADERBOARD_WIDGET_REGEX = /\[Leaderboard=(?<slug>[a-z0-9-]+)\]/giu;
const SLIDER_WIDGET_REGEX =
/\[Slider=(?<slug>[\p{L}\d_-]+)(?:[?\s]autoplay=(?<autoplay>[\d]+))?\]/giu;
const TAG_WIDGET_REGEX =
Expand All @@ -101,6 +109,7 @@ export async function cmsFromContent(
const GALLERY_WIDGET_REGEX =
/\[Gallery=(?<slug>[\p{L}\d_-]+)(?:[?\s]display=(?<display>[a-z0-9-]+))?\]/giu;
const CURRENCY_CALCULATOR_WIDGET_REGEX = /\[CurrencyCalculator=(?<slug>[a-z0-9-]+)\]/giu;
const QRCODE_REGEX = /\[QRCode=(?<slug>[\p{L}\d_-]+)\]/giu;

const productSlugs = new Set<string>();
const challengeSlugs = new Set<string>();
Expand All @@ -113,6 +122,8 @@ export async function cmsFromContent(
const tagProductsSlugs = new Set<string>();
const gallerySlugs = new Set<string>();
const currencyCalculatorSlugs = new Set<string>();
const qrCodeSlugs = new Set<string>();
const leaderboardSlugs = new Set<string>();

const tokens: {
desktop: Array<TokenObject>;
Expand Down Expand Up @@ -144,6 +155,8 @@ export async function cmsFromContent(
...matchAndSort(content, TAG_PRODUCTS_REGEX, 'tagProducts'),
...matchAndSort(content, GALLERY_WIDGET_REGEX, 'galleryWidget'),
...matchAndSort(content, CURRENCY_CALCULATOR_WIDGET_REGEX, 'currencyCalculatorWidget')
...matchAndSort(content, QRCODE_REGEX, 'qrCode'),
...matchAndSort(content, LEADERBOARD_WIDGET_REGEX, 'leaderboardWidget')
].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
for (const match of matches) {
const html = trimPrefix(trimSuffix(content.slice(index, match.index), '<p>'), '</p>');
Expand Down Expand Up @@ -254,6 +267,18 @@ export async function cmsFromContent(
currencyCalculatorSlugs.add(match.groups.slug);
token.push({
type: 'currencyCalculatorWidget',
case 'qrCode':
qrCodeSlugs.add(match.groups.slug);
token.push({
type: 'qrCode',
slug: match.groups.slug,
raw: match[0]
});
break;
case 'leaderboardWidget':
leaderboardSlugs.add(match.groups.slug);
token.push({
type: 'leaderboardWidget',
slug: match.groups.slug,
raw: match[0]
});
Expand All @@ -277,10 +302,28 @@ export async function cmsFromContent(
locals.user?.roleId === POS_ROLE_ID
? { 'actionSettings.retail.visible': true }
: { 'actionSettings.eShop.visible': true };

const leaderboards = await collections.leaderboards
.find({
_id: { $in: [...leaderboardSlugs] }
})
.project<Pick<Leaderboard, '_id' | 'name' | 'progress' | 'endsAt' | 'mode' | 'beginsAt'>>({
name: 1,
goal: 1,
progress: 1,
endsAt: 1,
beginsAt: 1,
mode: 1
})
.toArray();
const allProductsLead = leaderboards
.flatMap((leaderboard) => leaderboard.progress || [])
.map((progressItem) => progressItem.product);
const products = await collections.products
.find({
$or: [{ tagIds: { $in: [...tagProductsSlugs] } }, { _id: { $in: [...productSlugs] } }],
$or: [
{ tagIds: { $in: [...tagProductsSlugs] } },
{ _id: { $in: [...productSlugs, ...allProductsLead] } }
],
...query
})
.project<
Expand Down Expand Up @@ -420,6 +463,7 @@ export async function cmsFromContent(
secondary: { $ifNull: [`$translations.${locals.language}.secondary`, '$secondary'] }
})
.toArray();

return {
tokens,
challenges,
Expand All @@ -430,6 +474,7 @@ export async function cmsFromContent(
contactForms,
countdowns,
galleries,
leaderboards,
pictures: await collections.pictures
.find({
$or: [
Expand Down Expand Up @@ -469,3 +514,4 @@ export type CmsContactForm = Awaited<ReturnType<typeof cmsFromContent>>['contact
export type CmsCountdown = Awaited<ReturnType<typeof cmsFromContent>>['countdowns'][number];
export type CmsGallery = Awaited<ReturnType<typeof cmsFromContent>>['galleries'][number];
export type CmsToken = Awaited<ReturnType<typeof cmsFromContent>>['tokens']['desktop'][number];
export type CmsLeaderboard = Awaited<ReturnType<typeof cmsFromContent>>['leaderboards'][number];
2 changes: 2 additions & 0 deletions src/lib/server/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import type { Gallery } from '$lib/types/Gallery';
import type { VatProfile } from '$lib/types/VatProfile';
import type { Ticket } from '$lib/types/Ticket';
import type { OrderLabel } from '$lib/types/OrderLabel';
import type { Leaderboard } from '$lib/types/Leaderboard';

// Bigger than the default 10, helpful with MongoDB errors
Error.stackTraceLimit = 100;
Expand Down Expand Up @@ -85,6 +86,7 @@ const genCollection = () => ({
nostrReceivedMessages: db.collection<NostRReceivedMessage>('nostr.receivedMessage'),
cmsPages: db.collection<CMSPage>('cmsPages'),
challenges: db.collection<Challenge>('challenges'),
leaderboards: db.collection<Leaderboard>('leaderboards'),
roles: db.collection<Role>('roles'),
users: db.collection<User>('users'),
discounts: db.collection<Discount>('discounts'),
Expand Down
Loading

0 comments on commit 5adb716

Please sign in to comment.