Create partial sign up flow

remove cypress tests
This commit is contained in:
hzrd149 2023-10-14 12:12:33 -05:00
parent d2f307642a
commit 3a98501c93
32 changed files with 348 additions and 1512 deletions

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
dist
node_modules
cypress/videos
stats.html

View File

@ -1,12 +0,0 @@
import { defineConfig } from "cypress";
export default defineConfig({
viewportWidth: 1200,
viewportHeight: 800,
e2e: {
baseUrl: "http://localhost:5173",
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
});

View File

@ -1,127 +0,0 @@
describe("Embeds", () => {
describe("hashtags", () => {
it('should handle uppercase hashtags and ","', () => {
cy.visit(
"#/n/nevent1qqsrj5ns6wva3fcghlyx0hp7hhajqtqk3kuckp7xhhscrm4jl7futegpz9mhxue69uhkummnw3e82efwvdhk6qgswaehxw309ahx7um5wgh8w6twv5pkpt8l",
);
cy.findByRole("link", { name: "#Japan" }).should("be.visible");
cy.findByRole("link", { name: "#kyudo" }).should("be.visible");
cy.findByRole("link", { name: "#Shiseikan" }).should("be.visible");
cy.findByRole("link", { name: "#Nostrasia" }).should("be.visible");
});
});
describe("links", () => {
it("embed trustless.computer links", () => {
cy.visit(
"#/n/nevent1qqsfn2mv3pe2v7jak4r5wnyengt36t0rx26w04hgysrmtpml8jnlk5cprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2qgawaehxw309ahx7um5wgkhqatz9emk2mrvdaexgetj9ehx2aq2wry06",
);
cy.get('[href="https://trustless.computer/"]').should("be.visible");
cy.get(
'[href="https://mempool.space/tx/461c6f56015c94d74837b68c9d08f4b80e7db7ca1e5ac4c53d9aa8c76b667672"]',
).should("be.visible");
});
it("embeds links", () => {
cy.visit(
"#/n/nevent1qqsvg6kt4hl79qpp5p673g7ref6r0c5jvp4yys7mmvs4m50t30sy9dgpz9mhxue69uhkummnw3e82efwvdhk6qgjwaehxw309aex2mrp0yhxvdm69e5k7r3xlpe",
);
cy.get('[href="https://getalby.com/"]').should("exist");
cy.get('[href="https://lightningaddress.com/"]').should("exist");
cy.get('[href="https://snort.social/"]').should("exist");
cy.get('[href="http://damus.io/"]').should("exist");
cy.get('[href="https://vida.live/"]').should("exist");
});
it("embeds simplex.chat links", () => {
cy.visit(
"#/n/nevent1qqsymds0vlpp4f5s0dckjf4qz283pdsen0rmx8lu7ct6hpnxag2hpacpremhxue69uhkummnw3ez6un9d3shjtnwda4k7arpwfhjucm0d5q3qamnwvaz7tmwdaehgu3wwa5kueghxyq76",
);
cy.get(
'[href="https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2F0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU%3D%40smp8.simplex.im%2FVlHiRmia02CDgga7w-uNb2FQZTZsj3UR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAd2GEWU9Zjrljhw8O4FldcxrqehkDWezXl-cWD-VkeEw%253D%26srv%3Dbeccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion"]',
).should("be.visible");
});
});
describe("Nostr links", () => {
it("should embed noub1...", () => {
cy.visit(
"#/n/nevent1qqsd5yw7sntqfc4e7u4aempvgctry2plz653t9gpf97ctk5vc0ftskgpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3zamnwvaz7tmwdaehgun4v5hxxmmdfxdj3a",
);
cy.contains("Alby team");
cy.get(".chakra-card")
.first()
.within(() => {
cy.get('[href="#/u/npub13sajvl5ak6cpz4ycesl0e5v869r5sey5pt50l9mcy6uas0fqtpmscth4np"]').should("be.visible");
cy.get('[href="#/u/npub167n5w6cj2wseqtmk26zllc7n28uv9c4vw28k2kht206vnghe5a7stgzu3r"]').should("be.visible");
// make sure the leading @ is removed
cy.get(".chakra-card__body").should("not.contain.text", "@@");
});
});
});
describe("youtube", () => {
it("should embed playlists", () => {
cy.visit(
"#/n/nevent1qqs8w6e63smpr5ccmz4l0w5pvnkp6r7z2fxaadjwu2g74y95pl9xv0cpzpmhxue69uhkummnw3ezuamfdejszrthwden5te0dehhxtnvdakqqkgf54",
);
cy.findByTitle(/youtube video player/i).should("be.visible");
cy.findByTitle(/youtube video player/i).should("have.attr", "src");
});
});
describe("Music", () => {
it("should handle wavlake links", () => {
cy.visit(
"#/n/nevent1qqsve4ud5v8gjds2f2h7exlmjvhqayu4s520pge7frpwe22wezny0pcpp4mhxue69uhkummn9ekx7mqprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2mxs3z0",
);
cy.findByTitle("Wavlake Embed").should("be.visible");
});
it("should handle spotify links", () => {
cy.visit(
"#/n/nevent1qqsx0lz7m72qzq499exwhnfszvgwea8tv38x9wkv32yhkmwwmhgs7jgprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk25m3sln",
);
cy.findByTitle("Spotify List Embed").should("exist");
cy.visit(
"#/n/nevent1qqsqxkmz49hydf8ppa9k6x6zrcq7m4evhhlye0j3lcnz8hrl2q6np4spz3mhxue69uhhyetvv9ujuerpd46hxtnfdult02qz",
);
cy.findByTitle("Spotify Embed").should("exist");
});
it("should handle apple music links", () => {
cy.visit(
"#/n/nevent1qqs9kqt9d7r4zjpawcyl82x5qsn4hals4wn294dv95knrahs4mggwasprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2whhzvz",
);
cy.findByTitle("Apple Music Embed").should("exist");
cy.visit(
"#/n/nevent1qqszyrz4uug75j4086kj4f8peg3g0v8g9f04zjxplnpq0uxljtthggqprdmhxue69uhkvet9v3ejumn0wd68ytnzv9hxgtmdv4kk2aeexmq",
);
cy.findByTitle("Apple Music List Embed").should("exist");
});
it("should handle Tidal playlist links", () => {
cy.visit("#/n/nevent1qqsg4d6rvg3te0y7sa0xp8r2rgcrnqyp2jmddzm4ufnmqs36aa2247qpp4mhxue69uhkummn9ekx7mqacwd3t");
cy.findByTitle("Tidal List Embed").should("be.visible");
});
});
describe("Emoji", () => {
it("should embed emojis", () => {
cy.visit(
"#/n/nevent1qqsdj7k47uh4z0ypl2m29lvd4ar9zpf6dcy7ls0q6g6qctnxfj5n3pcpzpmhxue69uhkummnw3ezuamfdejszrthwden5te0dehhxtnvdakqdyqlpq",
);
cy.findByRole("img", { name: /pepeD/i }).should("be.visible");
});
});
});

View File

@ -1,68 +0,0 @@
describe("Login view", () => {
beforeEach(() => {
cy.visit("#/login");
cy.window().then(($win) => {
cy.stub($win, "prompt").returns("pass");
});
});
it("login with nip05", () => {
cy.intercept("get", "https://hzrd149.com/.well-known/nostr.json?name=_", {
names: {
_: "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5",
},
relays: {
"266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5": ["wss://nostrue.com"],
},
});
cy.findByRole("link", { name: /nip-05/i }).click();
cy.findByRole("textbox", { name: /nip-05/i }).type("_@hzrd149.com");
cy.contains(/found 1 relays/i);
cy.findByRole("button", { name: /login/i }).click();
cy.findByRole("button", { name: "Home" }).should("be.visible");
});
it("login with npub", () => {
cy.findByRole("link", { name: /npub/i }).click();
cy.findByRole("textbox", { name: /npub/i }).type("npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr");
cy.findByRole("combobox", { name: /bootstrap relay/i })
.clear()
.type("wss://nostrue.com");
cy.findByRole("button", { name: /login/i }).click();
cy.findByRole("button", { name: "Home" }).should("be.visible");
});
it("login with new nsec", () => {
cy.findByRole("link", { name: /nsec/i }).click();
cy.findByRole("button", { name: /generate/i }).click();
cy.findByRole("combobox", { name: /bootstrap relay/i })
.clear()
.type("wss://nostrue.com");
cy.findByRole("button", { name: /login/i }).click();
cy.findByRole("button", { name: "Home" }).should("be.visible");
});
it("should redirect after login", () => {
cy.visit(
"#/n/nevent1qqs88gdxv36qsjfwr66k7wxuq9r2tg8rsdcnfkcqdg4sc6vlnsma98qpzpmhxue69uhkummnw3ezuamfdejsz9rhwden5te0wfjkccte9ejxzmt4wvhxjmccew89d",
);
cy.findByRole("link", { name: /login/i }).click();
cy.findByRole("link", { name: /nsec/i }).click();
cy.findByRole("button", { name: /generate/i }).click();
cy.findByRole("combobox", { name: /bootstrap relay/i })
.clear()
.type("wss://nostrue.com");
cy.findByRole("button", { name: /login/i }).click();
// should be redirect to note
cy.contains(/GM, and happy bday to your son/i);
});
});

View File

@ -1,16 +0,0 @@
describe("Profile view", () => {
it("should load a rss feed profile", () => {
cy.visit(
"#/u/nprofile1qqsp6hxqjatvxtesgszs8aee0fcjccxa3ef3mzjva4uv2yr5lucp6jcpzemhxue69uhhyumnd3shjtnwdaehgu3wd4hk2s8c5un",
);
cy.contains("fjsmu");
cy.contains("https://rsshub.app/pixiv/user/7569500@rsslay.nostr.moe");
});
it("should load PABLOF7z", () => {
cy.visit("#/u/npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft");
cy.contains("npub1l2vyh...3afqutajft");
});
});

View File

@ -1,22 +0,0 @@
describe("No account", () => {
describe("note view", () => {
it("should fetch and render note", () => {
cy.visit(
"#/n/nevent1qqs84hwdlls703w4yf66qsszxjqfc0xselfxrzr6n4qp40vzdnczragpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5jcwczn",
);
cy.get(".chakra-card")
.first()
.within(() => {
// check for note content
cy.contains('I didn\'t know someone had taken the "rsslay" idea and made it good');
// check for author name
cy.get(".chakra-card__header .chakra-heading .chakra-link").should("not.contain", "npub");
});
// check for multiple replies
cy.get(".chakra-card").should("have.length.above", 2);
});
});
});

View File

@ -1,69 +0,0 @@
describe("Search", () => {
describe("Events", () => {
const links: [string, RegExp][] = [
[
"nostr:nevent1qqsvg6kt4hl79qpp5p673g7ref6r0c5jvp4yys7mmvs4m50t30sy9dgpp4mhxue69uhkummn9ekx7mqpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet59dl66z",
/Nostr zaps - a guide/i,
],
["nostr:note10twumllpulza2gn45ppqydyqns7dpn7jvxy8482qr27cym8sy86sgxe3c8", /someone had taken/i],
];
for (const [link, regexp] of links) {
it(`should handle ${link}`, () => {
cy.visit("#/search");
cy.findByRole("searchbox").type(link, { delay: 0 }).type("{enter}");
cy.contains(regexp).should("be.visible");
});
}
for (const [link, regexp] of links) {
const withoutPrefix = link.replace("nostr:", "");
it(`should handle ${withoutPrefix}`, () => {
cy.visit("#/search");
cy.findByRole("searchbox").type(link, { delay: 0 }).type("{enter}");
cy.contains(regexp).should("be.visible");
});
}
});
describe("Profiles", () => {
const profiles: [string, RegExp][] = [
[
"nostr:nprofile1qqsp2alytxwazryxxjv0u0pqhkp247hc9xjetn5rch8c4s6xx5cmpxcpzpmhxue69uhkummnw3ezuamfdejsz9nhwden5te0v96xcctn9ehx7um5wghxcctwvs6ymk33",
/npub1z4m7g\.\.\.kzdsxana6p/i,
],
["nostr:npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6", /npub180cvv\.\.\.gkwsyjh6w6/i],
];
for (const [search, regexp] of profiles) {
it(`should handle ${search}`, () => {
cy.visit("#/search");
cy.findByRole("searchbox").type(search, { delay: 0 }).type("{enter}");
cy.contains(regexp).should("be.visible");
});
}
for (const [search, regexp] of profiles) {
const withoutPrefix = search.replace("nostr:", "");
it(`should handle ${withoutPrefix}`, () => {
cy.visit("#/search");
cy.findByRole("searchbox").type(search, { delay: 0 }).type("{enter}");
cy.contains(regexp).should("be.visible");
});
}
});
describe("Hashtag", () => {
it("should redirect to hashtag view", () => {
cy.visit("#/search");
cy.findByRole("searchbox").type("#bitcoin").type("{enter}");
cy.url().should("contain", "/t/bitcoin");
cy.contains("#bitcoin");
});
});
});

View File

@ -1,17 +0,0 @@
describe("Thread", () => {
it("should handle quote notes with e tags correctly", () => {
cy.visit(
"#/n/nevent1qqsx2lnyuke6vmsrz9fdrd6uwjy0g0e9l6menfgdj5truugkh9qmkkgpzpmhxue69uhkummnw3ezuamfdejszrthwden5te0dehhxtnvdakqgc9md6",
);
// find first note
cy.get(".chakra-card")
.first()
.within(() => {
// get quoted note
cy.get(".chakra-card").within(() => {
cy.contains(/looking for people to send money/);
});
});
});
});

View File

@ -1,53 +0,0 @@
/// <reference types="cypress" />
import "@testing-library/cypress/add-commands";
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
declare global {
namespace Cypress {
interface Chainable {
loginWithNewUser(): Chainable<void>;
}
}
}
Cypress.Commands.add("loginWithNewUser", () => {
cy.visit("/login");
cy.window().then(($win) => {
cy.stub($win, "prompt").returns("pass");
});
cy.findByRole("link", { name: /nsec/i }).click();
cy.findByRole("button", { name: /generate/i }).click();
cy.findByRole("combobox", { name: /bootstrap relay/i })
.clear()
.type("wss://nostrue.com", { delay: 0 });
cy.findByRole("button", { name: /login/i }).click();
cy.findByRole("button", { name: "Home" }).should("be.visible");
});

View File

@ -1,31 +0,0 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')
beforeEach(() => {
cy.clearAllLocalStorage();
// remove the database for every test
new Promise((res, rej) => {
const request = window.indexedDB.deleteDatabase("storage");
request.onsuccess = res;
request.onerror = rej;
});
});

View File

@ -1,8 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../tsconfig.json",
"include": ["../next-env.d.ts", "../cypress.config.ts", "**/*.ts", "**/*.tsx"],
"compilerOptions": {
"types": ["cypress", "@testing-library/cypress", "next"]
}
}

