diff options
| -rw-r--r-- | src/components/About.svelte | 5 | ||||
| -rw-r--r-- | src/components/Account.svelte | 53 | ||||
| -rw-r--r-- | src/components/Balance.svelte | 6 | ||||
| -rw-r--r-- | src/components/Items.svelte | 209 | ||||
| -rw-r--r-- | src/lib/english.ts | 2048 | ||||
| -rw-r--r-- | src/lib/nostr.ts | 2 | ||||
| -rw-r--r-- | src/lib/store.ts | 1 | ||||
| -rw-r--r-- | src/locales/en.json | 4 | ||||
| -rw-r--r-- | src/routes/(app)/[username]/(profile)/+layout.svelte | 66 | ||||
| -rw-r--r-- | src/routes/(app)/[username]/(profile)/+page.svelte | 282 | ||||
| -rw-r--r-- | src/routes/(app)/[username]/account/setup/+page.svelte | 64 | ||||
| -rw-r--r-- | src/routes/(app)/[username]/account/setup/step2/+page.svelte | 46 |
12 files changed, 440 insertions, 2346 deletions
diff --git a/src/components/About.svelte b/src/components/About.svelte index 1e60db72..01ff4c7c 100644 --- a/src/components/About.svelte +++ b/src/components/About.svelte @@ -91,14 +91,15 @@ {@html $t("about.locations.description")} </p> <!-- <LocationsMap {locations} /> --> + <a href="/map"> <div class="relative"> <img src="/map.png" class="mx-auto" /> <button - class="bg-black text-white border rounded-full px-6 py-2 font-bold hover:opacity-80 absolute left-1/2 top-1/2 text-2xl" + class="bg-black text-white border border-4 rounded-full px-6 py-2 font-bold hover:bg-secondary absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 text-2xl" > - View map + {$t("about.locations.viewMap")} </button> </div> </a> diff --git a/src/components/Account.svelte b/src/components/Account.svelte new file mode 100644 index 00000000..5286c4d3 --- /dev/null +++ b/src/components/Account.svelte @@ -0,0 +1,53 @@ +<script> + import Icon from "$comp/Icon.svelte"; + import Balance from "$comp/Balance.svelte"; + import { t } from "$lib/translations"; + + export let user, rate, account; + let { balance, setup, type } = account; +</script> + +<div class="border rounded-xl shadow p-4 space-y-5"> + <div class="flex justify-center lg:justify-start"> + <Balance {balance} {user} {rate} /> + <div class="ml-auto text-secondary">{type}</div> + </div> + + <div class="flex flex-wrap gap-3 justify-center w-full text-xl"> + {#if setup} + <a href={`/send`} class="contents grow"> + <button + type="button" + class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-primary grow" + > + <div class="mx-auto flex gap-2"> + <Icon icon="send" style="w-8" /> + <div class="my-auto">{$t("user.dashboard.send")}</div> + </div> + </button> + </a> + + <a href={`/${user.username}/receive`} class="contents"> + <button + class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-primary grow" + > + <div class="mx-auto flex gap-2"> + <Icon icon="receive" style="w-8" /> + <div class="my-auto">{$t("user.dashboard.receive")}</div> + </div> + </button> + </a> + {:else} + <a href={`/${user.username}/account/setup`} class="contents"> + <button + class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-primary grow" + > + <div class="mx-auto flex gap-2"> + <Icon icon="settings" style="w-8" /> + <div class="my-auto">{$t("user.dashboard.setup")}</div> + </div> + </button> + </a> + {/if} + </div> +</div> diff --git a/src/components/Balance.svelte b/src/components/Balance.svelte index 4d5ac6bd..51c56555 100644 --- a/src/components/Balance.svelte +++ b/src/components/Balance.svelte @@ -6,7 +6,7 @@ import { t } from "$lib/translations"; let show; - export let user, rate; + export let user, rate, balance; </script> {#if show && user.haspin && $pin?.length !== 6} @@ -27,12 +27,12 @@ {#if isNaN(rate)} <div class="text-gray-200">—</div> {:else} - {f((user.balance * rate) / sats, user.currency)} + {f((balance * rate) / sats, user.currency)} {/if} </div> <div class="text-secondary text-2xl"> - ⚡️{s(user.balance)} + ⚡️{s(balance)} </div> {/if} </div> diff --git a/src/components/Items.svelte b/src/components/Items.svelte new file mode 100644 index 00000000..f449215c --- /dev/null +++ b/src/components/Items.svelte @@ -0,0 +1,209 @@ +<script> + import { btc, f, sat, post } from "$lib/utils"; + import { browser } from "$app/environment"; + import { goto, invalidate } from "$app/navigation"; + import { fade } from "svelte/transition"; + import { cubicIn } from "svelte/easing"; + import { flip } from "svelte/animate"; + import { + dndzone, + SHADOW_ITEM_MARKER_PROPERTY_NAME, + SOURCES, + TRIGGERS, + } from "svelte-dnd-action"; + import DeleteItem from "$comp/DeleteItem.svelte"; + import Icon from "$comp/Icon.svelte"; + + export let subject, user, items, total, rate, currency; + + let flipDurationMs = 200; + let dropTargetStyle = {}; + + let dragDisabled = + subject?.id !== user?.id || (browser && window.innerWidth < 768); + + let deleting; + let del = (item) => { + deleting = item; + }; + + function handleConsider(e) { + const { + items: newItems, + info: { source, trigger }, + } = e.detail; + items = newItems; + if (source === SOURCES.KEYBOARD && trigger === TRIGGERS.DRAG_STOPPED) + reset(); + } + + async function handleFinalize(e) { + const { + items: newItems, + info: { source }, + } = e.detail; + items = newItems; + if (source === SOURCES.POINTER) reset(); + + if (subject?.id === user?.id) { + await post("/api/items/sort", { items }); + invalidate("app:items"); + } + } + + function startDrag(e) { + e.preventDefault(); + dragDisabled = false; + } + + function handleKeyDown(e) { + if ((e.key === "Enter" || e.key === " ") && dragDisabled) + dragDisabled = false; + } + + $: if (total > 0 && browser) { + let payButton = document.querySelector("#payButton"); + if (payButton) payButton.addEventListener("click", checkout); + } + + let checkout = async () => { + let invoice = { + type: "lightning", + amount: btc(total, rate), + currency, + items: items.filter((i) => i.quantity > 0), + prompt: subject.prompt, + memoPrompt: subject.memoPrompt, + rate, + }; + + let { id } = await post(`/${subject.username}/invoice`, { + invoice, + user: { username: subject.username }, + }); + + if (invoice.memoPrompt) { + return goto(`/${subject.username}/invoice/${id}/memo`); + } + goto(`/${subject.username}/invoice/${id}`); + }; + + let reset = () => + (dragDisabled = + user?.username !== subject?.username || window.innerWidth < 768); +</script> + +<svelte:window on:resize={reset} /> + +{#if deleting} + <DeleteItem bind:item={deleting} /> +{/if} + +{#if items.length} + <h2 class="text-2xl font-bold">Select items</h2> +{/if} +<div + class="grid sm:grid-cols-2 gap-4 pb-20" + use:dndzone={{ items, flipDurationMs, dropTargetStyle, dragDisabled }} + on:consider={handleConsider} + on:finalize={handleFinalize} +> + {#each items as i (i.id)} + <button + type="button" + class="w-full text-center cursor-pointer hover:opacity-80 mb-auto" + on:click={() => i.quantity++} + > + <div class="relative w-full h-64 overflow-hidden rounded-xl"> + {#if i[SHADOW_ITEM_MARKER_PROPERTY_NAME]} + <div + in:fade={{ duration: 200, easing: cubicIn }} + class="custom-shadow-item" + /> + {:else if i.image} + <img + src={`/api/public/${i.image}.webp`} + alt={i.name} + class="mx-auto object-cover h-full w-full" + /> + {:else} + <div + class="bg-gradient-to-r from-primary to-gradient mb-4 cursor-pointer hover:opacity-80 w-full h-full" + alt="Banner" + /> + {/if} + {#if user?.username === subject.username} + <div class="flex gap-2 justify-center absolute top-2 right-2"> + <button type="button" on:click|stopPropagation={() => {}}> + <a href={`/${user.username}/items/${i.id}`}> + <button + class="bg-black rounded-full w-12 h-12 bg-opacity-40 hover:bg-opacity-100" + > + <Icon icon="edit" style="w-8 mx-auto invert" /> + </button> + </a> + </button> + <button + class="bg-black rounded-full w-12 h-12 bg-opacity-40 hover:bg-opacity-100" + on:click|stopPropagation={() => del(i)} + > + <Icon icon="trash" style="w-8 mx-auto invert" /> + </button> + <button + type="button" + on:click|stopPropagation={() => {}} + class="sm:hidden" + on:mousedown={startDrag} + on:touchstart={startDrag} + on:keydown={handleKeyDown} + > + <a href={`/${user.username}/items/${i.id}`}> + <button + class="bg-black rounded-full w-12 h-12 bg-opacity-40 hover:bg-opacity-100" + > + <Icon icon="move" style="w-8 mx-auto invert" /> + </button> + </a> + </button> + </div> + {/if} + </div> + <div class="bg-white rounded-xl py-2"> + <div class="flex text-left gap-2"> + <div class="overflow-hidden space-y-1"> + <div class="text-2xl break-words">{i.name}</div> + <div class="flex gap-2"> + <div class="font-semibold text-lg"> + {f(i.price, currency)} + </div> + <div class="text-secondary my-auto"> + {sat(btc(i.price, rate))} + </div> + </div> + </div> + <div class="flex gap-1 ml-auto mb-auto justify-around tabular-nums"> + {#if i.quantity} + <button + type="button" + on:click|stopPropagation={() => i.quantity > 0 && i.quantity--} + > + <Icon icon="minus" style="w-10 min-w-10" /> + </button> + {/if} + <div class="text-2xl my-auto text-center"> + {#if i.quantity}{i.quantity}{:else}—{/if} + </div> + {#if i.quantity} + <button + type="button" + on:click|stopPropagation={() => i.quantity++} + > + <Icon icon="plus" style="w-10 min-w-10" /> + </button> + {/if} + </div> + </div> + </div> + </button> + {/each} +</div> diff --git a/src/lib/english.ts b/src/lib/english.ts deleted file mode 100644 index 52a0da82..00000000 --- a/src/lib/english.ts +++ /dev/null @@ -1,2048 +0,0 @@ -export default `abandon -ability -able -about -above -absent -absorb -abstract -absurd -abuse -access -accident -account -accuse -achieve -acid -acoustic -acquire -across -act -action -actor -actress -actual -adapt -add -addict -address -adjust -admit -adult -advance -advice -aerobic -affair -afford -afraid -again -age -agent -agree -ahead -aim -air -airport -aisle -alarm -album -alcohol -alert -alien -all -alley -allow -almost -alone -alpha -already -also -alter -always -amateur -amazing -among -amount -amused -analyst -anchor -ancient -anger -angle -angry -animal -ankle -announce -annual -another -answer -antenna -antique -anxiety -any -apart -apology -appear -apple -approve -april -arch -arctic -area -arena -argue -arm -armed -armor -army -around -arrange -arrest -arrive -arrow -art -artefact -artist -artwork -ask -aspect -assault -asset -assist -assume -asthma -athlete -atom -attack -attend -attitude -attract -auction -audit -august -aunt -author -auto -autumn -average -avocado -avoid -awake -aware -away -awesome -awful -awkward -axis -baby -bachelor -bacon -badge -bag -balance -balcony -ball -bamboo -banana -banner -bar -barely -bargain -barrel -base -basic -basket -battle -beach -bean -beauty -because -become -beef -before -begin -behave -behind -believe -below -belt -bench -benefit -best -betray -better -between -beyond -bicycle -bid -bike -bind -biology -bird -birth -bitter -black -blade -blame -blanket -blast -bleak -bless -blind -blood -blossom -blouse -blue -blur -blush -board -boat -body -boil -bomb -bone -bonus -book -boost -border -boring -borrow -boss -bottom -bounce -box -boy -bracket -brain -brand -brass -brave -bread -breeze -brick -bridge -brief -bright -bring -brisk -broccoli -broken -bronze -broom -brother -brown -brush -bubble -buddy -budget -buffalo -build -bulb -bulk -bullet -bundle -bunker -burden -burger -burst -bus -business -busy -butter -buyer -buzz -cabbage -cabin -cable -cactus -cage -cake -call -calm -camera -camp -can -canal -cancel -candy -cannon -canoe -canvas -canyon -capable -capital -captain -car -carbon -card -cargo -carpet -carry -cart -case -cash -casino -castle -casual -cat -catalog -catch -category -cattle -caught -cause -caution -cave -ceiling -celery -cement -census -century -cereal -certain -chair -chalk -champion -change -chaos -chapter -charge -chase -chat -cheap -check -cheese -chef -cherry -chest -chicken -chief -child -chimney -choice -choose -chronic -chuckle -chunk -churn -cigar -cinnamon -circle -citizen -city -civil -claim -clap -clarify -claw -clay -clean -clerk -clever -click -client -cliff -climb -clinic -clip -clock -clog -close -cloth -cloud -clown -club -clump -cluster -clutch -coach -coast -coconut -code -coffee -coil -coin -collect -color -column -combine -come -comfort -comic -common -company -concert -conduct -confirm -congress -connect -consider -control -convince -cook -cool -copper -copy -coral -core -corn -correct -cost -cotton -couch -country -couple -course -cousin -cover -coyote -crack -cradle -craft -cram -crane -crash -crater -crawl -crazy -cream -credit -creek -crew -cricket -crime -crisp -critic -crop -cross -crouch -crowd -crucial -cruel -cruise -crumble -crunch -crush -cry -crystal -cube -culture -cup -cupboard -curious -current -curtain -curve -cushion -custom -cute -cycle -dad -damage -damp -dance -danger -daring -dash -daughter -dawn -day -deal -debate -debris -decade -december -decide -decline -decorate -decrease -deer -defense -define -defy -degree -delay -deliver -demand -demise -denial -dentist -deny -depart -depend -deposit -depth -deputy -derive -describe -desert -design -desk -despair -destroy -detail -detect -develop -device -devote -diagram -dial -diamond -diary -dice -diesel -diet -differ -digital -dignity -dilemma -dinner -dinosaur -direct -dirt -disagree -discover -disease -dish -dismiss -disorder -display -distance -divert -divide -divorce -dizzy -doctor -document -dog -doll -dolphin -domain -donate -donkey -donor -door -dose -double -dove -draft -dragon -drama -drastic -draw -dream -dress -drift -drill -drink -drip -drive -drop -drum -dry -duck -dumb -dune -during -dust -dutch -duty -dwarf -dynamic -eager -eagle -early -earn -earth -easily -east -easy -echo -ecology -economy -edge -edit -educate -effort -egg -eight -either -elbow -elder -electric -elegant -element -elephant -elevator -elite -else -embark -embody -embrace -emerge -emotion -employ -empower -empty -enable -enact -end -endless -endorse -enemy -energy -enforce -engage -engine -enhance -enjoy -enlist -enough -enrich -enroll -ensure -enter -entire -entry -envelope -episode -equal -equip -era -erase -erode -erosion -error -erupt -escape -essay -essence -estate -eternal -ethics -evidence -evil -evoke -evolve -exact -example -excess -exchange -excite -exclude -excuse -execute -exercise -exhaust -exhibit -exile -exist -exit -exotic -expand -expect -expire -explain -expose -express -extend -extra -eye -eyebrow -fabric -face -faculty -fade -faint -faith -fall -false -fame -family -famous -fan -fancy -fantasy -farm -fashion -fat -fatal -father -fatigue -fault -favorite -feature -february -federal -fee -feed -feel -female -fence -festival -fetch -fever -few -fiber -fiction -field -figure -file -film -filter -final -find -fine -finger -finish -fire -firm -first -fiscal -fish -fit -fitness -fix -flag -flame -flash -flat -flavor -flee -flight -flip -float -flock -floor -flower -fluid -flush -fly -foam -focus -fog -foil -fold -follow -food -foot -force -forest -forget -fork -fortune -forum -forward -fossil -foster -found -fox -fragile -frame -frequent -fresh -friend -fringe -frog -front -frost -frown -frozen -fruit -fuel -fun -funny -furnace -fury -future -gadget -gain -galaxy -gallery -game -gap -garage -garbage -garden -garlic -garment -gas -gasp -gate -gather -gauge -gaze -general -genius -genre -gentle -genuine -gesture -ghost -giant -gift -giggle -ginger -giraffe -girl -give -glad -glance -glare -glass -glide -glimpse -globe -gloom -glory -glove -glow -glue -goat -goddess -gold -good -goose -gorilla -gospel -gossip -govern -gown -grab -grace -grain -grant -grape -grass -gravity -great -green -grid -grief -grit -grocery -group -grow -grunt -guard -guess -guide -guilt -guitar -gun -gym -habit -hair -half -hammer -hamster -hand -happy -harbor -hard -harsh -harvest -hat -have -hawk -hazard -head -health -heart -heavy -hedgehog -height -hello -helmet -help -hen -hero -hidden -high -hill -hint -hip -hire -history -hobby -hockey -hold -hole -holiday -hollow -home -honey -hood -hope -horn -horror -horse -hospital -host -hotel -hour -hover -hub -huge -human -humble -humor -hundred -hungry -hunt -hurdle -hurry -hurt -husband -hybrid -ice -icon -idea -identify -idle -ignore -ill -illegal -illness -image -imitate -immense -immune -impact -impose -improve -impulse -inch -include -income -increase -index -indicate -indoor -industry -infant -inflict -inform -inhale -inherit -initial -inject -injury -inmate -inner -innocent -input -inquiry -insane -insect -inside -inspire -install -intact -interest -into -invest -invite -involve -iron -island -isolate -issue -item -ivory -jacket -jaguar -jar -jazz -jealous -jeans -jelly -jewel -job -join -joke -journey -joy -judge -juice -jump -jungle -junior -junk -just -kangaroo -keen -keep -ketchup -key -kick -kid -kidney -kind -kingdom -kiss -kit -kitchen -kite -kitten -kiwi -knee -knife -knock -know -lab -label -labor -ladder -lady -lake -lamp -language -laptop -large -later -latin -laugh -laundry -lava -law -lawn -lawsuit -layer -lazy -leader -leaf -learn -leave -lecture -left -leg -legal -legend -leisure -lemon -lend -length -lens -leopard -lesson -letter -level -liar -liberty -library -license -life -lift -light -like -limb -limit -link -lion -liquid -list -little -live -lizard -load -loan -lobster -local -lock -logic -lonely -long -loop -lottery -loud -lounge -love -loyal -lucky -luggage -lumber -lunar -lunch -luxury -lyrics -machine -mad -magic -magnet -maid -mail -main -major -make -mammal -man -manage -mandate -mango -mansion -manual -maple -marble -march -margin -marine -market -marriage -mask -mass -master -match -material -math -matrix -matter -maximum -maze -meadow -mean -measure -meat -mechanic -medal -media -melody -melt -member -memory -mention -menu -mercy -merge -merit -merry -mesh -message -metal -method -middle -midnight -milk -million -mimic -mind -minimum -minor -minute -miracle -mirror -misery -miss -mistake -mix -mixed -mixture -mobile -model -modify -mom -moment -monitor -monkey -monster -month -moon -moral -more -morning -mosquito -mother -motion -motor -mountain -mouse -move -movie -much -muffin -mule -multiply -muscle -museum -mushroom -music -must -mutual -myself -mystery -myth -naive -name -napkin -narrow -nasty -nation -nature -near -neck -need -negative -neglect -neither -nephew -nerve -nest -net -network -neutral -never -news -next -nice -night -noble -noise -nominee -noodle -normal -north -nose -notable -note -nothing -notice -novel -now -nuclear -number -nurse -nut -oak -obey -object -oblige -obscure -observe -obtain -obvious -occur -ocean -october -odor -off -offer -office -often -oil -okay -old -olive -olympic -omit -once -one -onion -online -only -open -opera -opinion -oppose -option -orange -orbit -orchard -order -ordinary -organ -orient -original -orphan -ostrich -other -outdoor -outer -output -outside -oval -oven -over -own -owner -oxygen -oyster -ozone -pact -paddle -page -pair -palace -palm -panda -panel -panic -panther -paper -parade -parent -park -parrot -party -pass -patch -path -patient -patrol -pattern -pause -pave -payment -peace -peanut -pear -peasant -pelican -pen -penalty -pencil -people -pepper -perfect -permit -person -pet -phone -photo -phrase -physical -piano -picnic -picture -piece -pig -pigeon -pill -pilot -pink -pioneer -pipe -pistol -pitch -pizza -place -planet -plastic -plate -play -please -pledge -pluck -plug -plunge -poem -poet -point -polar -pole -police -pond -pony -pool -popular -portion -position -possible -post -potato -pottery -poverty -powder -power -practice -praise -predict -prefer -prepare -present -pretty -prevent -price -pride -primary -print -priority -prison -private -prize -problem -process -produce -profit -program -project -promote -proof -property -prosper -protect -proud -provide -public -pudding -pull -pulp -pulse -pumpkin -punch -pupil -puppy -purchase -purity -purpose -purse -push -put -puzzle -pyramid -quality -quantum -quarter -question -quick -quit -quiz -quote -rabbit -raccoon -race -rack -radar -radio -rail -rain -raise -rally -ramp -ranch -random -range -rapid -rare -rate -rather -raven -raw -razor -ready -real -reason -rebel -rebuild -recall -receive -recipe -record -recycle -reduce -reflect -reform -refuse -region -regret -regular -reject -relax -release -relief -rely -remain -remember -remind -remove -render -renew -rent -reopen -repair -repeat -replace -report -require -rescue -resemble -resist -resource -response -result -retire -retreat -return -reunion -reveal -review -reward -rhythm -rib -ribbon -rice -rich -ride -ridge -rifle -right -rigid -ring -riot -ripple -risk -ritual -rival -river -road -roast -robot -robust -rocket -romance -roof -rookie -room -rose -rotate -rough -round -route -royal -rubber -rude -rug -rule -run -runway -rural -sad -saddle -sadness -safe -sail -salad -salmon -salon -salt -salute -same -sample -sand -satisfy -satoshi -sauce -sausage -save -say -scale -scan -scare -scatter -scene -scheme -school -science -scissors -scorpion -scout -scrap -screen -script -scrub -sea -search -season -seat -second -secret -section -security -seed -seek -segment -select -sell -seminar -senior -sense -sentence -series -service -session -settle -setup -seven -shadow -shaft -shallow -share -shed -shell -sheriff -shield -shift -shine -ship -shiver -shock -shoe -shoot -shop -short -shoulder -shove -shrimp -shrug -shuffle -shy -sibling -sick -side -siege -sight -sign -silent -silk -silly -silver -similar -simple -since -sing -siren -sister -situate -six -size -skate -sketch -ski -skill -skin -skirt -skull -slab -slam -sleep -slender -slice -slide -slight -slim -slogan -slot -slow -slush -small -smart -smile -smoke -smooth -snack -snake -snap -sniff -snow -soap -soccer -social -sock -soda -soft -solar -soldier -solid -solution -solve -someone -song -soon -sorry -sort -soul -sound -soup -source -south -space -spare -spatial -spawn -speak -special -speed -spell -spend -sphere -spice -spider -spike -spin -spirit -split -spoil -sponsor -spoon -sport -spot -spray -spread -spring -spy -square -squeeze -squirrel -stable -stadium -staff -stage -stairs -stamp -stand -start -state -stay -steak -steel -stem -step -stereo -stick -still -sting -stock -stomach -stone -stool -story -stove -strategy -street -strike -strong -struggle -student -stuff -stumble -style -subject -submit -subway -success -such -sudden -suffer -sugar -suggest -suit -summer -sun -sunny -sunset -super -supply -supreme -sure -surface -surge -surprise -surround -survey -suspect -sustain -swallow -swamp -swap -swarm -swear -sweet -swift -swim -swing -switch -sword -symbol -symptom -syrup -system -table -tackle -tag -tail -talent -talk -tank -tape -target -task -taste -tattoo -taxi -teach -team -tell -ten -tenant -tennis -tent -term -test -text -thank -that -theme -then -theory -there -they -thing -this -thought -three -thrive -throw -thumb -thunder -ticket -tide -tiger -tilt -timber -time -tiny -tip -tired -tissue -title -toast -tobacco -today -toddler -toe -together -toilet -token -tomato -tomorrow -tone -tongue -tonight -tool -tooth -top -topic -topple -torch -tornado -tortoise -toss -total -tourist -toward -tower -town -toy -track -trade -traffic -tragic -train -transfer -trap -trash -travel -tray -treat -tree -trend -trial -tribe -trick -trigger -trim -trip -trophy -trouble -truck -true -truly -trumpet -trust -truth -try -tube -tuition -tumble -tuna -tunnel -turkey -turn -turtle -twelve -twenty -twice -twin -twist -two -type -typical -ugly -umbrella -unable -unaware -uncle -uncover -under -undo -unfair -unfold -unhappy -uniform -unique -unit -universe -unknown -unlock -until -unusual -unveil -update -upgrade -uphold -upon -upper -upset -urban -urge -usage -use -used -useful -useless -usual -utility -vacant -vacuum -vague -valid -valley -valve -van -vanish -vapor -various -vast -vault -vehicle -velvet -vendor -venture -venue -verb -verify -version -very -vessel -veteran -viable -vibrant -vicious -victory -video -view -village -vintage -violin -virtual -virus -visa -visit -visual -vital -vivid -vocal -voice -void -volcano -volume -vote -voyage -wage -wagon -wait -walk -wall -walnut -want -warfare -warm -warrior -wash -wasp -waste -water -wave -way -wealth -weapon -wear -weasel -weather -web -wedding -weekend -weird -welcome -west -wet -whale -what -wheat -wheel -when -where -whip -whisper -wide -width -wife -wild -will -win -window -wine -wing -wink -winner -winter -wire -wisdom -wise -wish -witness -wolf -woman -wonder -wood -wool -word -work -world -worry -worth -wrap -wreck -wrestle -wrist -write -wrong -yard -year -yellow -you -young -youth -zebra -zero -zone -zoo`.split("\n"); diff --git a/src/lib/nostr.ts b/src/lib/nostr.ts index 1a02114d..bd2e65fe 100644 --- a/src/lib/nostr.ts +++ b/src/lib/nostr.ts @@ -18,8 +18,8 @@ import { } from "nostr-tools/nip49"; import { mnemonicToEntropy, entropyToMnemonic } from "@scure/bip39"; -import wordlist from "$lib/english"; import { bech32m } from "@scure/base"; + import { wordlist } from "@scure/bip39/wordlists/english"; import { generateSeedWords, privateKeyFromSeedWords } from "nostr-tools/nip06"; diff --git a/src/lib/store.ts b/src/lib/store.ts index 7609a67c..3c1ff321 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -48,6 +48,7 @@ export let invoice = writable(); export let last = writable(); export let loginRedirect = writable(); export let decrypted = persistLocal("decrypted", {}); +export let mnemonic = persistSession("mnemonic"); export let newPayment = persistLocal("newPayment"); export let password = writable<string | undefined>(); export let passwordPrompt = writable(); diff --git a/src/locales/en.json b/src/locales/en.json index 29929c6b..ad42b8cc 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -19,6 +19,7 @@ } }, "locations": { + "viewMap": "View map", "header": "Locations", "description": "Over 500 businesses worldwide are accepting bitcoin with Coinos. We hope that yours will be next!" } @@ -155,6 +156,7 @@ "subheader": "Latest Updates" }, "user": { + "url": "Public URL", "install": "Install app", "moreDetails": "Profile details", "noMessages": "You have not sent or received any messages", @@ -182,6 +184,7 @@ "this": "This", "lightningAddress": "Lightning address", "dashboard": { + "setup": "Setup self-custody", "payNow": "Pay now", "checkout": "Checkout", "go": "Go", @@ -325,7 +328,6 @@ "voucher": "Payment code", "qr": "QR", "copy": "Copy", - "copyPr": "Copy pay request", "amountPrompt": "Always prompt for amount", "shareLink": "Share link", "copyLink": "Copy link", diff --git a/src/routes/(app)/[username]/(profile)/+layout.svelte b/src/routes/(app)/[username]/(profile)/+layout.svelte index c4750581..2caf9b30 100644 --- a/src/routes/(app)/[username]/(profile)/+layout.svelte +++ b/src/routes/(app)/[username]/(profile)/+layout.svelte @@ -84,11 +84,10 @@ <div class="w-full lg:w-[280px] xl:w-[360px] lg:absolute space-y-5 left-20 mx-auto mb-5" > - <div - class="flex text-3xl font-bold text-center mx-auto justify-center gap-2" - > - <div class="my-auto break-words w-full">{display || username}</div> - </div> + <button type="button" on:click={toggleDetails} class="flex text-3xl font-bold text-center mx-auto justify-center gap-2"> + <div class="my-auto break-words">{display || username}</div> + <Icon icon="qr" style="w-8 invert my-auto" /> + </button> {#if subject.address} <div @@ -112,68 +111,51 @@ <span class="text-secondary">{$t("user.followers")}</span></a > </div> - - <button - type="button" - on:click={toggleDetails} - class="flex text-base text-secondary mx-auto" - > - <div class="mt-1"> - {$t("user.moreDetails")} - </div> - <div - class:-rotate-90={showDetails} - class:rotate-180={!showDetails} - class="my-auto" - > - <Icon icon="chevron-left" style="w-6 mr-2" /> - </div> - </button> </div> {#if showDetails} - <div class="text-xl space-y-5"> + <div class="space-y-5 pb-20"> <div> - <div class="font-bold">{$t("user.profilePage")}</div> + <div class="text-secondary">{$t("user.lightningAddress")}</div> <div class="flex gap-4"> - <div class="break-all grow text-secondary"> - {profile} + <div class="break-all grow text-xl"> + {lnaddr} </div> <div class="flex mb-auto gap-1"> - <button class="my-auto" on:click={() => copy(profile)} + <button class="my-auto" on:click={() => copy(lnaddr)} ><Icon icon="copy" style="max-w-max w-8 min-w-[32px]" /></button > - <a - href={`/qr/${encodeURIComponent( - `${$page.url.protocol}//${profile}` - )}`} - class="my-auto" - > + <a href={`/qr/${encodeURIComponent(lnaddr)}`} class="my-auto"> <Icon icon="qr" style="invert max-w-max min-w-[32px]" /> </a> </div> </div> </div> <div> - <div class="font-bold">{$t("user.lightningAddress")}</div> + <div class="text-secondary">{$t("user.url")}</div> <div class="flex gap-4"> - <div class="break-all grow text-secondary"> - {lnaddr} + <div class="break-all grow text-xl"> + {profile} </div> <div class="flex mb-auto gap-1"> - <button class="my-auto" on:click={() => copy(lnaddr)} + <button class="my-auto" on:click={() => copy(profile)} ><Icon icon="copy" style="max-w-max w-8 min-w-[32px]" /></button > - <a href={`/qr/${encodeURIComponent(lnaddr)}`} class="my-auto"> + <a + href={`/qr/${encodeURIComponent( + `${$page.url.protocol}//${profile}` + )}`} + class="my-auto" + > <Icon icon="qr" style="invert max-w-max min-w-[32px]" /> </a> </div> </div> </div> <div> - <div class="font-bold">{$t("user.nostrPubkey")}</div> + <div class="text-secondary">{$t("user.nostrPubkey")}</div> <div class="flex gap-4"> - <div class="break-all grow text-secondary"> + <div class="break-all grow text-xl"> {npub} </div> <div class="flex my-auto gap-1"> @@ -188,9 +170,9 @@ </div> {#if user.id === subject.id} <div> - <div class="font-bold">{$t("user.nwc")}</div> + <div class="text-secondary">{$t("user.nwc")}</div> <div class="flex gap-4"> - <div class="break-all grow text-secondary"> + <div class="break-all grow text-xl"> {nwc} </div> <div class="flex my-auto gap-1"> diff --git a/src/routes/(app)/[username]/(profile)/+page.svelte b/src/routes/(app)/[username]/(profile)/+page.svelte index 32e6807a..afb4e58c 100644 --- a/src/routes/(app)/[username]/(profile)/+page.svelte +++ b/src/routes/(app)/[username]/(profile)/+page.svelte @@ -1,283 +1,67 @@ <script> - import { browser } from "$app/environment"; - import { goto, invalidate } from "$app/navigation"; - import { btc, f, sat, post } from "$lib/utils"; + import { btc, f, sat } from "$lib/utils"; import Icon from "$comp/Icon.svelte"; + import Account from "$comp/Account.svelte"; import Balance from "$comp/Balance.svelte"; - import DeleteItem from "$comp/DeleteItem.svelte"; + import Items from "$comp/Items.svelte"; import { t } from "$lib/translations"; - import { fade } from "svelte/transition"; - import { cubicIn } from "svelte/easing"; - import { flip } from "svelte/animate"; import { installPrompt } from "$lib/store"; - import { - dndzone, - SHADOW_ITEM_MARKER_PROPERTY_NAME, - SOURCES, - TRIGGERS, - } from "svelte-dnd-action"; - export let data; - let flipDurationMs = 200; - let dropTargetStyle = {}; - let { subject, user, items, rates } = data; $: ({ currency } = subject); $: rate = rates[currency]; - $: refresh(data); - let refresh = (d) => ({ items, subject, user, rates } = d); - let dragDisabled = - subject?.id !== user?.id || (browser && window.innerWidth < 768); - - let deleting; $: total = items.reduce((a, b) => a + b.price * b.quantity, 0); - let del = (item) => { - deleting = item; - }; - - function handleConsider(e) { - const { - items: newItems, - info: { source, trigger }, - } = e.detail; - items = newItems; - if (source === SOURCES.KEYBOARD && trigger === TRIGGERS.DRAG_STOPPED) - reset(); - } - async function handleFinalize(e) { - const { - items: newItems, - info: { source }, - } = e.detail; - items = newItems; - if (source === SOURCES.POINTER) reset(); - - if (subject?.id === user?.id) { - await post("/api/items/sort", { items }); - invalidate("app:items"); - } - } - - function startDrag(e) { - e.preventDefault(); - dragDisabled = false; - } - - function handleKeyDown(e) { - if ((e.key === "Enter" || e.key === " ") && dragDisabled) - dragDisabled = false; - } - - $: if (total > 0 && browser) { - let payButton = document.querySelector("#payButton"); - if (payButton) payButton.addEventListener("click", checkout); - } - - let checkout = async () => { - let invoice = { - type: "lightning", - amount: btc(total, rate), - currency, - items: items.filter((i) => i.quantity > 0), - prompt: subject.prompt, - memoPrompt: subject.memoPrompt, - rate, - }; - - let { id } = await post(`/${subject.username}/invoice`, { - invoice, - user: { username: subject.username }, - }); - - if (invoice.memoPrompt) { - return goto(`/${subject.username}/invoice/${id}/memo`); - } - goto(`/${subject.username}/invoice/${id}`); - }; - - let reset = () => - (dragDisabled = - user?.username !== subject?.username || window.innerWidth < 768); + let refresh = (d) => ({ items, subject, user, rates } = d); + $: refresh(data); let install = async () => { if (!$installPrompt) return; await $installPrompt.prompt(); $installPrompt = null; }; -</script> -<svelte:window on:resize={reset} /> + user.savings = 0; -{#if deleting} - <DeleteItem bind:item={deleting} /> + let accounts = [ + { balance: user.balance, setup: true, type: "Cash" }, + { balance: user.savings, setup: false, type: "Savings" }, + ]; +</script> + +{#if !user.balance} + <div class="mb-8"> + <p class="text-secondary text-lg"> + {$t("user.welcome")} + </p> + </div> {/if} <div class="space-y-5"> {#if user?.id === subject.id} - <div class="flex justify-center lg:justify-start"> - <Balance {user} {rate} /> - </div> - - {#if !user.balance} - <div class="mb-8"> - <p class="text-secondary text-lg"> - {$t("user.welcome")} - </p> - </div> - {/if} - - <div class="flex flex-wrap gap-3 justify-center w-full text-xl"> - <a href={`/send`} class="contents grow"> - <button - type="button" - class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-primary grow" - > - <div class="mx-auto flex gap-2"> - <Icon icon="send" style="w-8" /> - <div class="my-auto">{$t("user.dashboard.send")}</div> - </div> - </button> - </a> - - <a href={`/${user.username}/receive`} class="contents"> - <button - class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-primary grow" - > - <div class="mx-auto flex gap-2"> - <Icon icon="receive" style="w-8" /> - <div class="my-auto">{$t("user.dashboard.receive")}</div> - </div> - </button> - </a> - - {#if $installPrompt} - <button - class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-black text-white grow basis-full fixed bottom-16 xl:hidden" - on:click={install} - > - <div class="mx-auto flex gap-2"> - <Icon icon="save" style="w-8 mx-auto invert" /> - <div class="my-auto text-xl whitespace-nowrap"> - {$t("user.install")} - </div> - </div> - </button> - {/if} - </div> - {/if} + {#each accounts as account} + <Account {user} {rate} {account} /> + {/each} - {#if items.length} - <h2 class="text-2xl font-bold">Select items</h2> - {/if} - <div - class="grid sm:grid-cols-2 gap-4 pb-20" - use:dndzone={{ items, flipDurationMs, dropTargetStyle, dragDisabled }} - on:consider={handleConsider} - on:finalize={handleFinalize} - > - {#each items as i (i.id)} + {#if $installPrompt} <button - type="button" - class="w-full text-center cursor-pointer hover:opacity-80 mb-auto" - on:click={() => i.quantity++} + class="rounded-2xl border py-5 px-6 font-bold hover:opacity-80 flex bg-black text-white grow basis-full fixed bottom-16 xl:hidden" + on:click={install} > - <div class="relative w-full h-64 overflow-hidden rounded-xl"> - {#if i[SHADOW_ITEM_MARKER_PROPERTY_NAME]} - <div - in:fade={{ duration: 200, easing: cubicIn }} - class="custom-shadow-item" - /> - {:else if i.image} - <img - src={`/api/public/${i.image}.webp`} - alt={i.name} - class="mx-auto object-cover h-full w-full" - /> - {:else} - <div - class="bg-gradient-to-r from-primary to-gradient mb-4 cursor-pointer hover:opacity-80 w-full h-full" - alt="Banner" - /> - {/if} - {#if user?.username === subject.username} - <div class="flex gap-2 justify-center absolute top-2 right-2"> - <button type="button" on:click|stopPropagation={() => {}}> - <a href={`/${user.username}/items/${i.id}`}> - <button - class="bg-black rounded-full w-12 h-12 bg-opacity-40 hover:bg-opacity-100" - > - <Icon icon="edit" style="w-8 mx-auto invert" /> - </button> - </a> - </button> - <button - class="bg-black rounded-full w-12 h-12 bg-opacity-40 hover:bg-opacity-100" - on:click|stopPropagation={() => del(i)} - > - <Icon icon="trash" style="w-8 mx-auto invert" /> - </button> - <button - type="button" - on:click|stopPropagation={() => {}} - class="sm:hidden" - on:mousedown={startDrag} - on:touchstart={startDrag} - on:keydown={handleKeyDown} - > - <a href={`/${user.username}/items/${i.id}`}> - <button - class="bg-black rounded-full w-12 h-12 bg-opacity-40 hover:bg-opacity-100" - > - <Icon icon="move" style="w-8 mx-auto invert" /> - </button> - </a> - </button> - </div> - {/if} - </div> - <div class="bg-white rounded-xl py-2"> - <div class="flex text-left gap-2"> - <div class="overflow-hidden space-y-1"> - <div class="text-2xl break-words">{i.name}</div> - <div class="flex gap-2"> - <div class="font-semibold text-lg"> - {f(i.price, currency)} - </div> - <div class="text-secondary my-auto"> - {sat(btc(i.price, rate))} - </div> - </div> - </div> - <div class="flex gap-1 ml-auto mb-auto justify-around tabular-nums"> - {#if i.quantity} - <button - type="button" - on:click|stopPropagation={() => - i.quantity > 0 && i.quantity--} - > - <Icon icon="minus" style="w-10 min-w-10" /> - </button> - {/if} - <div class="text-2xl my-auto text-center"> - {#if i.quantity}{i.quantity}{:else}—{/if} - </div> - {#if i.quantity} - <button - type="button" - on:click|stopPropagation={() => i.quantity++} - > - <Icon icon="plus" style="w-10 min-w-10" /> - </button> - {/if} - </div> + <div class="mx-auto flex gap-2"> + <Icon icon="save" style="w-8 mx-auto invert" /> + <div class="my-auto text-xl whitespace-nowrap"> + {$t("user.install")} </div> </div> </button> - {/each} - </div> + {/if} + {/if} + + <Items {items} {subject} {user} {total} {currency} {rate} /> + <div class="fixed inset-x-0 mx-auto flex bottom-16" class:static={!items.length} diff --git a/src/routes/(app)/[username]/account/setup/+page.svelte b/src/routes/(app)/[username]/account/setup/+page.svelte new file mode 100644 index 00000000..5c2102c7 --- /dev/null +++ b/src/routes/(app)/[username]/account/setup/+page.svelte @@ -0,0 +1,64 @@ +<script> + import { copy } from "$lib/utils"; + import { t } from "$lib/translations"; + import Icon from "$comp/Icon.svelte"; + import { mnemonic } from "$lib/store"; + import { generateMnemonic } from "@scure/bip39"; + import { wordlist } from "@scure/bip39/wordlists/english"; + + export let data; + let { user } = data; + + let reveal; + + let toggleSeed = async () => { + try { + $mnemonic = await generateMnemonic(wordlist); + reveal = !reveal; + } catch (e) { + console.log(e); + } + }; +</script> + +<div class="space-y-5"> + <h1 class="text-center text-3xl font-semibold">Account setup</h1> + <h2 class="text-center text-2xl text-secondary">Step 1</h2> + + <div class="container w-full mx-auto text-lg px-4 max-w-xl space-y-5"> + <p class="text-secondary mb-1"> + The keys to your savings account are derived from a seed phrase. When + you're ready, write it down and keep it safe. + </p> + + <p class="text-secondary mb-1"> + Whoever knows these words can withdraw funds from the account. We can't + help you recover them if you lose them. + </p> + + {#if reveal} + <div class="text-2xl border p-4 text-center rounded-2xl"> + {$mnemonic} + </div> + + <div> + <a href={`/${user.username}/account/setup/step2`} class="contents"> + <button + type="button" + class="bg-black text-white w-full px-5 py-6 text-xl rounded-2xl font-bold" + on:click={toggleSeed} + > + I've written down my seed phrase + </button> + </a> + </div> + {:else} + <div class="flex justify-center"> + <button type="button" class="primary" on:click={toggleSeed}> + <Icon icon="warning" style="mr-2 w-6 my-auto" /> + {$t("user.settings.revealSeed")} + </button> + </div> + {/if} + </div> +</div> diff --git a/src/routes/(app)/[username]/account/setup/step2/+page.svelte b/src/routes/(app)/[username]/account/setup/step2/+page.svelte new file mode 100644 index 00000000..3b403295 --- /dev/null +++ b/src/routes/(app)/[username]/account/setup/step2/+page.svelte @@ -0,0 +1,46 @@ +<script lang="ts"> + import { t } from "$lib/translations"; + import { mnemonic } from "$lib/store"; + import Icon from "$comp/Icon.svelte"; + import { encrypt, decrypt } from "nostr-tools/nip49"; + import { mnemonicToEntropy, entropyToMnemonic } from "@scure/bip39"; + import { wordlist } from "@scure/bip39/wordlists/english"; + + export let data; + let { user } = data; + + let password; + + let submit = async () => { + console.log($mnemonic); + let t = await encrypt(mnemonicToEntropy($mnemonic, wordlist), password); + + let m = await decrypt(t, password); + console.log(entropyToMnemonic(m, wordlist)); + }; +</script> + +<div class="space-y-5"> + <div> + <h1 class="text-center text-3xl font-semibold">Account setup</h1> + <h2 class="text-center text-2xl text-secondary">Step 2</h2> + </div> + + <form + on:submit|preventDefault={submit} + class="container w-full mx-auto text-lg px-4 max-w-xl space-y-5" + > + <p class="text-secondary mb-1"> + Add an additional password to your seed phrase. It should be different + than your login password. We can't reset it if you lose it. + </p> + + <input type="text" placeholder="Password" bind:value={password} /> + + <button + class="bg-black text-white w-full px-5 py-6 text-xl rounded-2xl font-bold" + > + Submit + </button> + </form> +</div> |
