mirror of
https://github.com/PrimalHQ/primal-web-app.git
synced 2024-09-30 00:41:09 +00:00
Add create account flow
This commit is contained in:
parent
bba3e08ce8
commit
13de3e001a
@ -409,6 +409,10 @@ body {
|
||||
|
||||
}
|
||||
|
||||
.invisible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Scrollbars
|
||||
|
||||
/* width */
|
||||
|
@ -9,6 +9,8 @@ import { createSignal } from 'solid-js';
|
||||
|
||||
export const [currentPin, setCurrentPin] = createSignal('');
|
||||
|
||||
export const [tempNsec, setTempNsec] = createSignal<string | undefined>();
|
||||
|
||||
export const generateKeys = (forceNewKey?: boolean) => {
|
||||
const sec = forceNewKey ?
|
||||
generatePrivateKey() :
|
||||
@ -79,11 +81,14 @@ export const decryptWithPin = async (pin: string, cipher: string) => {
|
||||
|
||||
export const PrimalNostr: (pk?: string) => NostrExtension = (pk?: string) => {
|
||||
const getSec = async () => {
|
||||
let sec: string = pk || readSecFromStorage() || generatePrivateKey();
|
||||
let sec: string = pk || readSecFromStorage() || tempNsec() || generatePrivateKey();
|
||||
|
||||
if (sec.startsWith(pinEncodePrefix)) {
|
||||
sec = await decryptWithPin(currentPin(), sec);
|
||||
}
|
||||
|
||||
console.log('SEC: ', sec)
|
||||
|
||||
const decoded = nip19.decode(sec);
|
||||
|
||||
if (decoded.type !== 'nsec' || !decoded.data) {
|
||||
|
@ -3,6 +3,9 @@ import { nip19 } from "nostr-tools"
|
||||
export const hexToNpub = (hex: string | undefined): string => {
|
||||
return hex ? nip19.npubEncode(hex) : '';
|
||||
}
|
||||
export const hexToNsec = (hex: string | undefined): string => {
|
||||
return hex ? nip19.nsecEncode(hex) : '';
|
||||
}
|
||||
|
||||
export const npubToHex = (npub: string | undefined): string => {
|
||||
try {
|
||||
|
@ -325,3 +325,8 @@ export const sendAllowList = async (allowlist: string[], date: number, content:
|
||||
|
||||
return await sendEvent(event, relays, relaySettings);
|
||||
};
|
||||
|
||||
export const getSuggestions = async () => {
|
||||
const resp = await fetch('https://media.primal.net/api/suggestions');
|
||||
console.log('>>>> ', resp)
|
||||
};
|
||||
|
447
src/pages/CreateAccount.module.scss
Normal file
447
src/pages/CreateAccount.module.scss
Normal file
@ -0,0 +1,447 @@
|
||||
.container {
|
||||
// background-color: var(--background-card);
|
||||
min-height: 100vh;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.fullHeader {
|
||||
position: relative;
|
||||
// background-color: var(--background-card);
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.bannerPlaceholder {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 214px;
|
||||
}
|
||||
|
||||
.banner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 214px;
|
||||
|
||||
.uploadingOverlay {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 214px;
|
||||
background-color: var(--background-card);
|
||||
z-index: 1;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
>label, .bannerPlaceholder>label {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
>img {
|
||||
width: 100%;
|
||||
height: 214px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
>div {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--background-card);
|
||||
color: var(--text-primary);
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s ease-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
>div {
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.25s ease-in;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.userImage {
|
||||
position: absolute;
|
||||
top: 148px;
|
||||
left: 15px;
|
||||
z-index: 2;
|
||||
|
||||
.avatar {
|
||||
border: solid 4px var(--background-card);
|
||||
border-radius: 50%;
|
||||
background-color: var(--background-card);
|
||||
|
||||
.uploadingOverlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--background-card);
|
||||
z-index: 3;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.phoneAvatar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.desktopAvatar {
|
||||
position: relative;
|
||||
display: block;
|
||||
|
||||
.uploadAction {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--background-card);
|
||||
color: var(--text-primary);
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s ease-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.uploadAction {
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.25s ease-in;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.blankActions {
|
||||
height: 81px;
|
||||
}
|
||||
|
||||
.uploadActions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.separator {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
margin-block: 28px;
|
||||
background-color: var(--subtile-devider);
|
||||
}
|
||||
|
||||
.uploadButton {
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--accent-1);
|
||||
width: auto;
|
||||
padding: 0;
|
||||
margin-block: 28px;
|
||||
margin-inline: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
text-transform: lowercase;
|
||||
|
||||
>label {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
.inputLabel {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.required {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: var(--text-tertiary-2);
|
||||
|
||||
.star {
|
||||
color: var(--warning-color);
|
||||
font-weight: 600;
|
||||
margin-right: 2PX;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: var(--background-card);
|
||||
color: var(--text-primary);
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
padding-block: 14px;
|
||||
padding-inline: 19px;
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
|
||||
color: var(--text-tertiary-2);
|
||||
opacity: 1; /* Firefox */
|
||||
}
|
||||
|
||||
&:-ms-input-placeholder { /* Internet Explorer 10-11 */
|
||||
color: var(--text-tertiary-2);
|
||||
}
|
||||
|
||||
&::-ms-input-placeholder { /* Microsoft Edge */
|
||||
color: var(--text-tertiary-2);
|
||||
}
|
||||
}
|
||||
|
||||
.inputWithPrefix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing);
|
||||
.inputPrefix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
height: 59px;
|
||||
color: var(--text-tertiary-2);
|
||||
padding-left: 19px;
|
||||
border-radius: 8px 0px 0px 8px;
|
||||
background: var(--background-card);
|
||||
}
|
||||
|
||||
>input {
|
||||
margin-bottom: 0;
|
||||
padding-left: 8px;
|
||||
border-radius: 0px 8px 8px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.inputError {
|
||||
color: var(--warning-color);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 14px;
|
||||
margin-top: calc(var(--spacing) * (-1));
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 720px) {
|
||||
|
||||
.banner {
|
||||
width: 100%;
|
||||
height: 125px;
|
||||
|
||||
>label {
|
||||
>img {
|
||||
width: 100%;
|
||||
height: 125px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.userImage {
|
||||
position: absolute;
|
||||
top: 96px;
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
.phoneAvatar {
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
.uploadAction {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--background-card);
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s ease-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.uploadAction {
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.25s ease-in;
|
||||
}
|
||||
}
|
||||
}
|
||||
.desktopAvatar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.cacheFlag {
|
||||
img {
|
||||
border: 1px solid red;
|
||||
}
|
||||
}
|
||||
|
||||
.horizontalSeparator {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
margin-block: 16px;
|
||||
background-color: var(--subtile-devider);
|
||||
}
|
||||
|
||||
.moreTrigger {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid var(--subtile-devider);
|
||||
padding-block: 16px;
|
||||
|
||||
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--accent-1);
|
||||
width: auto;
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
text-transform: capitalize;
|
||||
margin: 0;
|
||||
|
||||
&.hidden {
|
||||
&::after {
|
||||
border-style: solid;
|
||||
border-width: 0.1em 0.1em 0 0;
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
margin-top: 4px;
|
||||
margin-left: 4px;
|
||||
position: relative;
|
||||
transform: rotate(-220deg);
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
&.shown {
|
||||
&::after {
|
||||
border-style: solid;
|
||||
border-width: 0.1em 0.1em 0 0;
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
margin-top: 8px;
|
||||
margin-left: 4px;
|
||||
position: relative;
|
||||
transform: rotate(-45deg);
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.moreInputs {
|
||||
overflow: hidden;
|
||||
&.hide {
|
||||
max-height: 0;
|
||||
transition: max-height 0.25s ease-out;
|
||||
}
|
||||
&.show {
|
||||
max-height: 210px;
|
||||
transition: max-height 0.25s ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
.formSubmit {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
|
||||
button {
|
||||
width: 80px;
|
||||
height: 28px;
|
||||
margin: 0px 16px 11px 0px;
|
||||
|
||||
&.primaryButton {
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 700;
|
||||
background: var(--brand-gradient-vertical);
|
||||
color: var(--text-primary);
|
||||
>span {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
&.secondaryButton {
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 1px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 700;
|
||||
background: var(--brand-gradient-vertical);
|
||||
color: var(--text-tertiary-2);
|
||||
>div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
border-radius: 6px;
|
||||
background-color: var(--background-card);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,12 +1,584 @@
|
||||
import { Component } from 'solid-js';
|
||||
import MissingPage from '../components/MissingPage/MissingPage';
|
||||
import { useIntl } from '@cookbook/solid-intl';
|
||||
import { useNavigate } from '@solidjs/router';
|
||||
import { Component, createEffect, createSignal, onMount, Show } from 'solid-js';
|
||||
import { APP_ID } from '../App';
|
||||
import Avatar from '../components/Avatar/Avatar';
|
||||
import Loader from '../components/Loader/Loader';
|
||||
import PageCaption from '../components/PageCaption/PageCaption';
|
||||
import PageTitle from '../components/PageTitle/PageTitle';
|
||||
import { useToastContext } from '../components/Toaster/Toaster';
|
||||
import { usernameRegex, Kind } from '../constants';
|
||||
import { useAccountContext } from '../contexts/AccountContext';
|
||||
import { useMediaContext } from '../contexts/MediaContext';
|
||||
import { useProfileContext } from '../contexts/ProfileContext';
|
||||
import { uploadMedia } from '../lib/media';
|
||||
import { getSuggestions, sendProfile } from '../lib/profile';
|
||||
import { subscribeTo as uploadSub } from "../uploadSocket";
|
||||
import {
|
||||
actions as tActions,
|
||||
account as tAccount,
|
||||
settings as tSettings,
|
||||
toast as tToast,
|
||||
} from '../translations';
|
||||
import { NostrMediaUploaded, NostrRelays } from '../types/primal';
|
||||
|
||||
import styles from './CreateAccount.module.scss';
|
||||
import { createStore, reconcile } from 'solid-js/store';
|
||||
import { generateKeys, setTempNsec } from '../lib/PrimalNostr';
|
||||
import { hexToNpub, hexToNsec } from '../lib/keys';
|
||||
import { storeSec } from '../lib/localStore';
|
||||
import { getPreConfiguredRelays } from '../lib/relays';
|
||||
import CreatePinModal from '../components/CreatePinModal/CreatePinModal';
|
||||
|
||||
type AutoSizedTextArea = HTMLTextAreaElement & { _baseScrollHeight: number };
|
||||
|
||||
const CreateAccount: Component = () => { const intl = useIntl();
|
||||
const profile = useProfileContext();
|
||||
const media = useMediaContext();
|
||||
const account = useAccountContext();
|
||||
const toast = useToastContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
let textArea: HTMLTextAreaElement | undefined;
|
||||
let fileUploadAvatar: HTMLInputElement | undefined;
|
||||
let fileUploadBanner: HTMLInputElement | undefined;
|
||||
let nameInput: HTMLInputElement | undefined;
|
||||
|
||||
const [isBannerCached, setIsBannerCached] = createSignal(false);
|
||||
const [isMoreVisible, setIsMoreVisible] = createSignal(false);
|
||||
const [isUploadingAvatar, setIsUploadingAvatar] = createSignal(false);
|
||||
const [isUploadingBanner, setIsUploadingBanner] = createSignal(false);
|
||||
|
||||
const [avatarPreview, setAvatarPreview] = createSignal<string>();
|
||||
const [bannerPreview, setBannerPreview] = createSignal<string>();
|
||||
|
||||
const [isNameValid, setIsNameValid] = createSignal<boolean>(false);
|
||||
|
||||
const flagBannerForWarning = () => {
|
||||
const dev = localStorage.getItem('devMode') === 'true';
|
||||
|
||||
// @ts-ignore
|
||||
if (isBannerCached() || !dev) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return styles.cacheFlag;
|
||||
}
|
||||
|
||||
const imgError = (event: any) => {
|
||||
const banner = document.getElementById('profile_banner');
|
||||
|
||||
if (banner) {
|
||||
banner.innerHTML = `<div class="${styles.bannerPlaceholder}"></div>`;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const banner = () => {
|
||||
const src = bannerPreview();
|
||||
const url = media?.actions.getMediaUrl(src, 'm', true);
|
||||
|
||||
setIsBannerCached(!!url);
|
||||
|
||||
return url ?? src;
|
||||
}
|
||||
|
||||
const setProfile = (hex: string | undefined) => {
|
||||
profile?.actions.setProfileKey(hex);
|
||||
profile?.actions.clearNotes();
|
||||
profile?.actions.fetchNotes(hex);
|
||||
}
|
||||
|
||||
const getScrollHeight = (elm: AutoSizedTextArea) => {
|
||||
var savedValue = elm.value
|
||||
elm.value = ''
|
||||
elm._baseScrollHeight = elm.scrollHeight
|
||||
elm.value = savedValue
|
||||
}
|
||||
|
||||
const onExpandableTextareaInput: () => void = () => {
|
||||
const maxHeight = document.documentElement.clientHeight || window.innerHeight || 0;
|
||||
|
||||
const elm = textArea as AutoSizedTextArea;
|
||||
|
||||
const minRows = parseInt(elm.getAttribute('data-min-rows') || '0');
|
||||
|
||||
!elm._baseScrollHeight && getScrollHeight(elm);
|
||||
|
||||
if (elm.scrollHeight >= (maxHeight / 3)) {
|
||||
return;
|
||||
}
|
||||
|
||||
elm.rows = minRows;
|
||||
const rows = Math.ceil((elm.scrollHeight - elm._baseScrollHeight) / 20);
|
||||
elm.rows = minRows + rows;
|
||||
}
|
||||
|
||||
const onNameInput = () => {
|
||||
const value = nameInput?.value || '';
|
||||
|
||||
setIsNameValid(usernameRegex.test(value))
|
||||
};
|
||||
|
||||
const onUpload = (target: 'picture' | 'banner', fileUpload: HTMLInputElement | undefined) => {
|
||||
|
||||
if (!fileUpload) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = fileUpload.files ? fileUpload.files[0] : null;
|
||||
|
||||
// @ts-ignore fileUpload.value assignment
|
||||
file && uploadFile(file, target, () => fileUpload.value = null);
|
||||
|
||||
}
|
||||
|
||||
const uploadFile = (file: File, target: 'picture' | 'banner', callback?: () => void) => {
|
||||
target === 'banner' && setIsUploadingBanner(true);
|
||||
target === 'picture' && setIsUploadingAvatar(true);
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
if (!e.target?.result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subid = `upload_${APP_ID}`;
|
||||
|
||||
const data = e.target?.result as string;
|
||||
|
||||
const unsub = uploadSub(subid, (type, subId, content) => {
|
||||
|
||||
if (type === 'EVENT') {
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.kind === Kind.Uploaded) {
|
||||
const uploaded = content as NostrMediaUploaded;
|
||||
|
||||
if (target === 'picture') {
|
||||
setAvatarPreview(uploaded.content);
|
||||
return
|
||||
}
|
||||
|
||||
if (target === 'banner') {
|
||||
setBannerPreview(uploaded.content);
|
||||
return
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'NOTICE') {
|
||||
target === 'banner' && setIsUploadingBanner(false);
|
||||
target === 'picture' && setIsUploadingAvatar(false);
|
||||
unsub();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'EOSE') {
|
||||
target === 'banner' && setIsUploadingBanner(false);
|
||||
target === 'picture' && setIsUploadingAvatar(false);
|
||||
unsub();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
uploadMedia(createdAccount.pubkey, subid, data);
|
||||
}
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
callback && callback();
|
||||
}
|
||||
|
||||
const onSubmit = async (e: SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!e.target || !account) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = new FormData(e.target as HTMLFormElement);
|
||||
|
||||
const name = data.get('name')?.toString() || '';
|
||||
|
||||
if (!usernameRegex.test(name)) {
|
||||
toast?.sendWarning(intl.formatMessage(tSettings.profile.name.formError));
|
||||
return false;
|
||||
}
|
||||
|
||||
let relaySettings = getPreConfiguredRelays();
|
||||
|
||||
let metadata: Record<string, string> = {};
|
||||
|
||||
[ 'displayName',
|
||||
'name',
|
||||
'website',
|
||||
'about',
|
||||
'lud16',
|
||||
'nip05',
|
||||
'picture',
|
||||
'banner',
|
||||
].forEach(key => {
|
||||
if (data.get(key)) {
|
||||
metadata[key] = data.get(key) as string;
|
||||
|
||||
if (key === 'displayName') {
|
||||
metadata['display_name'] = data.get(key) as string;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const CreateAccount: Component = () => {
|
||||
const { success } = await sendProfile({ ...metadata }, account.relays, relaySettings);
|
||||
|
||||
if (success) {
|
||||
toast?.sendSuccess(intl.formatMessage(tToast.updateProfileSuccess));
|
||||
|
||||
setShowCreatePin(true);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
toast?.sendWarning(intl.formatMessage(tToast.updateProfileFail))
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const [createdAccount, setCreatedAccount] = createStore<{ sec?: string, pubkey?: string, relays?: NostrRelays }>({});
|
||||
const [currentStep, setCurrentStep] = createSignal<'name' | 'info' | 'follow'>('name');
|
||||
const [showCreatePin, setShowCreatePin] = createSignal(false);
|
||||
|
||||
const toNext = () => {
|
||||
switch(currentStep()) {
|
||||
case 'name':
|
||||
setCurrentStep('info');
|
||||
break;
|
||||
case 'info':
|
||||
setCurrentStep('follow');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const toPrevious = () => {
|
||||
switch(currentStep()) {
|
||||
case 'info':
|
||||
setCurrentStep('name');
|
||||
break;
|
||||
case 'follow':
|
||||
setCurrentStep('info');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
const { sec, pubkey } = generateKeys(true);
|
||||
|
||||
const nsec = hexToNsec(sec);
|
||||
|
||||
account?.actions.setSec(nsec);
|
||||
setTempNsec(nsec);
|
||||
|
||||
setCreatedAccount(() => ({ sec: nsec, pubkey }));
|
||||
getSuggestions()
|
||||
});
|
||||
|
||||
|
||||
const onStoreSec = (sec: string | undefined) => {
|
||||
storeSec(sec);
|
||||
setTempNsec(undefined);
|
||||
setCreatedAccount(reconcile({}));
|
||||
onAbort();
|
||||
navigate('/');
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
setShowCreatePin(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<MissingPage title="create account" />
|
||||
</>
|
||||
|
||||
<div class={styles.container}>
|
||||
<PageTitle title={intl.formatMessage(tAccount.create.title)} />
|
||||
|
||||
<PageCaption title={intl.formatMessage(tAccount.create.title)} />
|
||||
|
||||
<div class={['name', 'info'].includes(currentStep()) ? '' : 'invisible'}>
|
||||
<div id="central_header" class={styles.fullHeader}>
|
||||
<div id="profile_banner" class={`${styles.banner} ${flagBannerForWarning()}`}>
|
||||
<Show when={isUploadingBanner()}>
|
||||
<div class={styles.uploadingOverlay}><Loader /></div>
|
||||
</Show>
|
||||
<Show
|
||||
when={banner()}
|
||||
fallback={
|
||||
<div class={styles.bannerPlaceholder}>
|
||||
<label for="upload-banner">
|
||||
<div>{intl.formatMessage(tSettings.profile.uploadBanner)}</div>
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<label for="upload-banner">
|
||||
<img
|
||||
src={banner()}
|
||||
onerror={imgError}
|
||||
/>
|
||||
<div>{intl.formatMessage(tSettings.profile.uploadBanner)}</div>
|
||||
</label>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class={styles.userImage}>
|
||||
<div class={styles.avatar}>
|
||||
<Show when={isUploadingAvatar()}>
|
||||
<div class={styles.uploadingOverlay}><Loader /></div>
|
||||
</Show>
|
||||
<label for="upload-avatar">
|
||||
<div class={styles.desktopAvatar}>
|
||||
<Avatar src={avatarPreview()} size="xxl" />
|
||||
<div class={styles.uploadAction}>
|
||||
{intl.formatMessage(tSettings.profile.uploadAvatar)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class={styles.phoneAvatar}>
|
||||
<Avatar src={avatarPreview()} size="lg" />
|
||||
<div class={styles.uploadAction}>
|
||||
{intl.formatMessage(tSettings.profile.uploadAvatar)}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={currentStep() === 'name'}
|
||||
fallback={
|
||||
<div class={styles.blankActions}></div>
|
||||
}
|
||||
>
|
||||
<div class={styles.uploadActions}>
|
||||
<div class={styles.uploadButton}>
|
||||
<input
|
||||
id="upload-avatar"
|
||||
type="file"
|
||||
onChange={() => onUpload('picture', fileUploadAvatar)}
|
||||
ref={fileUploadAvatar}
|
||||
hidden={true}
|
||||
accept="image/*"
|
||||
/>
|
||||
<label for="upload-avatar">
|
||||
{intl.formatMessage(tSettings.profile.uploadAvatar)}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class={styles.separator}></div>
|
||||
|
||||
<div class={styles.uploadButton}>
|
||||
<input
|
||||
id="upload-banner"
|
||||
type="file"
|
||||
onchange={() => onUpload('banner', fileUploadBanner)}
|
||||
ref={fileUploadBanner}
|
||||
hidden={true}
|
||||
accept="image/*"
|
||||
/>
|
||||
<label for="upload-banner">
|
||||
{intl.formatMessage(tSettings.profile.uploadBanner)}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={onSubmit}>
|
||||
<div class={currentStep() === 'name' ? '' : 'invisible'}>
|
||||
<div class={styles.inputLabel}>
|
||||
<label for='name'>{intl.formatMessage(tSettings.profile.name.label)}</label>
|
||||
<span class={styles.required}>
|
||||
<span class={styles.star}>*</span>
|
||||
{intl.formatMessage(tSettings.profile.required)}
|
||||
</span>
|
||||
</div>
|
||||
<div class={styles.inputWithPrefix}>
|
||||
<div class={styles.inputPrefix}>
|
||||
@
|
||||
</div>
|
||||
<input
|
||||
name='name'
|
||||
type='text'
|
||||
ref={nameInput}
|
||||
class={styles.inputWithPrefix}
|
||||
placeholder={intl.formatMessage(tSettings.profile.name.placeholder)}
|
||||
value={profile?.userProfile?.name || ''}
|
||||
onInput={onNameInput}
|
||||
/>
|
||||
</div>
|
||||
<Show when={!isNameValid()}>
|
||||
<div class={styles.inputError}>
|
||||
{intl.formatMessage(tSettings.profile.name.error)}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class={styles.inputLabel}>
|
||||
<label for='displayName'>{intl.formatMessage(tSettings.profile.displayName.label)}</label>
|
||||
</div>
|
||||
<input
|
||||
name='displayName'
|
||||
type='text'
|
||||
placeholder={intl.formatMessage(tSettings.profile.displayName.placeholder)}
|
||||
value={profile?.userProfile?.displayName || profile?.userProfile?.display_name || ''}
|
||||
/>
|
||||
|
||||
<div class={`${styles.moreInputs} ${isMoreVisible() ? styles.show : styles.hide}`}>
|
||||
<div class={styles.inputLabel}>
|
||||
<label for='picture'>{intl.formatMessage(tSettings.profile.picture.label)}</label>
|
||||
</div>
|
||||
<input
|
||||
name='picture'
|
||||
type='text'
|
||||
placeholder={intl.formatMessage(tSettings.profile.picture.placeholder)}
|
||||
value={avatarPreview() || ''}
|
||||
onChange={(e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
target.value && setAvatarPreview(target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class={styles.inputLabel}>
|
||||
<label for='banner'>{intl.formatMessage(tSettings.profile.banner.label)}</label>
|
||||
</div>
|
||||
<input
|
||||
name='banner'
|
||||
type='text'
|
||||
placeholder={intl.formatMessage(tSettings.profile.banner.placeholder)}
|
||||
value={bannerPreview() || ''}
|
||||
onChange={(e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
target.value && setBannerPreview(target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class={currentStep() === 'info' ? '' : 'invisible'}>
|
||||
<div class={styles.inputLabel}>
|
||||
<label for='website'>{intl.formatMessage(tSettings.profile.website.label)}</label>
|
||||
</div>
|
||||
<input
|
||||
name='website'
|
||||
type='text'
|
||||
placeholder={intl.formatMessage(tSettings.profile.website.placeholder)}
|
||||
value={profile?.userProfile?.website || ''}
|
||||
/>
|
||||
|
||||
<div class={styles.inputLabel}>
|
||||
<label for='about'>{intl.formatMessage(tSettings.profile.about.label)}</label>
|
||||
</div>
|
||||
<textarea
|
||||
name='about'
|
||||
placeholder={intl.formatMessage(tSettings.profile.about.placeholder)}
|
||||
value={profile?.userProfile?.about || ''}
|
||||
ref={textArea}
|
||||
rows={1}
|
||||
data-min-rows={1}
|
||||
onInput={onExpandableTextareaInput}
|
||||
/>
|
||||
|
||||
<div class={styles.inputLabel}>
|
||||
<label for='lud16'>{intl.formatMessage(tSettings.profile.lud16.label)}</label>
|
||||
</div>
|
||||
<input
|
||||
name='lud16'
|
||||
type='text'
|
||||
placeholder={intl.formatMessage(tSettings.profile.lud16.placeholder)}
|
||||
value={profile?.userProfile?.lud16 || ''}
|
||||
/>
|
||||
|
||||
<div class={styles.inputLabel}>
|
||||
<label for='nip05'>{intl.formatMessage(tSettings.profile.nip05.label)}</label>
|
||||
</div>
|
||||
<input
|
||||
name='nip05'
|
||||
type='text'
|
||||
placeholder={intl.formatMessage(tSettings.profile.nip05.placeholder)}
|
||||
value={profile?.userProfile?.nip05 || ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={currentStep() === 'follow' ? '' : 'invisible'}>
|
||||
<div>
|
||||
HERE BE FOLLOWS
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class={styles.formSubmit}>
|
||||
<Show when={currentStep() !== 'name'}>
|
||||
<button
|
||||
type='button'
|
||||
class={styles.secondaryButton}
|
||||
onClick={toPrevious}
|
||||
>
|
||||
<div>
|
||||
<span>
|
||||
{intl.formatMessage(tActions.previous)}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</Show>
|
||||
<Show
|
||||
when={currentStep() === 'follow'}
|
||||
fallback={
|
||||
<button
|
||||
type='button'
|
||||
class={styles.primaryButton}
|
||||
disabled={currentStep() === 'name' && !isNameValid()}
|
||||
onClick={toNext}
|
||||
>
|
||||
{intl.formatMessage(tActions.next)}
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type='submit'
|
||||
class={styles.primaryButton}
|
||||
disabled={!isNameValid()}
|
||||
>
|
||||
{intl.formatMessage(tActions.finish)}
|
||||
</button>
|
||||
</Show>
|
||||
<button
|
||||
type='button'
|
||||
class={styles.secondaryButton}
|
||||
onClick={() => navigate('/profile')}
|
||||
>
|
||||
<div>
|
||||
<span>{intl.formatMessage(tActions.cancel)}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<CreatePinModal
|
||||
open={showCreatePin()}
|
||||
onAbort={() => {
|
||||
onStoreSec(createdAccount.sec);
|
||||
}}
|
||||
valueToEncrypt={createdAccount.sec}
|
||||
onPinApplied={onStoreSec}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,21 @@ export const account = {
|
||||
defaultMessage: 'New to Nostr? Create your account now and join this magical place. It’s quick and easy!',
|
||||
description: 'Label inviting users to join Nostr',
|
||||
},
|
||||
create: {
|
||||
title: {
|
||||
id: 'settings.account.title',
|
||||
defaultMessage: 'Create Account',
|
||||
description: 'Title of the create account page',
|
||||
},
|
||||
descriptions: {
|
||||
step_one: {
|
||||
id: 'settings.account.descriptions.step_on',
|
||||
defaultMessage: "Let’s start with the basics. Only the username is required!",
|
||||
description: 'Description on step one',
|
||||
},
|
||||
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const login = {
|
||||
@ -188,6 +203,21 @@ export const actions = {
|
||||
defaultMessage: 'save',
|
||||
description: 'Save changes action label',
|
||||
},
|
||||
previous: {
|
||||
id: 'actions.previous',
|
||||
defaultMessage: 'previous',
|
||||
description: 'Go to previous step action label',
|
||||
},
|
||||
next: {
|
||||
id: 'actions.next',
|
||||
defaultMessage: 'next',
|
||||
description: 'Go to next step action label',
|
||||
},
|
||||
finish: {
|
||||
id: 'actions.finish',
|
||||
defaultMessage: 'finish',
|
||||
description: 'Finish the wizard action label',
|
||||
},
|
||||
editProfile: {
|
||||
id: 'actions.editProfile',
|
||||
defaultMessage: 'edit profile',
|
||||
|
Loading…
Reference in New Issue
Block a user