View File

@ -8,8 +8,6 @@
"dev": "VITE_APP_VERSION=development vite serve",
"build": "tsc --project tsconfig.json && vite build",
"format": "prettier --ignore-path .prettierignore -w .",
"e2e": "cypress open",
"test": "cypress run --e2e --browser=chrome",
"analyze": "npx vite-bundle-visualizer -o ./stats.html",
"build-icons": "node ./scripts/build-icons.mjs"
},
@ -67,7 +65,6 @@
},
"devDependencies": {
"@changesets/cli": "^2.26.2",
"@testing-library/cypress": "^9.0.0",
"@types/chroma-js": "^2.4.1",
"@types/debug": "^4.1.8",
"@types/identicon.js": "^2.3.1",
@ -80,7 +77,6 @@
"@types/webscopeio__react-textarea-autocomplete": "^4.7.2",
"@vitejs/plugin-react": "^4.0.4",
"camelcase": "^8.0.0",
"cypress": "^12.17.4",
"prettier": "^3.0.2",
"typescript": "^5.1.6",
"vite": "^4.4.9",

View File

@ -19,11 +19,11 @@ import NotificationsView from "./views/notifications";
import DirectMessagesView from "./views/messages";
import DirectMessageChatView from "./views/messages/chat";
import LoginView from "./views/login";
import LoginStartView from "./views/login/start";
import LoginNpubView from "./views/login/npub";
import LoginNip05View from "./views/login/nip05";
import LoginNsecView from "./views/login/nsec";
import LoginView from "./views/signin";
import LoginStartView from "./views/signin/start";
import LoginNpubView from "./views/signin/npub";
import LoginNip05View from "./views/signin/nip05";
import LoginNsecView from "./views/signin/nsec";
import UserView from "./views/user";
import UserNotesTab from "./views/user/notes";
@ -67,6 +67,7 @@ import RelayView from "./views/relays/relay";
import RelayReviewsView from "./views/relays/reviews";
import PopularRelaysView from "./views/relays/popular";
import UserTracksTab from "./views/user/tracks";
import SignupView from "./views/signup";
const ToolsHomeView = React.lazy(() => import("./views/tools"));
const NetworkView = React.lazy(() => import("./views/tools/network"));
@ -122,7 +123,7 @@ const RootPage = () => {
const router = createHashRouter([
{
path: "login",
path: "signin",
element: <LoginView />,
children: [
{ path: "", element: <LoginStartView /> },
@ -131,6 +132,10 @@ const router = createHashRouter([
{ path: "nsec", element: <LoginNsecView /> },
],
},
{
path: "signup",
element: <SignupView />,
},
{
path: "streams/:naddr",
element: (

View File

@ -83,7 +83,7 @@ export default function AccountSwitcher() {
leftIcon={<AddIcon />}
onClick={() => {
accountService.logout();
navigate("/login", { state: { from: location.pathname } });
navigate("/signin", { state: { from: location.pathname } });
}}
>
Add Account

View File

@ -64,8 +64,8 @@ export default function DesktopSideNav(props: Omit<FlexProps, "children">) {
<NavItems />
<Box h="4" />
{!account && (
<Button as={RouterLink} to="/login" state={{ from: location.pathname }} colorScheme="primary" w="full">
Login
<Button as={RouterLink} to="/signin" state={{ from: location.pathname }} colorScheme="primary" w="full">
Sign in
</Button>
)}
</Flex>

View File

@ -45,8 +45,8 @@ export default function MobileSideDrawer({ ...props }: Omit<DrawerProps, "childr
<NavItems />
<Box h="2" />
{!account && (
<Button as={RouterLink} to="/login" colorScheme="primary">
Login
<Button as={RouterLink} to="/signin" colorScheme="primary">
Sign in
</Button>
)}
</DrawerBody>

View File

@ -42,14 +42,23 @@ export default function PeopleListSelection({
<MenuOptionGroup value={selected} onChange={handleSelect} type="radio">
{account && <MenuItemOption value="following">Following</MenuItemOption>}
{!hideGlobalOption && <MenuItemOption value="global">Global</MenuItemOption>}
{lists.length > 0 && <MenuDivider />}
{lists
.filter((l) => l.kind === PEOPLE_LIST_KIND)
.map((list) => (
<MenuItemOption key={getEventCoordinate(list)} value={getEventCoordinate(list)} isTruncated maxW="90vw">
{getListName(list)}
</MenuItemOption>
))}
{lists.length > 0 && (
<>
<MenuDivider />
{lists
.filter((l) => l.kind === PEOPLE_LIST_KIND)
.map((list) => (
<MenuItemOption
key={getEventCoordinate(list)}
value={getEventCoordinate(list)}
isTruncated
maxW="90vw"
>
{getListName(list)}
</MenuItemOption>
))}
</>
)}
</MenuOptionGroup>
{favoriteLists.length > 0 && (
<>

View File

@ -21,5 +21,6 @@ export default function useUserLists(pubkey?: string, additionalRelays: string[]
{ enabled: !!pubkey, eventFilter },
);
return useSubject(timeline.timeline);
const lists = useSubject(timeline.timeline);
return pubkey ? lists : [];
}

View File

@ -43,12 +43,13 @@ function useListCoordinate(listId: ListId) {
export type PeopleListProviderProps = PropsWithChildren & {
initList?: ListId;
};
export default function PeopleListProvider({ children, initList = "following" }: PeopleListProviderProps) {
export default function PeopleListProvider({ children, initList }: PeopleListProviderProps) {
const account = useCurrentAccount();
const [params, setParams] = useSearchParams();
const navigate = useNavigate();
const location = useLocation();
const selected = params.get("people") || (initList as ListId);
const selected = params.get("people") || (initList as ListId) || (account ? "following" : "global");
const setSelected = useCallback(
(value: ListId) => {
const newParams = new URLSearchParams(location.search);

View File

@ -27,15 +27,9 @@ export default function RequireCurrentAccount({ children }: { children: JSX.Elem
if (!account)
return (
<Flex direction="column" w="full" h="full" alignItems="center" justifyContent="center" gap="4">
<Heading size="md">You must be logged in to use this view</Heading>
<Button
as={Link}
to="/login"
state={{ from: location.pathname }}
colorScheme="primary"
rightIcon={<ExternalLinkIcon />}
>
Login
<Heading size="md">You must be signed in to use this view</Heading>
<Button as={Link} to="/signin" state={{ from: location.pathname }} colorScheme="primary">
Sign in
</Button>
</Flex>
);

View File

@ -95,6 +95,10 @@ class AccountService {
}
logout() {
if (this.current.value) {
this.removeAccount(this.current.value.pubkey);
}
this.current.next(null);
this.isGhost.next(false);
localStorage.removeItem("lastAccount");

View File

@ -34,6 +34,7 @@ import Timestamp from "../../components/timestamp";
import VerticalPageLayout from "../../components/vertical-page-layout";
import BadgeAwardCard from "./components/badge-award-card";
import TimelineLoader from "../../classes/timeline-loader";
import { ErrorBoundary } from "../../components/error-boundary";
function BadgeActivityTab({ timeline }: { timeline: TimelineLoader }) {
const awards = useSubject(timeline.timeline);
@ -43,7 +44,9 @@ function BadgeActivityTab({ timeline }: { timeline: TimelineLoader }) {
<Flex direction="column" gap="4">
<IntersectionObserverProvider callback={callback}>
{awards.map((award) => (
<BadgeAwardCard key={award.id} award={award} showImage={false} />
<ErrorBoundary key={award.id}>
<BadgeAwardCard award={award} showImage={false} />
</ErrorBoundary>
))}
</IntersectionObserverProvider>
</Flex>

View File

@ -12,6 +12,7 @@ import useSubject from "../../hooks/use-subject";
import IntersectionObserverProvider from "../../providers/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import BadgeAwardCard from "./components/badge-award-card";
import { ErrorBoundary } from "../../components/error-boundary";
function BadgesPage() {
const { filter, listId } = usePeopleListContext();
@ -47,7 +48,9 @@ function BadgesPage() {
</Flex>
<IntersectionObserverProvider callback={callback}>
{awards.map((award) => (
<BadgeAwardCard key={award.id} award={award} />
<ErrorBoundary key={award.id}>
<BadgeAwardCard award={award} />
</ErrorBoundary>
))}
</IntersectionObserverProvider>
</VerticalPageLayout>

View File

@ -1,4 +1,4 @@
import { Avatar, Flex, Heading } from "@chakra-ui/react";
import { Avatar, Center, Flex, Heading } from "@chakra-ui/react";
import { Navigate, Outlet, useLocation } from "react-router-dom";
import { ReloadPrompt } from "../../components/reload-prompt";
import useSubject from "../../hooks/use-subject";
@ -13,20 +13,15 @@ export default function LoginView() {
return (
<>
<ReloadPrompt />
<Flex
direction="column"
alignItems="center"
justifyContent="center"
gap="4"
height="100%"
padding="4"
overflowX="hidden"
overflowY="auto"
>
<Avatar src="/apple-touch-icon.png" size="lg" flexShrink={0} />
<Heading>noStrudel</Heading>
<Outlet />
</Flex>
<Center w="full" h="full">
<Flex direction="column" alignItems="center" gap="2" maxW="sm" w="full" mx="4">
<Avatar src="/apple-touch-icon.png" size="lg" flexShrink={0} />
<Heading size="lg" mb="2">
Sign in
</Heading>
<Outlet />
</Flex>
</Center>
</>
);
}

View File

@ -89,7 +89,7 @@ export default function LoginNip05View() {
};
return (
<Flex as="form" direction="column" gap="4" onSubmit={handleSubmit} minWidth="350">
<Flex as="form" direction="column" gap="4" onSubmit={handleSubmit} minWidth="350" w="full">
<FormControl>
<FormLabel>Enter user NIP-05 id</FormLabel>
<InputGroup>

View File

@ -27,7 +27,7 @@ export default function LoginNpubView() {
};
return (
<Flex as="form" direction="column" gap="4" onSubmit={handleSubmit} minWidth="350">
<Flex as="form" direction="column" gap="4" onSubmit={handleSubmit} minWidth="350" w="full">
<FormControl>
<FormLabel>Enter user npub</FormLabel>
<Input type="text" placeholder="npub1" isRequired value={npub} onChange={(e) => setNpub(e.target.value)} />

View File

@ -77,11 +77,11 @@ export default function LoginNsecView() {
};
return (
<Flex as="form" direction="column" gap="4" onSubmit={handleSubmit} minWidth="350">
<Flex as="form" direction="column" gap="4" onSubmit={handleSubmit} minWidth="350" w="full">
<Alert status="warning" maxWidth="30rem">
<AlertIcon />
<Box>
<AlertTitle>Using nsec keys is insecure.</AlertTitle>
<AlertTitle>Using secret keys is insecure</AlertTitle>
<AlertDescription>
You should use a browser extension like{" "}
<Link isExternal href="https://getalby.com/" target="_blank">

View File

@ -1,28 +1,19 @@
import {
Alert,
AlertDescription,
AlertIcon,
AlertTitle,
Box,
Button,
Flex,
Heading,
Spinner,
useToast,
} from "@chakra-ui/react";
import { useState } from "react";
import { Badge, Button, Flex, Spinner, Text, useDisclosure, useToast } from "@chakra-ui/react";
import { Link as RouterLink, useLocation } from "react-router-dom";
import AccountCard from "./components/account-card";
import useSubject from "../../hooks/use-subject";
import accountService from "../../services/account";
import Key01 from "../../components/icons/key-01";
import ChevronDown from "../../components/icons/chevron-down";
import ChevronUp from "../../components/icons/chevron-up";
export default function LoginStartView() {
const location = useLocation();
const toast = useToast();
const [loading, setLoading] = useState(false);
const accounts = useSubject(accountService.accounts);
const advanced = useDisclosure();
const loginWithExtension = async () => {
const signinWithExtension = async () => {
if (window.nostr) {
try {
setLoading(true);
@ -60,37 +51,43 @@ export default function LoginStartView() {
return (
<Flex direction="column" gap="2" flexShrink={0} alignItems="center">
<Alert status="warning" maxWidth="30rem">
<AlertIcon />
<Box>
<AlertTitle>This app is half-baked.</AlertTitle>
<AlertDescription>There are bugs and things will break.</AlertDescription>
</Box>
</Alert>
<Button onClick={loginWithExtension} colorScheme="primary">
Use browser extension (NIP-07)
<Button onClick={signinWithExtension} leftIcon={<Key01 boxSize={6} />} w="sm" colorScheme="primary">
Sign in with extension
</Button>
<Button as={RouterLink} to="./nip05" state={location.state}>
Login with Nip-05 Id
<Button
variant="link"
onClick={advanced.onToggle}
mt="2"
w="sm"
rightIcon={advanced.isOpen ? <ChevronUp /> : <ChevronDown />}
>
Show Advanced
</Button>
<Button as={RouterLink} to="./npub" state={location.state}>
Login with pubkey key (npub)
</Button>
<Button as={RouterLink} to="./nsec" state={location.state}>
Login with secret key (nsec)
</Button>
{accounts.length > 0 && (
{advanced.isOpen && (
<>
<Heading size="md" mt="4">
Accounts:
</Heading>
<Flex gap="2" direction="column" minW={300}>
{accounts.map((account) => (
<AccountCard key={account.pubkey} account={account} />
))}
</Flex>
<Button as={RouterLink} to="./nip05" state={location.state} w="sm">
NIP05
<Badge ml="2" colorScheme="blue">
read-only
</Badge>
</Button>
<Button as={RouterLink} to="./npub" state={location.state} w="sm">
public key (npub)
<Badge ml="2" colorScheme="blue">
read-only
</Badge>
</Button>
<Button as={RouterLink} to="./nsec" state={location.state} w="sm">
secret key (nsec)
</Button>
</>
)}
<Text fontWeight="bold" mt="4">
Don't have an account?
</Text>
<Button as={RouterLink} to="/signup" state={location.state}>
Sign up
</Button>
</Flex>
);
}

222
src/views/signup/index.tsx Normal file
View File

@ -0,0 +1,222 @@
import { useEffect, useRef, useState } from "react";
import {
Avatar,
Box,
Button,
Card,
CardBody,
Center,
Flex,
FlexProps,
Heading,
Input,
SimpleGrid,
Text,
VisuallyHiddenInput,
} from "@chakra-ui/react";
import { useForm } from "react-hook-form";
import { Link as RouterLink, useLocation } from "react-router-dom";
import { useSet } from "react-use";
import { Kind0ParsedContent } from "../../helpers/user-metadata";
import { useRelayInfo } from "../../hooks/use-relay-info";
import { RelayFavicon } from "../../components/relay-favicon";
import ImagePlus from "../../components/icons/image-plus";
const containerProps: FlexProps = {
w: "full",
maxW: "sm",
mx: "4",
alignItems: "center",
direction: "column",
};
const AppIcon = () => <Avatar src="/apple-touch-icon.png" size="lg" flexShrink={0} />;
function NameStep({ onSubmit }: { onSubmit: (metadata: Kind0ParsedContent) => void }) {
const location = useLocation();
const { register, handleSubmit } = useForm({
defaultValues: {
name: "",
},
mode: "all",
});
const submit = handleSubmit((values) => {
const displayName = values.name;
const username = values.name.toLocaleLowerCase().replaceAll(/(\p{Z}|\p{P}|\p{C}|\p{M})/gu, "_");
onSubmit({
name: username,
display_name: displayName,
});
});
return (
<Flex as="form" gap="2" onSubmit={submit} {...containerProps}>
<AppIcon />
<Heading size="lg" mb="2">
Sign up
</Heading>
<Text>What should we call you?</Text>
<Input placeholder="Jane" w="full" mb="2" {...register("name", { required: true })} autoComplete="off" />
<Button w="full" colorScheme="primary" mb="4">
Next
</Button>
<Text fontWeight="bold">Already have an account?</Text>
<Button as={RouterLink} to="/signin" state={location.state}>
Sign in
</Button>
</Flex>
);
}
function ProfileImageStep({ displayName, onSubmit }: { displayName?: string; onSubmit: (picture?: File) => void }) {
const [file, setFile] = useState<File>();
const uploadRef = useRef<HTMLInputElement | null>(null);
const [preview, setPreview] = useState("");
useEffect(() => {
if (file) {
const url = URL.createObjectURL(file);
setPreview(url);
return () => URL.revokeObjectURL(url);
}
}, [file]);
return (
<Flex gap="4" {...containerProps}>
<Heading size="lg" mb="2">
Add a profile image
</Heading>
<VisuallyHiddenInput
type="file"
accept="image/*"
ref={uploadRef}
onChange={(e) => setFile(e.target.files?.[0])}
/>
<Avatar
as="button"
size="xl"
src={preview}
onClick={() => uploadRef.current?.click()}
cursor="pointer"
icon={<ImagePlus boxSize={8} />}
/>
<Heading size="md">{displayName}</Heading>
<Button w="full" colorScheme="primary" mb="4" maxW="sm" onClick={() => onSubmit(file)}>
{file ? "Next" : "Skip for now"}
</Button>
</Flex>
);
}
function RelayButton({ url, selected, onClick }: { url: string; selected: boolean; onClick: () => void }) {
const { info } = useRelayInfo(url);
return (
<Card
variant="outline"
size="sm"
borderColor={selected ? "primary.500" : "gray.500"}
borderRadius="lg"
cursor="pointer"
onClick={onClick}
>
<CardBody>
<Flex gap="2" mb="2">
<RelayFavicon relay={url} />
<Box>
<Heading size="sm">{info?.name}</Heading>
<Text fontSize="sm">{url}</Text>
</Box>
</Flex>
<Text>{info?.description}</Text>
</CardBody>
</Card>
);
}
const recommendedRelays = [
"wss://relay.damus.io",
"wss://welcome.nostr.wine",
"wss://nos.lol",
"wss://purplerelay.com",
"wss://nostr.bitcoiner.social",
"wss://nostr-pub.wellorder.net",
];
const defaultRelaySelection = new Set(["wss://relay.damus.io", "wss://nos.lol", "wss://welcome.nostr.wine"]);
function RelayStep({ onSubmit }: { onSubmit: (relays: string[]) => void }) {
const [relays, relayActions] = useSet<string>(defaultRelaySelection);
return (
<Flex gap="4" {...containerProps} maxW="8in">
<Heading size="lg" mb="2">
Select some relays
</Heading>
<SimpleGrid columns={[1, 1, 2]} spacing="4">
{recommendedRelays.map((url) => (
<RelayButton key={url} url={url} selected={relays.has(url)} onClick={() => relayActions.toggle(url)} />
))}
</SimpleGrid>
{relays.size === 0 && <Text color="orange">You must select at least one relay</Text>}
<Button
w="full"
colorScheme="primary"
mb="4"
maxW="sm"
isDisabled={relays.size === 0}
onClick={() => onSubmit(Array.from(relays))}
>
Next
</Button>
</Flex>
);
}
export default function SignupView() {
const [step, setStep] = useState(0);
const [metadata, setMetadata] = useState<Kind0ParsedContent>({});
const [profileImage, setProfileImage] = useState<File>();
const [relays, setRelays] = useState<string[]>([]);
const renderStep = () => {
const next = () => setStep((v) => v + 1);
switch (step) {
case 0:
return (
<NameStep
onSubmit={(m) => {
setMetadata((v) => ({ ...v, ...m }));
next();
}}
/>
);
case 1:
return (
<ProfileImageStep
displayName={metadata.display_name}
onSubmit={(file) => {
setProfileImage(file);
next();
}}
/>
);
case 2:
return (
<RelayStep
onSubmit={(r) => {
setRelays(r);
next();
}}
/>
);
}
};
return (
<Center w="full" h="full">
{renderStep()}
</Center>
);
}

View File

@ -7,8 +7,8 @@ process.env.VITE_ANALYTICS_SCRIPT = isProd
? `
<script
async defer
src="https://ackee.nostrudel.ninja/tracker.js"
data-ackee-server="https://ackee.nostrudel.ninja"
src="//ackee.nostrudel.ninja/tracker.js"
data-ackee-server="//ackee.nostrudel.ninja"
data-ackee-domain-id="58b1c39f-43f9-422b-bc7d-06aff35e764e"
></script>`
: "";

1006
yarn.lock

File diff suppressed because it is too large Load Diff