Compare commits

...

3 Commits

Author SHA1 Message Date
Bojan Mojsilovic
40bc2ec8a7 fix 2024-06-07 17:52:59 +02:00
Bojan Mojsilovic
4524f7d4fc Update featured author layout 2024-06-07 16:33:06 +02:00
Bojan Mojsilovic
de05c5d0a1 Filter-out non-sats prices 2024-06-07 16:24:16 +02:00
11 changed files with 294 additions and 113 deletions

View File

@ -1,7 +1,7 @@
.authorSubscribeCard {
display: flex;
flex-direction: column;
gap: 8px;
gap: 16px;
border-radius: 8px;
background: var(--background-header-input);
width: 300px;
@ -14,6 +14,7 @@
.userData {
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
overflow: hidden;
@ -49,9 +50,9 @@
.actions {
display: flex;
justify-content: flex-end;
justify-content: space-between;
align-items: center;
gap: 6px;
gap: 8px;
>button {
color: var(--text-primary);
@ -60,6 +61,7 @@
font-weight: 600;
line-height: 16px;
padding-inline: 16px;
width: 100%;
}
}
}

View File

@ -19,13 +19,7 @@ import Avatar from '../Avatar/Avatar';
import ButtonPrimary from '../Buttons/ButtonPrimary';
import ButtonSecondary from '../Buttons/ButtonSecondary';
import Loader from '../Loader/Loader';
import { NoteReactionsState } from '../Note/Note';
import NoteContextTrigger from '../Note/NoteContextTrigger';
import ArticleFooter from '../Note/NoteFooter/ArticleFooter';
import NoteFooter from '../Note/NoteFooter/NoteFooter';
import NoteTopZaps from '../Note/NoteTopZaps';
import NoteTopZapsCompact from '../Note/NoteTopZapsCompact';
import { Tier } from '../SubscribeToAuthorModal/SubscribeToAuthorModal';
import { Tier, TierCost } from '../SubscribeToAuthorModal/SubscribeToAuthorModal';
import VerificationCheck from '../VerificationCheck/VerificationCheck';
import styles from './AuthorSubscribe.module.scss';
@ -59,10 +53,10 @@ const AuthoreSubscribe: Component<{
getAuthorData();
});
const doSubscription = async (tier: Tier) => {
const doSubscription = async (tier: Tier, cost: TierCost) => {
const a = author();
if (!a || !account) return;
if (!a || !account || !cost) return;
const subEvent = {
kind: Kind.Subscribe,
@ -71,7 +65,7 @@ const AuthoreSubscribe: Component<{
tags: [
['p', a.pubkey],
['e', tier.id],
['amount', tier.costs[0].amount, tier.costs[0].unit, tier.costs[0].duration],
['amount', cost.amount, cost.unit, cost.cadence],
['event', JSON.stringify(tier.event)],
// Copy any zap splits
...(tier.event.tags?.filter(t => t[0] === 'zap') || []),
@ -97,7 +91,7 @@ const AuthoreSubscribe: Component<{
>
<div class={styles.authorSubscribeCard}>
<div class={styles.userInfo}>
<Avatar user={author()} />
<Avatar user={author()} size="ml" />
<div class={styles.userData}>
<div class={styles.userName}>
{userName(author())}

View File

@ -164,6 +164,21 @@
}
}
.mlAvatar {
@include avatar;
width: 60px;
height: 60px;
.missingBack {
width: 60px;
height: 60px;
}
.iconBackground {
@include iconBackground;
}
}
.largeAvatar {
@include avatar;
width: 72px;
@ -290,6 +305,13 @@
font-size: 16px;
}
.mlMissing {
@include missing;
width: 60px;
height: 60px;
font-size: 16px;
}
.largeMissing {
@include missing;
width: 68px;

View File

@ -11,7 +11,7 @@ import styles from './Avatar.module.scss';
const Avatar: Component<{
src?: string | undefined,
size?: "micro" | "xxs" | "xss" | "xs" | "vvs" | "vs" | "sm" | "md" | "lg" | "xl" | "xxl",
size?: "micro" | "xxs" | "xss" | "xs" | "vvs" | "vs" | "sm" | "md" | "ml" | "lg" | "xl" | "xxl",
user?: PrimalUser,
highlightBorder?: boolean,
id?: string,
@ -34,6 +34,7 @@ const Avatar: Component<{
vs: styles.vsAvatar,
sm: styles.smallAvatar,
md: styles.midAvatar,
ml: styles.mlAvatar,
lg: styles.largeAvatar,
xl: styles.extraLargeAvatar,
xxl: styles.xxlAvatar,
@ -48,6 +49,7 @@ const Avatar: Component<{
vs: styles.vsMissing,
sm: styles.smallMissing,
md: styles.midMissing,
ml: styles.mlMissing,
lg: styles.largeMissing,
xl: styles.extraLargeMissing,
xxl: styles.xxlMissing,

View File

@ -116,6 +116,12 @@ const ReadsSidebar: Component< { id?: string } > = (props) => {
// const author = '88cc134b1a65f54ef48acc1df3665063d3ea45f04eab8af4646e561c5ae99079';
// setFeautredAuthor(() => author);
// const author = 'fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52';
// setFeautredAuthor(() => author);
// const author = '3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24';
// setFeautredAuthor(() => author);
setFeautredAuthor(() => authors[Math.floor(Math.random() * authors.length)]);
},
onEose: () => {

View File

@ -1,6 +1,7 @@
.subscribeToAuthor {
position: fixed;
min-width: 472px;
min-height: 344px;
color: var(--text-primary);
background-color: var(--background-input);
border-radius: 8px;
@ -79,6 +80,7 @@
.tiers {
display: flex;
gap: 12px;
min-height: 220px;
.tier {
display: flex;
@ -103,27 +105,6 @@
line-height: 20px;
}
.cost {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
.amount {
color: var(--text-primary);
font-size: 20px;
font-weight: 600;
line-height: 20px;
}
.duration {
color: var(--text-secondary);
font-size: 16px;
font-weight: 400;
line-height: 16px;
}
}
.content {
display: flex;
border-top: 1px solid var(--subtile-devider);
@ -219,3 +200,91 @@
-webkit-mask: url(../../assets/icons/explore/zaps_hollow.svg) no-repeat 2px 0 / 19px 22px;
mask: url(../../assets/icons/explore/zaps_hollow.svg) no-repeat 2px 0 / 19px 22px;
}
.selectCosts {
}
.selectTrigger {
background-color: var(--background-input);
width: 360px;
border: none;
outline: none;
margin: 0;
padding: 0;
.selectValue {
.cost {
.duration {
display: flex;
gap: 6px;
.chevIcon {
width: 6px;
height: 16px;
background-color: var(--text-tertiary);
-webkit-mask: url(../../assets/icons/chevron_right.svg) no-repeat 0 / 100%;
mask: url(../../assets/icons/chevron_right.svg) no-repeat 0 / 100%;
rotate: 90deg;
}
}
}
}
}
.selectContent {
background-color: var(--background-sheet);
z-index: 9999;
width: 390px;
.selectListbox {
border: 1px solid var(--subtile-devider);
border-radius: 8px;
background-color: var(--background-sheet);
padding: 0;
.cost {
border-radius: 8px;
padding-block: 12px;
padding-inline: 12px;
cursor: pointer;
&:hover {
background-color: var(--background-input);
}
}
}
}
.cost {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
.amount {
color: var(--text-primary);
font-size: 20px;
font-weight: 600;
line-height: 20px;
}
.duration {
color: var(--text-secondary);
font-size: 16px;
font-weight: 400;
line-height: 16px;
}
}
.noTiers {
display: flex;
justify-content: center;
width: 100%;
color: var(--text-secondary);
font-size: 16px;
font-weight: 600;
line-height: 16px;
text-transform: uppercase;
}

View File

@ -1,18 +1,12 @@
import { useIntl } from '@cookbook/solid-intl';
// @ts-ignore
import { decode } from 'light-bolt11-decoder';
import { Component, createEffect, For, Show } from 'solid-js';
import { createStore, reconcile } from 'solid-js/store';
import { emptyInvoice, Kind } from '../../constants';
import { date, dateFuture } from '../../lib/dates';
import { createStore } from 'solid-js/store';
import { Kind } from '../../constants';
import { hookForDev } from '../../lib/devTools';
import { humanizeNumber } from '../../lib/stats';
import { cashuInvoice } from '../../translations';
import { LnbcInvoice, NostrTier, PrimalUser } from '../../types/primal';
import { NostrTier, PrimalUser } from '../../types/primal';
import ButtonPrimary from '../Buttons/ButtonPrimary';
import Modal from '../Modal/Modal';
import QrCode from '../QrCode/QrCode';
import { getDecodedToken, Token } from "@cashu/cashu-ts";
import styles from './SubscribeToAuthorModal.module.scss';
import { userName } from '../../stores/profile';
@ -22,13 +16,23 @@ import { APP_ID } from '../../App';
import { subsTo } from '../../sockets';
import { getAuthorSubscriptionTiers } from '../../lib/feed';
import ButtonSecondary from '../Buttons/ButtonSecondary';
import { Select } from '@kobalte/core';
import Loader from '../Loader/Loader';
export type TierCost = {
amount: string,
unit: string,
cadence: string,
id: string,
}
export type Tier = {
title: string,
content: string,
id: string,
perks: string[],
costs: { amount: string, unit: string, duration: string}[],
costs: TierCost[],
activeCost: TierCost | undefined,
client: string,
event: NostrTier,
};
@ -36,20 +40,24 @@ export type Tier = {
export type TierStore = {
tiers: Tier[],
selectedTier: Tier | undefined,
selectedCost: TierCost | undefined,
isFetchingTiers: boolean,
}
export const payUnits = ['sats', 'msats', ''];
export const payUnits = ['sats', 'msat', ''];
const SubscribeToAuthorModal: Component<{
id?: string,
author: PrimalUser | undefined,
onClose: () => void,
onSubscribe: (tier: Tier) => void,
onSubscribe: (tier: Tier, cost: TierCost) => void,
}> = (props) => {
const [store, updateStore] = createStore<TierStore>({
tiers: [],
selectedTier: undefined,
selectedCost: undefined,
isFetchingTiers: false,
})
createEffect(() => {
@ -76,14 +84,23 @@ const SubscribeToAuthorModal: Component<{
if (content.kind === Kind.Tier) {
const t = content as NostrTier;
const costs = t.tags?.filter((t: string[]) => t[0] === 'amount').map((t: string[]) => (
{
amount: t[1],
unit: t[2],
cadence: t[3],
id: `${t[1]}_${t[2]}_${t[3]}`
})) || [];
const tier = {
title: (t.tags?.find((t: string[]) => t[0] === 'title') || [])[1] || t.content || '',
id: t.id || '',
content: t.content || '',
perks: t.tags?.filter((t: string[]) => t[0] === 'perk').map((t: string[]) => t[1]) || [],
costs: t.tags?.filter((t: string[]) => t[0] === 'amount').map((t: string[]) => ({ amount: t[1], unit: t[2], duration: t[3]})) || [],
costs,
client: (t.tags?.find((t: string[]) => t[0] === 'client') || [])[1] || t.content || '',
event: t,
activeCost: costs[0],
}
tiers.push(tier)
@ -93,28 +110,35 @@ const SubscribeToAuthorModal: Component<{
},
onEose: () => {
unsub();
updateStore('isFetchingTiers', () => false);
updateStore('tiers', () => [...tiers]);
updateStore('selectedTier', () => ( tiers.length > 0 ? { ...tiers[0]} : undefined))
const tier: Tier | undefined = tiers.length > 0 ? Object.assign(tiers[0]) : undefined;
updateStore('selectedTier', () => tier ? ({ ...tier }) : undefined);
updateStore('selectedCost', () => tier ? ({ ...tier?.costs[0] }) : undefined);
},
})
updateStore('isFetchingTiers', () => true);
getAuthorSubscriptionTiers(author.pubkey, subId)
}
const selectTier = (tier: Tier) => {
updateStore('selectedTier', () => ({ ...tier }));
if (tier.id !== store.selectedTier?.id) {
updateStore('selectedTier', () => ({ ...tier }));
updateStore('selectedCost', (sc) => ({ ...costOptions(tier)[0] }) );
}
}
const isSelectedTier = (tier: Tier) => tier.id === store.selectedTier?.id;
// const costForTier = (tier: Tier) => {
// const costs = tier.costs.filter(c => payUnits.includes(c.unit));
const costOptions = (tier: Tier) => {
return tier.costs.filter(cost => payUnits.includes(cost.unit));
}
// costs.reduce((acc, c) => {
// return
// }, [])
// }
const displayCost = (cost: TierCost | undefined) => {
return `${cost?.unit === 'msat' ? Math.ceil(parseInt(cost?.amount || '0') / 1_000) : cost?.amount} sats`;
};
return (
<Modal open={props.author !== undefined} onClose={props.onClose}>
@ -138,38 +162,104 @@ const SubscribeToAuthorModal: Component<{
<div class={styles.body}>
<div class={styles.tiers}>
<For each={store.tiers}>
{tier => (
<button
class={`${styles.tier} ${isSelectedTier(tier) ? styles.selected : ''}`}
onClick={() => selectTier(tier)}
>
<div class={styles.title}>{tier.title}</div>
<div class={styles.cost}>
<div class={styles.amount}>
{tier.costs[0].amount} {tier.costs[0].unit}
</div>
<div class={styles.duration}>
{tier.costs[0].duration}
</div>
</div>
<div class={styles.content}>
{tier.content}
</div>
<div class={styles.perks}>
<For each={tier.perks}>
{perk => (
<div class={styles.perk}>
<div class={styles.checkIcon}></div>
<div class={styles.text}>{perk}</div>
</div>
)}
</For>
<Show
when={!store.isFetchingTiers}
fallback={<div><Loader/></div>}
>
<For
each={store.tiers}
fallback={
<div class={styles.noTiers}>
No compatible tiers found
</div>
}
>
{(tier) => (
<button
class={`${styles.tier} ${isSelectedTier(tier) ? styles.selected : ''}`}
onClick={() => selectTier(tier)}
>
<div class={styles.title}>{tier.title}</div>
</button>
)}
</For>
<Show
when={costOptions(tier).length > 1 && store.selectedTier?.id === tier.id}
fallback={<div class={styles.cost}>
<div class={styles.amount}>
{displayCost(costOptions(tier)[0])}
</div>
<div class={styles.duration}>
{costOptions(tier)[0].cadence}
</div>
</div>}
>
<Select.Root
class={styles.selectCosts}
options={costOptions(tier)}
optionValue="id"
value={store.selectedCost}
onChange={(cost) => {
// updateStore('tiers', index(), 'activeCost', () => ({ ...cost }));
// updateStore('selectedTier', 'activeCost', () => ({ ...cost }));
updateStore('selectedCost', () => ({ ...cost }));
}}
itemComponent={props => (
<Select.Item item={props.item} class={styles.cost}>
<div class={styles.amount}>
{displayCost(props.item.rawValue)}
</div>
<div class={styles.duration}>
{props.item.rawValue.cadence}
</div>
</Select.Item>
)}
>
<Select.Trigger class={styles.selectTrigger}>
<Select.Value class={styles.selectValue}>
{state => {
const cost = state.selectedOption() as TierCost;
return (
<div class={styles.cost}>
<div class={styles.amount}>
{displayCost(cost)}
</div>
<div class={styles.duration}>
<div>{cost?.cadence}</div>
<div class={styles.chevIcon}></div>
</div>
</div>
)
}}
</Select.Value>
</Select.Trigger>
<Select.Portal>
<Select.Content class={styles.selectContent}>
<Select.Listbox class={styles.selectListbox} />
</Select.Content>
</Select.Portal>
</Select.Root>
</Show>
<div class={styles.content}>
{tier.content}
</div>
<div class={styles.perks}>
<For each={tier.perks}>
{perk => (
<div class={styles.perk}>
<div class={styles.checkIcon}></div>
<div class={styles.text}>{perk}</div>
</div>
)}
</For>
</div>
</button>
)}
</For>
</Show>
</div>
</div>
@ -184,13 +274,15 @@ const SubscribeToAuthorModal: Component<{
</ButtonSecondary>
</div>
<div class={styles.payAction}>
<ButtonPrimary
onClick={() => store.selectedTier && props.onSubscribe(store.selectedTier)}
>
subscribe
</ButtonPrimary>
</div>
<Show when={store.selectedTier}>
<div class={styles.payAction}>
<ButtonPrimary
onClick={() => store.selectedTier && store.selectedCost && props.onSubscribe(store.selectedTier, store.selectedCost)}
>
subscribe
</ButtonPrimary>
</div>
</Show>
</div>
</div>
</Modal>

View File

@ -9,7 +9,7 @@ import {
} from "solid-js";
import { PrimalArticle, PrimalNote, PrimalUser, ZapOption } from "../types/primal";
import { CashuMint } from "@cashu/cashu-ts";
import { Tier } from "../components/SubscribeToAuthorModal/SubscribeToAuthorModal";
import { Tier, TierCost } from "../components/SubscribeToAuthorModal/SubscribeToAuthorModal";
import { Kind } from "../constants";
import { sendEvent } from "../lib/notes";
@ -86,7 +86,7 @@ export type AppContextStore = {
openConfirmModal: (confirmInfo: ConfirmInfo) => void,
closeConfirmModal: () => void,
getCashuMint: (url: string) => CashuMint | undefined,
openAuthorSubscribeModal: (author: PrimalUser | undefined, subscribeTo: (tier: Tier) => void) => void,
openAuthorSubscribeModal: (author: PrimalUser | undefined, subscribeTo: (tier: Tier, cost: TierCost) => void) => void,
closeAuthorSubscribeModal: () => void,
},
}
@ -230,7 +230,7 @@ export const AppProvider = (props: { children: JSXElement }) => {
return store.cashuMints.get(formatted);
};
const openAuthorSubscribeModal = (author: PrimalUser | undefined, subscribeTo: (tier: Tier) => void) => {
const openAuthorSubscribeModal = (author: PrimalUser | undefined, subscribeTo: (tier: Tier, cost: TierCost) => void) => {
if (!author) return;
updateStore('subscribeToAuthor', () => ({ ...author }));

View File

@ -577,8 +577,6 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
const fetchNextArticlesPage = () => {
const lastArticle = store.articles[store.articles.length - 1];
console.log('Articles: Next page: ', lastArticle);
if (!lastArticle) {
return;
}
@ -593,10 +591,7 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
const until = noteData[criteria];
console.log('Articles: Next page: until: ', until);
if (until > 0 && store.profileKey) {
console.log('Articles: Next page: call: ', until);
fetchArticles(store.profileKey, until);
}
};

View File

@ -48,7 +48,7 @@ import ArticleSidebar from "../components/HomeSidebar/ArticleSidebar";
import ReplyToNote from "../components/ReplyToNote/ReplyToNote";
import { sanitize } from "dompurify";
import { fetchNotes } from "../handleNotes";
import { Tier } from "../components/SubscribeToAuthorModal/SubscribeToAuthorModal";
import { Tier, TierCost } from "../components/SubscribeToAuthorModal/SubscribeToAuthorModal";
import ButtonPrimary from "../components/Buttons/ButtonPrimary";
import { zapSubscription } from "../lib/zap";
@ -359,7 +359,6 @@ const Longform: Component< { naddr: string } > = (props) => {
fetchArticle();
});
createEffect(() => {
if (store.article?.user) {
getTiers(store.article.user);
@ -391,10 +390,10 @@ const Longform: Component< { naddr: string } > = (props) => {
getAuthorSubscriptionTiers(author.pubkey, subId)
}
const doSubscription = async (tier: Tier) => {
const doSubscription = async (tier: Tier, cost: TierCost) => {
const a = store.article?.user;
if (!a || !account) return;
if (!a || !account || !cost) return;
const subEvent = {
kind: Kind.Subscribe,
@ -403,7 +402,7 @@ const Longform: Component< { naddr: string } > = (props) => {
tags: [
['p', a.pubkey],
['e', tier.id],
['amount', tier.costs[0].amount, tier.costs[0].unit, tier.costs[0].duration],
['amount', cost.amount, cost.unit, cost.cadence],
['event', JSON.stringify(tier.event)],
// Copy any zap splits
...(tier.event.tags?.filter(t => t[0] === 'zap') || []),

View File

@ -45,7 +45,7 @@ import ProfileQrCodeModal from '../components/ProfileQrCodeModal/ProfileQrCodeMo
import { CustomZapInfo, useAppContext } from '../contexts/AppContext';
import ProfileAbout from '../components/ProfileAbout/ProfileAbout';
import ButtonPrimary from '../components/Buttons/ButtonPrimary';
import { Tier } from '../components/SubscribeToAuthorModal/SubscribeToAuthorModal';
import { Tier, TierCost } from '../components/SubscribeToAuthorModal/SubscribeToAuthorModal';
import { Kind } from '../constants';
import { getAuthorSubscriptionTiers } from '../lib/feed';
import { zapSubscription } from '../lib/zap';
@ -544,13 +544,13 @@ const Profile: Component = () => {
},
})
getAuthorSubscriptionTiers(author.pubkey, subId)
getAuthorSubscriptionTiers(author.pubkey, subId);
}
const doSubscription = async (tier: Tier) => {
const doSubscription = async (tier: Tier, cost: TierCost) => {
const a = profile?.userProfile;
if (!a || !account) return;
if (!a || !account || !cost) return;
const subEvent = {
kind: Kind.Subscribe,
@ -559,7 +559,7 @@ const Profile: Component = () => {
tags: [
['p', a.pubkey],
['e', tier.id],
['amount', tier.costs[0].amount, tier.costs[0].unit, tier.costs[0].duration],
['amount', cost.amount, cost.unit, cost.cadence],
['event', JSON.stringify(tier.event)],
// Copy any zap splits
...(tier.event.tags?.filter(t => t[0] === 'zap') || []),