feat: add interest screen to onboarding

This commit is contained in:
reya 2024-01-19 14:53:26 +07:00
parent f65175f11e
commit a3460418f6
9 changed files with 372 additions and 161 deletions

View File

@ -1,7 +1,6 @@
import { useArk } from "@lume/ark";
import { CheckIcon, ChevronDownIcon, LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { onboardingAtom } from "@lume/utils";
import NDK, {
NDKEvent,
NDKKind,
@ -9,11 +8,11 @@ import NDK, {
NDKPrivateKeySigner,
} from "@nostr-dev-kit/ndk";
import * as Select from "@radix-ui/react-select";
import { UnlistenFn } from "@tauri-apps/api/event";
import { desktopDir } from "@tauri-apps/api/path";
import { Window } from "@tauri-apps/api/window";
import { save } from "@tauri-apps/plugin-dialog";
import { writeTextFile } from "@tauri-apps/plugin-fs";
import { useSetAtom } from "jotai";
import { nanoid } from "nanoid";
import { getPublicKey, nip19 } from "nostr-tools";
import { useState } from "react";
@ -42,7 +41,6 @@ export function CreateAccountScreen() {
const storage = useStorage();
const services = useLoaderData() as NDKEvent[];
const navigate = useNavigate();
const setOnboarding = useSetAtom(onboardingAtom);
const [serviceId, setServiceId] = useState(services?.[0]?.id);
const [loading, setIsLoading] = useState(false);
@ -87,8 +85,6 @@ export function CreateAccountScreen() {
privkey: signer.privateKey,
});
setOnboarding(true);
return navigate("/auth/onboarding", { replace: true });
};
@ -117,33 +113,47 @@ export function CreateAccountScreen() {
);
// handle auth url request
let unlisten: UnlistenFn;
let authWindow: Window;
remoteSigner.addListener("authUrl", (authUrl: string) => {
let account: string = undefined;
remoteSigner.addListener("authUrl", async (authUrl: string) => {
authWindow = new Window(`auth-${serviceId}`, {
url: authUrl,
title: domain,
titleBarStyle: "overlay",
width: 415,
height: 600,
width: 600,
height: 650,
center: true,
closable: false,
});
unlisten = await authWindow.onCloseRequested(() => {
if (!account) {
setIsLoading(false);
unlisten();
return authWindow.close();
}
});
});
// create new account
const account = await remoteSigner.createAccount(
account = await remoteSigner.createAccount(
data.username,
domain,
data.email,
);
if (!account) {
authWindow.close();
unlisten();
setIsLoading(false);
authWindow.close();
return toast.error("Failed to create new account, try again later");
}
unlisten();
authWindow.close();
// add account to storage
@ -165,7 +175,6 @@ export function CreateAccountScreen() {
// await ark.createEvent({ kind: NDKKind.Metadata, content: "", tags: [] });
await ark.createEvent({ kind: NDKKind.Contacts, content: "", tags: [] });
setOnboarding(true);
setIsLoading(false);
return navigate("/auth/onboarding", { replace: true });
@ -213,7 +222,10 @@ export function CreateAccountScreen() {
minLength: 1,
})}
spellCheck={false}
placeholder="satoshi"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder="lume"
className="flex-1 min-w-0 text-xl bg-transparent border-transparent outline-none focus:outline-none focus:ring-0 focus:border-none h-14 ring-0 placeholder:text-neutral-600"
/>
<Select.Root value={serviceId} onValueChange={setServiceId}>

View File

@ -22,7 +22,10 @@ export function Navigation() {
useHotkeys("meta+n", () => setIsEditorOpen((state) => !state), []);
return (
<div className="flex flex-col justify-between w-20 h-full px-4 py-3 shrink-0">
<div
data-tauri-drag-region
className="flex flex-col justify-between w-20 h-full px-4 py-3 shrink-0"
>
<div className="flex flex-col flex-1">
<div className="flex flex-col gap-3">
<ActiveAccount />

View File

@ -27,7 +27,7 @@ export function OnboardingHomeScreen() {
<div className="mt-4 flex flex-col gap-2 items-center">
<button
type="button"
onClick={() => navigate("/profile-settings")}
onClick={() => navigate("/profile")}
className="inline-flex items-center justify-center gap-2 w-44 font-medium h-11 rounded-xl bg-blue-100 text-blue-500 hover:bg-blue-200 dark:bg-blue-900 dark:text-blue-500 dark:hover:bg-blue-800"
>
Profile Settings

View File

@ -0,0 +1,123 @@
import { ArrowLeftIcon, LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { TOPICS, cn } from "@lume/utils";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
export function OnboardingInterestScreen() {
const storage = useStorage();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [hashtags, setHashtags] = useState([]);
const toggleHashtag = (item: string) => {
const arr = hashtags.includes(item)
? hashtags.filter((i) => i !== item)
: [...hashtags, item];
setHashtags(arr);
};
const toggleAll = (item: string[]) => {
const sets = new Set([...hashtags, ...item]);
setHashtags([...sets]);
};
const submit = async () => {
try {
setLoading(true);
if (!hashtags.length) return navigate("/finish");
const save = await storage.createSetting(
"interests",
JSON.stringify({ hashtags }),
);
setLoading(false);
if (save) return navigate("/finish");
} catch (e) {
setLoading(false);
toast.error(String(e));
}
};
return (
<div className="w-full h-full flex flex-col">
<div className="h-16 shrink-0 px-8 border-b border-neutral-100 dark:border-neutral-900 flex w-full items-center justify-between">
<div className="flex flex-col">
<h3 className="font-semibold">Interests</h3>
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
Pick things you'd like to see in your home feed.
</p>
</div>
</div>
<div className="w-full flex-1 min-h-0 flex flex-col justify-between">
<div className="flex-1 min-h-0 overflow-y-auto px-8 py-8">
<div className="flex flex-col gap-8">
{TOPICS.map((topic, index) => (
<div key={topic.title + index} className="flex flex-col gap-4">
<div className="w-full flex items-center justify-between">
<div className="inline-flex items-center gap-2.5">
<img
src={topic.icon}
alt={topic.title}
className="size-8 object-cover rounded-lg"
/>
<h3 className="text-lg font-semibold">{topic.title}</h3>
</div>
<button
type="button"
onClick={() => toggleAll(topic.content)}
className="text-sm font-medium text-blue-500"
>
Follow All
</button>
</div>
<div className="flex flex-wrap items-center gap-3">
{topic.content.map((hashtag) => (
<button
type="button"
onClick={() => toggleHashtag(hashtag)}
className={cn(
"inline-flex items-center rounded-full bg-neutral-100 dark:bg-neutral-900 border border-transparent px-2 py-1 text-sm font-medium",
hashtags.includes(hashtag)
? "border-blue-500 text-blue-500"
: "",
)}
>
{hashtag}
</button>
))}
</div>
</div>
))}
</div>
</div>
<div className="h-16 shrink-0 w-full flex items-center px-8 justify-center gap-2 border-t border-neutral-100 dark:border-neutral-900 bg-neutral-50 dark:bg-neutral-950">
<button
type="button"
onClick={() => navigate(-1)}
className="inline-flex h-9 flex-1 gap-2 shrink-0 items-center justify-center rounded-lg bg-neutral-100 font-medium dark:bg-neutral-900 dark:hover:bg-neutral-800 hover:bg-blue-200"
>
<ArrowLeftIcon className="size-4" />
Back
</button>
<button
type="button"
onClick={submit}
className="inline-flex h-9 flex-1 shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="size-4 animate-spin" />
) : (
"Continue"
)}
</button>
</div>
</div>
</div>
);
}

View File

@ -11,7 +11,11 @@ export function OnboardingModal() {
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/10 backdrop-blur-sm dark:bg-white/10" />
<Dialog.Content className="fixed inset-0 z-50 flex items-center justify-center min-h-full">
<div className="relative w-full max-w-lg bg-white h-[500px] rounded-xl dark:bg-black overflow-hidden">
<div
data-tauri-drag-region
className="h-20 absolute top-0 left-0 w-full"
/>
<div className="relative w-full max-w-xl xl:max-w-2xl bg-white h-[600px] xl:h-[700px] rounded-xl dark:bg-black overflow-hidden">
<OnboardingRouter />
</div>
</Dialog.Content>

View File

@ -11,7 +11,7 @@ import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
import { AvatarUploadButton } from "../avatarUploadButton";
export function OnboardingProfileSettingsScreen() {
export function OnboardingProfileScreen() {
const [picture, setPicture] = useState("");
const [loading, setLoading] = useState(false);
@ -32,7 +32,7 @@ export function OnboardingProfileSettingsScreen() {
if (!data.name.length && !data.about.length) {
setLoading(false);
navigate("/follow");
navigate("/interests");
}
const oldProfile = await ark.getUserProfile();
@ -41,7 +41,6 @@ export function OnboardingProfileSettingsScreen() {
...data,
lud16: "", // temporary remove lud16
nip05: oldProfile?.nip05 || "",
display_name: data.name,
bio: data.about,
image: picture,
picture: picture,
@ -62,7 +61,7 @@ export function OnboardingProfileSettingsScreen() {
);
setLoading(false);
navigate("/follow");
navigate("/interests");
}
} catch (e) {
setLoading(false);
@ -72,8 +71,13 @@ export function OnboardingProfileSettingsScreen() {
return (
<div className="w-full h-full flex flex-col gap-4">
<div className="h-12 shrink-0 px-8 border-b border-neutral-100 dark:border-neutral-900 flex font-medium text-neutral-700 dark:text-neutral-600 w-full items-center">
Profile Settings
<div className="h-16 shrink-0 px-8 border-b border-neutral-100 dark:border-neutral-900 flex w-full items-center justify-between">
<div className="flex flex-col">
<h3 className="font-semibold">About you</h3>
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
Tell Lume about yourself to start building your home feed.
</p>
</div>
</div>
<form
onSubmit={handleSubmit(onSubmit)}
@ -111,9 +115,10 @@ export function OnboardingProfileSettingsScreen() {
</label>
<input
type={"text"}
{...register("name")}
{...register("name", { required: true, minLength: 1 })}
placeholder="e.g. Alice"
spellCheck={false}
className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/>
</div>
<div className="flex flex-col gap-1">
@ -122,8 +127,21 @@ export function OnboardingProfileSettingsScreen() {
</label>
<textarea
{...register("about")}
placeholder="e.g. Artist, anime-lover, and k-pop fan"
spellCheck={false}
className="relative h-24 w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-2 !outline-none placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
className="relative h-24 w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-2 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/>
</div>
<div className="flex flex-col gap-1">
<label htmlFor="website" className="font-medium">
Website
</label>
<input
type="url"
{...register("website")}
placeholder="e.g. https://alice.me"
spellCheck={false}
className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/>
</div>
</motion.div>

View File

@ -6,9 +6,9 @@ import {
UNSAFE_LocationContext,
} from "react-router-dom";
import { OnboardingFinishScreen } from "./finish";
import { OnboardingFollowScreen } from "./follow";
import { OnboardingHomeScreen } from "./home";
import { OnboardingProfileSettingsScreen } from "./profileSettings";
import { OnboardingInterestScreen } from "./interest";
import { OnboardingProfileScreen } from "./profile";
export function OnboardingRouter() {
return (
@ -17,11 +17,8 @@ export function OnboardingRouter() {
<AnimatePresence>
<Routes>
<Route path="/" element={<OnboardingHomeScreen />} />
<Route
path="/profile-settings"
element={<OnboardingProfileSettingsScreen />}
/>
<Route path="/follow" element={<OnboardingFollowScreen />} />
<Route path="/profile" element={<OnboardingProfileScreen />} />
<Route path="/interests" element={<OnboardingInterestScreen />} />
<Route path="/finish" element={<OnboardingFinishScreen />} />
</Routes>
</AnimatePresence>

View File

@ -86,50 +86,95 @@ export const COL_TYPES = {
topic: 6,
trendingNotes: 9000,
trendingAccounts: 9001,
foryou: 9998,
newsfeed: 9999,
};
export const TOPICS = [
{
icon: "/anime.jpg",
title: "Anime & Manga",
content: [
"#animestr",
"#anime",
"#manga",
"#otaku",
"#frieren",
"#fate",
"#aot",
"#AttackOnTitan",
"#JujutsuKaisen",
"#OnePiece",
"#KimetsuNoYaiba",
"#Overlord",
"#Evangelion",
"#DemonSlayer",
"#JoJo",
"#SPYxFAMILY",
"#MatoSeiheinoSlave",
"#ghibli",
"#ChainsawMan",
"#Gintama",
"#animeart",
"#animegirl",
"#cosplay",
"#weeb",
"#animeworld",
"#fanart",
"#vocaloid",
"#vtuber",
"#hololive",
"#hololivemeet",
"#pixiv",
"#waifu",
],
},
{
icon: "/gaming.jpg",
title: "Gaming",
content: [
"#gamestr",
"#gaming",
"#gamer",
"#ps",
"#playstation",
"#videogames",
"#game",
"#xbox",
"#games",
"#twitch",
"#fortnite",
"#pc",
"#pcgaming",
"#gamers",
"#gamingcommunity",
"#switch",
"#gamergirl",
"#nintendo",
"#gta",
"#callofduty",
"#pubg",
"#videogame",
"#esports",
"#genshinimpact",
"#honkaiimpact",
"#warthunder",
"#hoyoverse",
"#arknights",
"#soullike",
"#eldenring",
"#GenshinImpact",
"#HonkaiStarRail",
"#HonkaiImpact",
"#steam",
"#pubg",
"#cs2",
"#Cyberpunk",
"#Skyrim",
"#GTA",
"#GTA6",
"#CallofDuty",
"#Pokemon",
"#apexlegends",
"#baldurgate3",
"#baldurgate",
"#starfield",
"#gta6",
"#thefinals",
"#palworld",
"#famitsu",
"#jrpg",
"#ffxiv",
"#gacha",
"#warthunder",
"#hoyoverse",
"#arknights",
"#soulslike",
"#eldenring",
"#persona",
"#playstation",
"#steamdeck",
"#xbox",
"#xbot",
"#consolewars",
"#game",
"#games",
"#twitch",
"#fortnite",
"#pcgaming",
"#nintendo",
"#switch",
"#pubg",
"#esports",
"#gameoftheyear",
"#darksoul",
"#batterfield",
@ -144,18 +189,35 @@ export const TOPICS = [
],
},
{
title: "Music",
icon: "/music.jpg",
title: "Music & Entertainment",
content: [
"#audiostr",
"#musicstr",
"#music",
"#love",
"#hiphop",
"#movie",
"#BLACKPINK",
"#Lisa",
"#Jennie",
"#Taylor",
"#BTC",
"#Twice",
"#TikTok",
"#KPOP",
"#Marvel",
"#DC",
"#Woke",
"#fan",
"#StarWars",
"#Podcast",
"#JoeRogan",
"#Ariana",
"#SONTUNGMTP",
"#rap",
"#metal",
"#vinyl",
"#art",
"#musician",
"#artist",
"#musica",
"#singer",
"#dj",
"#rock",
@ -164,77 +226,28 @@ export const TOPICS = [
"#song",
"#newmusic",
"#producer",
"#life",
"#rapper",
"#party",
"#fashion",
"#explorepage",
"#viral",
"#beats",
"#dvd",
"#amass",
"#bluray",
"#Blu_Ray",
"#taylor",
],
},
{
title: "Photography",
content: [
"#photography",
"#photooftheday",
"#love",
"#photo",
"#nature",
"#picoftheday",
"#photographer",
"#beautiful",
"#fashion",
"#travel",
"#photoshoot",
"#naturephotography",
"#model",
"#me",
"#smile",
"#style",
"#happy",
"#likes",
"#myself",
],
},
{
title: "Art",
content: [
"#nostrdesign",
"#artstr",
"#art",
"#artist",
"#drawing",
"#artwork",
"#painting",
"#fashion",
"#beautiful",
"#illustration",
"#digitalart",
"#design",
"#nature",
"#photo",
"#sketch",
"#style",
"#arte",
"#happy",
"#cute",
"#draw",
"#artoftheday",
],
},
{
title: "Movie",
icon: "/movie.jpg",
title: "Television",
content: [
"#filmstr",
"#moviestr",
"#movies",
"#movie",
"#HBO",
"#BreakingBad",
"#Wednesday",
"#Disney+",
"#film",
"#cinema",
"#films",
@ -249,24 +262,28 @@ export const TOPICS = [
"#horror",
"#bollywood",
"#movienight",
"#photography",
"#comedy",
"#cinephile",
"#cine",
"#tv",
"#director",
"#horrormovies",
"#drama",
"#filmmaker",
"#kdrama",
],
},
{
icon: "/technology.jpg",
title: "Technology",
content: [
"#apple",
"#xiaomi",
"#huawei",
"#Apple",
"#Tesla",
"#AMD",
"#Intel",
"#Xiaomi",
"#Huawei",
"#OpenAI",
"#BigTech",
"#ai",
"#IOS",
"#Android",
"#oppo",
"#nostr",
"#technology",
@ -278,8 +295,6 @@ export const TOPICS = [
"#technews",
"#science",
"#gadgets",
"#electronics",
"#android",
"#software",
"#programming",
"#smartphone",
@ -289,51 +304,90 @@ export const TOPICS = [
"#security",
"#gadget",
"#mobile",
"#technologynews",
"#opensource",
"#tor",
"#bitcoin",
"#lightning",
],
},
{
title: "Anime",
icon: "/photography.jpg",
title: "Photography",
content: [
"#animestr",
"#anime",
"#manga",
"#otaku",
"#animeart",
"#animegirl",
"#cosplay",
"#weeb",
"#onepiece",
"#demonslayer",
"#animeworld",
"#aot",
"#fanart",
"#vocaloid",
"#vtuber",
"#fate",
"#hololive",
"#hololivemeet",
"#pixiv",
"#waifu",
"#photostr",
"#NewProfilePic",
"#photography",
"#photooftheday",
"#foot",
"#love",
"#photo",
"#nature",
"#picoftheday",
"#photographer",
"#beautiful",
"#fashion",
"#travel",
"#photoshoot",
"#nature",
"#naturephotography",
"#smile",
"#style",
"#happy",
"#likes",
"#myself",
],
},
{
icon: "/art.jpg",
title: "Art & Design",
content: [
"#nostrdesign",
"#artstr",
"#art",
"#design",
"#ui",
"#ux",
"#MidJourney",
"#Dall-E",
"#aiart",
"#retro",
"#webdesign",
"#artist",
"#pixelart",
"#pixel",
"#3D",
"#drawing",
"#artwork",
"#painting",
"#fashion",
"#beautiful",
"#illustration",
"#digitalart",
"#nature",
"#photo",
"#sketch",
"#style",
"#draw",
"#artoftheday",
],
},
{
icon: "/nsfw.jpg",
title: "NSFW",
content: [
"#pornstr",
"#porn",
"#Lesbian",
"#ntr",
"#yuri",
"#BigCock",
"#Anal",
"#BDSM",
"#pornhub",
"#nsfw",
"#bdsm",
"#lewd",
"#kink",
"#nude",
"#sexy",
"#loli",
"#hentai",
"#ntr",
"#69",
],
},
];

View File

@ -11,7 +11,7 @@ export const editorValueAtom = atom([
]);
// Onboarding
export const onboardingAtom = atom(false);
export const onboardingAtom = atom(true);
// Activity
export const activityAtom = atom(false);