wip
This commit is contained in:
@@ -1 +0,0 @@
|
||||
export { LayoutOnboarding as Layout } from "./layout";
|
||||
@@ -4,39 +4,39 @@ import { useProfile } from "@utils/hooks/useProfile";
|
||||
import { shortenKey } from "@utils/shortenKey";
|
||||
|
||||
export function User({ pubkey }: { pubkey: string }) {
|
||||
const { user } = useProfile(pubkey);
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative h-11 w-11 shrink-0 rounded-md bg-zinc-800 animate-pulse" />
|
||||
<div className="flex w-full flex-1 flex-col items-start gap-1 text-start">
|
||||
<span className="w-full h-4 rounded bg-zinc-800 animate-pulse" />
|
||||
<span className="w-1/2 h-3 rounded bg-zinc-800 animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const { status, user } = useProfile(pubkey);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative h-11 w-11 shrink rounded-md">
|
||||
<Image
|
||||
src={user.image}
|
||||
fallback={DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
className="h-11 w-11 rounded-md object-cover"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-1 flex-col items-start text-start">
|
||||
<span className="truncate font-medium leading-tight text-zinc-100">
|
||||
{user.displayName || user.name}
|
||||
</span>
|
||||
<span className="text-base leading-tight text-zinc-400">
|
||||
{user.nip05?.toLowerCase() || shortenKey(pubkey)}
|
||||
</span>
|
||||
</div>
|
||||
{status === "loading" ? (
|
||||
<>
|
||||
<div className="relative h-11 w-11 shrink-0 rounded-md bg-zinc-800 animate-pulse" />
|
||||
<div className="flex w-full flex-1 flex-col items-start gap-1 text-start">
|
||||
<span className="w-1/2 h-4 rounded bg-zinc-800 animate-pulse" />
|
||||
<span className="w-1/3 h-3 rounded bg-zinc-800 animate-pulse" />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="relative h-11 w-11 shrink rounded-md">
|
||||
<Image
|
||||
src={user.image}
|
||||
fallback={DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
className="h-11 w-11 rounded-md object-cover"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-1 flex-col items-start text-start">
|
||||
<span className="truncate font-medium leading-tight text-zinc-100">
|
||||
{user.displayName || user.name}
|
||||
</span>
|
||||
<span className="text-base leading-tight text-zinc-400">
|
||||
{user.nip05?.toLowerCase() || shortenKey(pubkey)}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
9
src/app/auth/create/index.tsx
Normal file
9
src/app/auth/create/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
|
||||
export function AuthCreateScreen() {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
105
src/app/auth/create/step-1.tsx
Normal file
105
src/app/auth/create/step-1.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { createAccount } from "@libs/storage";
|
||||
import { Button } from "@shared/button";
|
||||
import { EyeOffIcon, EyeOnIcon } from "@shared/icons";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { generatePrivateKey, getPublicKey, nip19 } from "nostr-tools";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export function CreateStep1Screen() {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [type, setType] = useState("password");
|
||||
|
||||
const privkey = useMemo(() => generatePrivateKey(), []);
|
||||
const pubkey = getPublicKey(privkey);
|
||||
const npub = nip19.npubEncode(pubkey);
|
||||
const nsec = nip19.nsecEncode(privkey);
|
||||
|
||||
// toggle private key
|
||||
const showPrivateKey = () => {
|
||||
if (type === "password") {
|
||||
setType("text");
|
||||
} else {
|
||||
setType("password");
|
||||
}
|
||||
};
|
||||
|
||||
const account = useMutation({
|
||||
mutationFn: (data: any) =>
|
||||
createAccount(data.npub, data.pubkey, data.privkey, null, 1),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["currentAccount"] });
|
||||
// redirect to next step
|
||||
navigate("/auth/create/step-2", { replace: true });
|
||||
},
|
||||
});
|
||||
|
||||
const submit = async () => {
|
||||
account.mutate({
|
||||
npub,
|
||||
pubkey,
|
||||
privkey,
|
||||
follows: null,
|
||||
is_active: 1,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-xl font-semibold text-zinc-100">
|
||||
Lume is auto-generated key for you
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-base font-semibold text-zinc-400">
|
||||
Public Key
|
||||
</label>
|
||||
<input
|
||||
readOnly
|
||||
value={npub}
|
||||
className="relative w-full rounded-lg py-3 pl-3.5 pr-11 !outline-none placeholder:text-zinc-400 bg-zinc-800 text-zinc-100"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-base font-semibold text-zinc-400">
|
||||
Private Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
readOnly
|
||||
type={type}
|
||||
value={nsec}
|
||||
className="relative w-full rounded-lg py-3 pl-3.5 pr-11 !outline-none placeholder:text-zinc-400 bg-zinc-800 text-zinc-100"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => showPrivateKey()}
|
||||
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
|
||||
>
|
||||
{type === "password" ? (
|
||||
<EyeOffIcon
|
||||
width={20}
|
||||
height={20}
|
||||
className="text-zinc-500 group-hover:text-zinc-100"
|
||||
/>
|
||||
) : (
|
||||
<EyeOnIcon
|
||||
width={20}
|
||||
height={20}
|
||||
className="text-zinc-500 group-hover:text-zinc-100"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Button preset="large" onClick={() => submit()}>
|
||||
Continue →
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
112
src/app/auth/create/step-2.tsx
Normal file
112
src/app/auth/create/step-2.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { AvatarUploader } from "@shared/avatarUploader";
|
||||
import { LoaderIcon } from "@shared/icons";
|
||||
import { Image } from "@shared/image";
|
||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||
import { useOnboarding } from "@stores/onboarding";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export function CreateStep2Screen() {
|
||||
const navigate = useNavigate();
|
||||
const createProfile = useOnboarding((state: any) => state.createProfile);
|
||||
|
||||
const [image, setImage] = useState(DEFAULT_AVATAR);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { isDirty, isValid },
|
||||
} = useForm();
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const profile = { ...data, name: data.displayName };
|
||||
createProfile(profile);
|
||||
// redirect to step 3
|
||||
navigate("/auth/create/step-3");
|
||||
} catch {
|
||||
console.log("error");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setValue("picture", image);
|
||||
}, [setValue, image]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-xl font-semibold text-zinc-100">
|
||||
Create your profile
|
||||
</h1>
|
||||
</div>
|
||||
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900 p-5">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
|
||||
<input
|
||||
type={"hidden"}
|
||||
{...register("picture")}
|
||||
value={image}
|
||||
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
||||
Avatar
|
||||
</label>
|
||||
<div className="relative inline-flex h-36 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
|
||||
<Image
|
||||
src={image}
|
||||
fallback={DEFAULT_AVATAR}
|
||||
alt="avatar"
|
||||
className="relative z-10 h-11 w-11 rounded-md"
|
||||
/>
|
||||
<div className="absolute bottom-3 right-3 z-10">
|
||||
<AvatarUploader valueState={setImage} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
||||
Display Name *
|
||||
</label>
|
||||
<input
|
||||
type={"text"}
|
||||
{...register("displayName", {
|
||||
required: true,
|
||||
minLength: 4,
|
||||
})}
|
||||
spellCheck={false}
|
||||
className="relative h-10 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
||||
Bio
|
||||
</label>
|
||||
<textarea
|
||||
{...register("about")}
|
||||
spellCheck={false}
|
||||
className="resize-none relative h-20 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isDirty || !isValid}
|
||||
className="inline-flex items-center justify-center h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||
) : (
|
||||
"Continue →"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
95
src/app/auth/create/step-3.tsx
Normal file
95
src/app/auth/create/step-3.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||
import { Button } from "@shared/button";
|
||||
import { LoaderIcon } from "@shared/icons";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { useOnboarding } from "@stores/onboarding";
|
||||
import { Body, fetch } from "@tauri-apps/api/http";
|
||||
import { useAccount } from "@utils/hooks/useAccount";
|
||||
import { useContext, useState } from "react";
|
||||
|
||||
export function CreateStep3Screen() {
|
||||
const ndk = useContext(RelayContext);
|
||||
const profile = useOnboarding((state: any) => state.profile);
|
||||
const { account } = useAccount();
|
||||
|
||||
const [username, setUsername] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const createNIP05 = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const response = await fetch("https://lume.nu/api/user-create", {
|
||||
method: "POST",
|
||||
timeout: 30,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
body: Body.json({
|
||||
username: username,
|
||||
pubkey: account.pubkey,
|
||||
lightningAddress: "",
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = { ...profile, nip05: `${username}@lume.nu` };
|
||||
|
||||
const signer = new NDKPrivateKeySigner(account.privkey);
|
||||
ndk.signer = signer;
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
// build event
|
||||
event.content = JSON.stringify(data);
|
||||
event.kind = 0;
|
||||
event.pubkey = account.pubkey;
|
||||
event.tags = [];
|
||||
// publish event
|
||||
event.publish();
|
||||
|
||||
// redirect to step 4
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
console.error("Error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-xl font-semibold text-zinc-100">
|
||||
Create your Lume ID
|
||||
</h1>
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-center items-center gap-4">
|
||||
<div className="w-full inline-flex items-center justify-center gap-2 rounded-lg bg-zinc-800">
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
autoCapitalize="false"
|
||||
autoCorrect="none"
|
||||
spellCheck="false"
|
||||
placeholder="satoshi"
|
||||
className="relative w-full py-3 pl-3.5 !outline-none placeholder:text-zinc-500 bg-transparent text-zinc-100"
|
||||
/>
|
||||
<span className="text-fuchsia-500 font-semibold pr-3.5">
|
||||
@lume.nu
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
preset="large"
|
||||
onClick={() => createNIP05()}
|
||||
disabled={username.length === 0}
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||
) : (
|
||||
"Continue →"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { User } from "@app/auth/components/user";
|
||||
import { updateAccount } from "@libs/storage";
|
||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||
import { CheckCircleIcon, LoaderIcon } from "@shared/icons";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useAccount } from "@utils/hooks/useAccount";
|
||||
import { arrayToNIP02 } from "@utils/transform";
|
||||
import { useContext, useState } from "react";
|
||||
import { navigate } from "vite-plugin-ssr/client/router";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const initialList = [
|
||||
{
|
||||
@@ -106,15 +108,15 @@ const initialList = [
|
||||
},
|
||||
];
|
||||
|
||||
export function Page() {
|
||||
export function CreateStep4Screen() {
|
||||
const ndk = useContext(RelayContext);
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [follows, setFollows] = useState([]);
|
||||
const [account, updateFollows] = useActiveAccount((state: any) => [
|
||||
state.account,
|
||||
state.updateFollows,
|
||||
]);
|
||||
|
||||
const { account } = useAccount();
|
||||
|
||||
// toggle follow state
|
||||
const toggleFollow = (pubkey: string) => {
|
||||
@@ -124,6 +126,16 @@ export function Page() {
|
||||
setFollows(arr);
|
||||
};
|
||||
|
||||
const update = useMutation({
|
||||
mutationFn: (follows: any) =>
|
||||
updateAccount("follows", follows, account.pubkey),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["currentAccount"] });
|
||||
// redirect to next step
|
||||
navigate("/auth/onboarding", { replace: true });
|
||||
},
|
||||
});
|
||||
|
||||
// save follows to database then broadcast
|
||||
const submit = async () => {
|
||||
try {
|
||||
@@ -142,75 +154,64 @@ export function Page() {
|
||||
// publish event
|
||||
event.publish();
|
||||
|
||||
// update account follows
|
||||
updateFollows(follows);
|
||||
|
||||
// redirect to onboarding
|
||||
setTimeout(
|
||||
() =>
|
||||
navigate("/app/onboarding", {
|
||||
overwriteLastHistoryEntry: true,
|
||||
}),
|
||||
2000,
|
||||
);
|
||||
// update
|
||||
update.mutate(follows);
|
||||
} catch {
|
||||
console.log("error");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-xl font-semibold text-zinc-100">
|
||||
Personalized your newsfeed
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900 overflow-hidden">
|
||||
<div className="inline-flex h-10 w-full items-center gap-1 border-b border-zinc-800 px-4 text-base font-medium text-zinc-400">
|
||||
Follow at least
|
||||
<span className="text-fuchsia-500 font-semibold">
|
||||
{follows.length}/10
|
||||
</span>{" "}
|
||||
plebs
|
||||
</div>
|
||||
<div className="scrollbar-hide flex h-96 flex-col overflow-y-auto py-2">
|
||||
{initialList.map((item: { pubkey: string }, index: number) => (
|
||||
<button
|
||||
key={`item-${index}`}
|
||||
type="button"
|
||||
onClick={() => toggleFollow(item.pubkey)}
|
||||
className="inline-flex transform items-center justify-between bg-zinc-900 px-4 py-2 hover:bg-zinc-800 active:translate-y-1"
|
||||
>
|
||||
<User pubkey={item.pubkey} />
|
||||
{follows.includes(item.pubkey) && (
|
||||
<div>
|
||||
<CheckCircleIcon
|
||||
width={16}
|
||||
height={16}
|
||||
className="text-green-400"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-xl font-semibold text-zinc-100">
|
||||
Personalized your newsfeed
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900 overflow-hidden">
|
||||
<div className="inline-flex h-10 w-full items-center gap-1 border-b border-zinc-800 px-4 text-base font-medium text-zinc-400">
|
||||
Follow at least
|
||||
<span className="text-fuchsia-500 font-semibold">
|
||||
{follows.length}/10
|
||||
</span>{" "}
|
||||
plebs
|
||||
</div>
|
||||
<div className="scrollbar-hide flex h-96 flex-col overflow-y-auto py-2">
|
||||
{initialList.map((item: { pubkey: string }, index: number) => (
|
||||
<button
|
||||
key={`item-${index}`}
|
||||
type="button"
|
||||
onClick={() => toggleFollow(item.pubkey)}
|
||||
className="inline-flex transform items-center justify-between bg-zinc-900 px-4 py-2 hover:bg-zinc-800 active:translate-y-1"
|
||||
>
|
||||
<User pubkey={item.pubkey} />
|
||||
{follows.includes(item.pubkey) && (
|
||||
<div>
|
||||
<CheckCircleIcon
|
||||
width={16}
|
||||
height={16}
|
||||
className="text-green-400"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{follows.length >= 10 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
className="inline-flex items-center justify-center h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||
) : (
|
||||
"Finish →"
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{follows.length >= 10 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
className="inline-flex items-center justify-center h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||
) : (
|
||||
"Finish →"
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
9
src/app/auth/import/index.tsx
Normal file
9
src/app/auth/import/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
|
||||
export function AuthImportScreen() {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
111
src/app/auth/import/step-1.tsx
Normal file
111
src/app/auth/import/step-1.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { createAccount } from "@libs/storage";
|
||||
import { LoaderIcon } from "@shared/icons";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { getPublicKey, nip19 } from "nostr-tools";
|
||||
import { Resolver, useForm } from "react-hook-form";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
type FormValues = {
|
||||
key: string;
|
||||
};
|
||||
|
||||
const resolver: Resolver<FormValues> = async (values) => {
|
||||
return {
|
||||
values: values.key ? values : {},
|
||||
errors: !values.key
|
||||
? {
|
||||
key: {
|
||||
type: "required",
|
||||
message: "This is required.",
|
||||
},
|
||||
}
|
||||
: {},
|
||||
};
|
||||
};
|
||||
|
||||
export function ImportStep1Screen() {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const account = useMutation({
|
||||
mutationFn: (data: any) =>
|
||||
createAccount(data.npub, data.pubkey, data.privkey, null, 1),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["currentAccount"] });
|
||||
// redirect to next step
|
||||
navigate("/auth/import/step-2", { replace: true });
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
setError,
|
||||
handleSubmit,
|
||||
formState: { errors, isDirty, isValid, isSubmitting },
|
||||
} = useForm<FormValues>({ resolver });
|
||||
|
||||
const onSubmit = async (data: any) => {
|
||||
try {
|
||||
let privkey = data["key"];
|
||||
|
||||
if (privkey.substring(0, 4) === "nsec") {
|
||||
privkey = nip19.decode(privkey).data;
|
||||
}
|
||||
|
||||
if (typeof getPublicKey(privkey) === "string") {
|
||||
const pubkey = getPublicKey(privkey);
|
||||
const npub = nip19.npubEncode(pubkey);
|
||||
|
||||
// update
|
||||
account.mutate({
|
||||
npub,
|
||||
pubkey,
|
||||
privkey,
|
||||
follows: null,
|
||||
is_active: 1,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setError("key", {
|
||||
type: "custom",
|
||||
message: "Private Key is invalid, please check again",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-xl font-semibold text-zinc-100">Import your key</h1>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<input
|
||||
{...register("key", { required: true, minLength: 32 })}
|
||||
type={"password"}
|
||||
placeholder="Paste private key here..."
|
||||
className="relative w-full rounded-lg px-3 py-3 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
||||
/>
|
||||
<span className="text-base text-red-400">
|
||||
{errors.key && <p>{errors.key.message}</p>}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isDirty || !isValid}
|
||||
className="inline-flex items-center justify-center h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||
) : (
|
||||
"Continue →"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
81
src/app/auth/import/step-2.tsx
Normal file
81
src/app/auth/import/step-2.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { User } from "@app/auth/components/user";
|
||||
import { updateAccount } from "@libs/storage";
|
||||
import { Button } from "@shared/button";
|
||||
import { LoaderIcon } from "@shared/icons";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useAccount } from "@utils/hooks/useAccount";
|
||||
import { setToArray } from "@utils/transform";
|
||||
import { useContext, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export function ImportStep2Screen() {
|
||||
const ndk = useContext(RelayContext);
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { status, account } = useAccount();
|
||||
|
||||
const update = useMutation({
|
||||
mutationFn: (follows: any) =>
|
||||
updateAccount("follows", follows, account.pubkey),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["currentAccount"] });
|
||||
// redirect to next step
|
||||
navigate("/auth/onboarding", { replace: true });
|
||||
},
|
||||
});
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
// show loading indicator
|
||||
setLoading(true);
|
||||
|
||||
const user = ndk.getUser({ hexpubkey: account.pubkey });
|
||||
const follows = await user.follows();
|
||||
|
||||
// follows as list
|
||||
const followsList = setToArray(follows);
|
||||
|
||||
// update
|
||||
update.mutate(followsList);
|
||||
} catch {
|
||||
console.log("error");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-xl font-semibold">
|
||||
{loading ? "Creating..." : "Continue with"}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900 p-4">
|
||||
{status === "loading" ? (
|
||||
<div className="w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-11 w-11 animate-pulse rounded-lg bg-zinc-800" />
|
||||
<div>
|
||||
<h3 className="mb-1 h-4 w-16 animate-pulse rounded bg-zinc-800" />
|
||||
<p className="h-3 w-36 animate-pulse rounded bg-zinc-800" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3">
|
||||
<User pubkey={account.pubkey} />
|
||||
<Button preset="large" onClick={() => submit()}>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||
) : (
|
||||
"Continue →"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "@shared/icons";
|
||||
import useSWR from "swr";
|
||||
|
||||
const fetcher = async () => {
|
||||
const { platform } = await import("@tauri-apps/api/os");
|
||||
return await platform();
|
||||
};
|
||||
|
||||
export function LayoutOnboarding({ children }: { children: React.ReactNode }) {
|
||||
const { data: platform } = useSWR("platform", fetcher);
|
||||
|
||||
const goBack = () => {
|
||||
window.history.back();
|
||||
};
|
||||
|
||||
const goForward = () => {
|
||||
window.history.forward();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100">
|
||||
<div className="flex h-screen w-full flex-col">
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="relative h-9 shrink-0 border border-zinc-100 bg-white dark:border-zinc-900 dark:bg-black"
|
||||
>
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="flex h-full w-full flex-1 items-center px-2"
|
||||
>
|
||||
<div
|
||||
className={`flex h-full items-center gap-2 ${
|
||||
platform === "darwin" ? "pl-[68px]" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => goBack()}
|
||||
className="group inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-zinc-900"
|
||||
>
|
||||
<ArrowLeftIcon
|
||||
width={16}
|
||||
height={16}
|
||||
className="text-zinc-500 group-hover:text-zinc-300"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => goForward()}
|
||||
className="group inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-zinc-900"
|
||||
>
|
||||
<ArrowRightIcon
|
||||
width={16}
|
||||
height={16}
|
||||
className="text-zinc-500 group-hover:text-zinc-300"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex min-h-0 w-full flex-1">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
97
src/app/auth/onboarding.tsx
Normal file
97
src/app/auth/onboarding.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||
import { ArrowRightCircleIcon } from "@shared/icons/arrowRightCircle";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { User } from "@shared/user";
|
||||
import { dateToUnix } from "@utils/date";
|
||||
import { useAccount } from "@utils/hooks/useAccount";
|
||||
import { useContext } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
export function OnboardingScreen() {
|
||||
const ndk = useContext(RelayContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { status, account } = useAccount();
|
||||
|
||||
const publish = async () => {
|
||||
try {
|
||||
const event = new NDKEvent(ndk);
|
||||
const signer = new NDKPrivateKeySigner(account.privkey);
|
||||
ndk.signer = signer;
|
||||
|
||||
event.content =
|
||||
"Running Lume, fighting for better future, join us here: https://lume.nu";
|
||||
event.created_at = dateToUnix();
|
||||
event.pubkey = account.pubkey;
|
||||
event.kind = 1;
|
||||
event.tags = [];
|
||||
|
||||
// publish event
|
||||
event.publish();
|
||||
|
||||
// redirect to home
|
||||
navigate("/", { replace: true });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="mb-2 text-xl font-semibold text-zinc-100">
|
||||
👋 Hello, welcome you to Lume
|
||||
</h1>
|
||||
<p className="text-sm text-zinc-300">
|
||||
You're a part of better future that we're fighting
|
||||
</p>
|
||||
<p className="text-sm text-zinc-300">
|
||||
If Lume gets your attention, please help us spread via button below
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full border-t border-zinc-800/50 bg-zinc-900 rounded-xl">
|
||||
<div className="h-min w-full px-5 py-3">
|
||||
{status === "success" && (
|
||||
<User
|
||||
pubkey={account.pubkey}
|
||||
time={Math.floor(Date.now() / 1000)}
|
||||
/>
|
||||
)}
|
||||
<div className="-mt-6 pl-[49px] select-text whitespace-pre-line break-words text-base text-zinc-100">
|
||||
<p>Running Lume, fighting for better future</p>
|
||||
<p>
|
||||
join us here:{" "}
|
||||
<a
|
||||
href="https://lume.nu"
|
||||
className="text-fuchsia-500 hover:text-fuchsia-600 no-underline font-normal"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
https://lume.nu
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 w-full flex flex-col gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => publish()}
|
||||
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg px-6 font-medium text-zinc-100 bg-fuchsia-500 hover:bg-fuchsia-600"
|
||||
>
|
||||
<span className="w-5" />
|
||||
<span>Publish</span>
|
||||
<ArrowRightCircleIcon className="w-5 h-5" />
|
||||
</button>
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded-lg px-6 text-sm font-medium text-zinc-200"
|
||||
>
|
||||
Skip for now
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import { Button } from "@shared/button";
|
||||
import { EyeOffIcon, EyeOnIcon } from "@shared/icons";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { generatePrivateKey, getPublicKey, nip19 } from "nostr-tools";
|
||||
import { useMemo, useState } from "react";
|
||||
import { navigate } from "vite-plugin-ssr/client/router";
|
||||
|
||||
export function Page() {
|
||||
const createAccount = useActiveAccount((state: any) => state.create);
|
||||
|
||||
const [type, setType] = useState("password");
|
||||
const privkey = useMemo(() => generatePrivateKey(), []);
|
||||
|
||||
const pubkey = getPublicKey(privkey);
|
||||
const npub = nip19.npubEncode(pubkey);
|
||||
const nsec = nip19.nsecEncode(privkey);
|
||||
|
||||
// toggle private key
|
||||
const showPrivateKey = () => {
|
||||
if (type === "password") {
|
||||
setType("text");
|
||||
} else {
|
||||
setType("password");
|
||||
}
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
createAccount(npub, pubkey, privkey, null, 1);
|
||||
navigate("/app/auth/create/step-2");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-xl font-semibold text-zinc-100">
|
||||
Lume is auto-generated key for you
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-base font-semibold text-zinc-400">
|
||||
Public Key
|
||||
</label>
|
||||
<input
|
||||
readOnly
|
||||
value={npub}
|
||||
className="relative w-full rounded-lg py-3 pl-3.5 pr-11 !outline-none placeholder:text-zinc-400 bg-zinc-800 text-zinc-100"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-base font-semibold text-zinc-400">
|
||||
Private Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
readOnly
|
||||
type={type}
|
||||
value={nsec}
|
||||
className="relative w-full rounded-lg py-3 pl-3.5 pr-11 !outline-none placeholder:text-zinc-400 bg-zinc-800 text-zinc-100"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => showPrivateKey()}
|
||||
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
|
||||
>
|
||||
{type === "password" ? (
|
||||
<EyeOffIcon
|
||||
width={20}
|
||||
height={20}
|
||||
className="text-zinc-500 group-hover:text-zinc-100"
|
||||
/>
|
||||
) : (
|
||||
<EyeOnIcon
|
||||
width={20}
|
||||
height={20}
|
||||
className="text-zinc-500 group-hover:text-zinc-100"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Button preset="large" onClick={() => submit()}>
|
||||
Continue →
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
import { AvatarUploader } from "@shared/avatarUploader";
|
||||
import { LoaderIcon } from "@shared/icons";
|
||||
import { Image } from "@shared/image";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { navigate } from "vite-plugin-ssr/client/router";
|
||||
|
||||
export function Page() {
|
||||
const createTempProfile = useActiveAccount(
|
||||
(state: any) => state.createTempProfile,
|
||||
);
|
||||
|
||||
const [image, setImage] = useState(DEFAULT_AVATAR);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { isDirty, isValid },
|
||||
} = useForm();
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const profile = { ...data, name: data.displayName };
|
||||
createTempProfile(profile);
|
||||
// redirect to step 3
|
||||
navigate("/app/auth/create/step-3", {
|
||||
overwriteLastHistoryEntry: true,
|
||||
});
|
||||
} catch {
|
||||
console.log("error");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setValue("picture", image);
|
||||
}, [setValue, image]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-xl font-semibold text-zinc-100">
|
||||
Create your profile
|
||||
</h1>
|
||||
</div>
|
||||
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900 p-5">
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
<input
|
||||
type={"hidden"}
|
||||
{...register("picture")}
|
||||
value={image}
|
||||
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
||||
Avatar
|
||||
</label>
|
||||
<div className="relative inline-flex h-36 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
|
||||
<Image
|
||||
src={image}
|
||||
fallback={DEFAULT_AVATAR}
|
||||
alt="avatar"
|
||||
className="relative z-10 h-11 w-11 rounded-md"
|
||||
/>
|
||||
<div className="absolute bottom-3 right-3 z-10">
|
||||
<AvatarUploader valueState={setImage} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
||||
Display Name *
|
||||
</label>
|
||||
<input
|
||||
type={"text"}
|
||||
{...register("displayName", {
|
||||
required: true,
|
||||
minLength: 4,
|
||||
})}
|
||||
spellCheck={false}
|
||||
className="relative h-10 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
||||
Bio
|
||||
</label>
|
||||
<textarea
|
||||
{...register("about")}
|
||||
spellCheck={false}
|
||||
className="resize-none relative h-20 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isDirty || !isValid}
|
||||
className="inline-flex items-center justify-center h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||
) : (
|
||||
"Continue →"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||
import { Button } from "@shared/button";
|
||||
import { LoaderIcon } from "@shared/icons";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { Body, fetch } from "@tauri-apps/api/http";
|
||||
import { useContext, useState } from "react";
|
||||
import { navigate } from "vite-plugin-ssr/client/router";
|
||||
|
||||
export function Page() {
|
||||
const ndk = useContext(RelayContext);
|
||||
const [account, tempProfile] = useActiveAccount((state: any) => [
|
||||
state.account,
|
||||
state.tempProfile,
|
||||
]);
|
||||
|
||||
const [username, setUsername] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const createNIP05 = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const response = await fetch("https://lume.nu/api/user-create", {
|
||||
method: "POST",
|
||||
timeout: 30,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
body: Body.json({
|
||||
username: username,
|
||||
pubkey: account.pubkey,
|
||||
lightningAddress: "",
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const profile = { ...tempProfile, nip05: `${username}@lume.nu` };
|
||||
|
||||
const signer = new NDKPrivateKeySigner(account.privkey);
|
||||
ndk.signer = signer;
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
// build event
|
||||
event.content = JSON.stringify(profile);
|
||||
event.kind = 0;
|
||||
event.pubkey = account.pubkey;
|
||||
event.tags = [];
|
||||
// publish event
|
||||
event.publish();
|
||||
|
||||
// redirect to step 4
|
||||
navigate("/app/auth/create/step-4", {
|
||||
overwriteLastHistoryEntry: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
console.error("Error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-xl font-semibold text-zinc-100">
|
||||
Create your Lume ID
|
||||
</h1>
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-center items-center gap-4">
|
||||
<div className="w-full inline-flex items-center justify-center gap-2 rounded-lg bg-zinc-800">
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
autoCapitalize="false"
|
||||
autoCorrect="none"
|
||||
spellCheck="false"
|
||||
placeholder="satoshi"
|
||||
className="relative w-full py-3 pl-3.5 !outline-none placeholder:text-zinc-500 bg-transparent text-zinc-100"
|
||||
/>
|
||||
<span className="text-fuchsia-500 font-semibold pr-3.5">
|
||||
@lume.nu
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
preset="large"
|
||||
onClick={() => createNIP05()}
|
||||
disabled={username.length === 0}
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||
) : (
|
||||
"Continue →"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import { LoaderIcon } from "@shared/icons";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { getPublicKey, nip19 } from "nostr-tools";
|
||||
import { Resolver, useForm } from "react-hook-form";
|
||||
import { navigate } from "vite-plugin-ssr/client/router";
|
||||
|
||||
type FormValues = {
|
||||
key: string;
|
||||
};
|
||||
|
||||
const resolver: Resolver<FormValues> = async (values) => {
|
||||
return {
|
||||
values: values.key ? values : {},
|
||||
errors: !values.key
|
||||
? {
|
||||
key: {
|
||||
type: "required",
|
||||
message: "This is required.",
|
||||
},
|
||||
}
|
||||
: {},
|
||||
};
|
||||
};
|
||||
|
||||
export function Page() {
|
||||
const createAccount = useActiveAccount((state: any) => state.create);
|
||||
const {
|
||||
register,
|
||||
setError,
|
||||
handleSubmit,
|
||||
formState: { errors, isDirty, isValid, isSubmitting },
|
||||
} = useForm<FormValues>({ resolver });
|
||||
|
||||
const onSubmit = async (data: any) => {
|
||||
try {
|
||||
let privkey = data["key"];
|
||||
|
||||
if (privkey.substring(0, 4) === "nsec") {
|
||||
privkey = nip19.decode(privkey).data;
|
||||
}
|
||||
|
||||
if (typeof getPublicKey(privkey) === "string") {
|
||||
const pubkey = getPublicKey(privkey);
|
||||
const npub = nip19.npubEncode(pubkey);
|
||||
|
||||
createAccount(npub, pubkey, privkey, null, 1);
|
||||
navigate("/app/auth/import/step-2");
|
||||
}
|
||||
} catch (error) {
|
||||
setError("key", {
|
||||
type: "custom",
|
||||
message: "Private Key is invalid, please check again",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-xl font-semibold text-zinc-100">
|
||||
Import your key
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-3"
|
||||
>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<input
|
||||
{...register("key", { required: true, minLength: 32 })}
|
||||
type={"password"}
|
||||
placeholder="Paste private key here..."
|
||||
className="relative w-full rounded-lg px-3 py-3 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
||||
/>
|
||||
<span className="text-base text-red-400">
|
||||
{errors.key && <p>{errors.key.message}</p>}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isDirty || !isValid}
|
||||
className="inline-flex items-center justify-center h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||
) : (
|
||||
"Continue →"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { User } from "@app/auth/components/user";
|
||||
import { Button } from "@shared/button";
|
||||
import { LoaderIcon } from "@shared/icons";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { useActiveAccount } from "@stores/accounts";
|
||||
import { setToArray } from "@utils/transform";
|
||||
import { useContext, useState } from "react";
|
||||
import { navigate } from "vite-plugin-ssr/client/router";
|
||||
|
||||
export function Page() {
|
||||
const ndk = useContext(RelayContext);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [account, updateFollows] = useActiveAccount((state: any) => [
|
||||
state.account,
|
||||
state.updateFollows,
|
||||
]);
|
||||
|
||||
const submit = async () => {
|
||||
// show loading indicator
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const user = ndk.getUser({ hexpubkey: account.pubkey });
|
||||
const follows = await user.follows();
|
||||
|
||||
// follows as list
|
||||
const followsList = setToArray(follows);
|
||||
|
||||
// update account follows in store
|
||||
updateFollows(followsList);
|
||||
|
||||
// redirect to onboarding
|
||||
setTimeout(
|
||||
() => navigate("/app/onboarding", { overwriteLastHistoryEntry: true }),
|
||||
2000,
|
||||
);
|
||||
} catch {
|
||||
console.log("error");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="text-xl font-semibold">
|
||||
{loading ? "Creating..." : "Continue with"}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="w-full rounded-xl border-t border-zinc-800/50 bg-zinc-900 p-4">
|
||||
{!account ? (
|
||||
<div className="w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-11 w-11 animate-pulse rounded-lg bg-zinc-800" />
|
||||
<div>
|
||||
<h3 className="mb-1 h-4 w-16 animate-pulse rounded bg-zinc-800" />
|
||||
<p className="h-3 w-36 animate-pulse rounded bg-zinc-800" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3">
|
||||
<User pubkey={account.pubkey} />
|
||||
<Button preset="large" onClick={() => submit()}>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||
) : (
|
||||
"Continue →"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ArrowRightCircleIcon } from "@shared/icons/arrowRightCircle";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export function Page() {
|
||||
export function WelcomeScreen() {
|
||||
return (
|
||||
<div className="w-full h-full grid grid-cols-12 gap-4 px-4 py-4">
|
||||
<div className="col-span-5 border-t border-zinc-800/50 bg-zinc-900 rounded-xl flex flex-col">
|
||||
@@ -19,20 +20,20 @@ export function Page() {
|
||||
</h3>
|
||||
</div>
|
||||
<div className="mt-auto w-full flex flex-col gap-2 px-4 py-4">
|
||||
<a
|
||||
href="/app/auth/import"
|
||||
<Link
|
||||
to="/auth/import"
|
||||
className="inline-flex h-12 w-full items-center justify-between gap-2 rounded-lg px-6 font-medium text-zinc-100 bg-fuchsia-500 hover:bg-fuchsia-600"
|
||||
>
|
||||
<span className="w-5" />
|
||||
<span>Login with private key</span>
|
||||
<ArrowRightCircleIcon className="w-5 h-5" />
|
||||
</a>
|
||||
<a
|
||||
href="/app/auth/create"
|
||||
</Link>
|
||||
<Link
|
||||
to="/auth/create"
|
||||
className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-lg px-6 font-medium text-zinc-200 bg-zinc-800 hover:bg-zinc-700"
|
||||
>
|
||||
Create new key
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
Reference in New Issue
Block a user