Merge pull request #5 from reyamir/feat/improve-dx

Improved dev experience
This commit is contained in:
Ren Amamiya 2023-03-05 09:42:28 +07:00 committed by GitHub
commit 42f4c8339e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1828 additions and 1116 deletions

View File

@ -5,13 +5,13 @@
"next/core-web-vitals",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"rules": {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-explicit-any": "error"
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-explicit-any": "warn"
},
"ignorePatterns": ["dist", "**/*.js", "**/*.json", "node_modules"]
}

View File

@ -3,12 +3,19 @@
"trailingComma": "es5",
"singleQuote": true,
"tabWidth": 2,
"printWidth": 150,
"printWidth": 120,
"useTabs": false,
"endOfLine": "lf",
"bracketSpacing": true,
"bracketSameLine": true,
"importOrder": ["^@layouts/(.*)$", "^@pages/(.*)$", "^@components/(.*)$", "^@utils/(.*)$", "^@stores/(.*)$", "<THIRD_PARTY_MODULES>", "^[./]"],
"bracketSameLine": false,
"importOrder": [
"^@layouts/(.*)$",
"^@pages/(.*)$",
"^@components/(.*)$",
"^@utils/(.*)$",
"<THIRD_PARTY_MODULES>",
"^[./]"
],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"plugins": ["@trivago/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"],

View File

@ -51,7 +51,7 @@ Lume is "an ambitious nostr client", so I don't want to limit it be a part of yo
## Features
**Current**: v0.1.1-alpha
**Current**: v0.1.2-alpha
- [x] create new key
- [x] import private key (hex/nsec)
@ -64,15 +64,15 @@ Lume is "an ambitious nostr client", so I don't want to limit it be a part of yo
- [x] publish a note (support markdown)
- [x] update profile
- [x] cache profile to local database
- [x] offline support
- [x] implement newsfeed infinite loading
- [x] native notification
**Next**:
- [ ] offline support
- [ ] implement newsfeed infinite loading
- [ ] handle tags (#[x]) in note
- [ ] handle reply note
- [ ] integrate webtorrent
- [ ] native notification
- [ ] publish a reply
- [ ] direct message
- [ ] multi accounts
@ -83,3 +83,29 @@ Lume is "an ambitious nostr client", so I don't want to limit it be a part of yo
## Roadmap
Public roadmap will be released when Lume reach v0.3.0
## Running dev build
Prerequisites:
- Nodejs >= 18.0.0
- Install pnpm: [docs](https://pnpm.io/)
- Setup Tauri: [docs](https://tauri.app/v1/guides/getting-started/prerequisites)
Clone repo:
```
git clone https://github.com/reyamir/lume-desktop.git
```
Install dependencies
```
pnpm install
```
Run development window
```
pnpm tauri dev
```

View File

@ -23,15 +23,14 @@
"boring-avatars": "^1.7.0",
"framer-motion": "^9.1.7",
"moment": "^2.29.4",
"nanostores": "^0.7.4",
"next": "^13.2.1",
"next": "^13.2.3",
"next-remove-imports": "^1.0.10",
"nostr-relaypool": "^0.5.3",
"nostr-tools": "^1.7.1",
"nostr-tools": "^1.7.4",
"qrcode.react": "^3.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.43.2",
"react-hook-form": "^7.43.4",
"react-moment": "^1.1.3",
"react-player": "^2.11.2",
"react-virtuoso": "^4.1.0",
@ -43,7 +42,7 @@
"@tailwindcss/typography": "^0.5.9",
"@tauri-apps/cli": "^1.2.3",
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
"@types/node": "^18.14.2",
"@types/node": "^18.14.6",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.54.0",
@ -51,7 +50,7 @@
"autoprefixer": "^10.4.13",
"csstype": "^3.1.1",
"eslint": "^8.35.0",
"eslint-config-next": "^13.2.1",
"eslint-config-next": "^13.2.3",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
@ -59,7 +58,7 @@
"lint-staged": "^13.1.2",
"postcss": "^8.4.21",
"prettier": "^2.8.4",
"prettier-plugin-tailwindcss": "^0.2.3",
"prettier-plugin-tailwindcss": "^0.2.4",
"prop-types": "^15.8.1",
"tailwindcss": "^3.2.7",
"typescript": "^4.9.5"

File diff suppressed because it is too large Load Diff

View File

@ -145,8 +145,8 @@
.w-md-editor-toolbar-child {
position: absolute;
border-radius: 3px;
box-shadow: 0 0 0 1px var(--color-border-default),
0 0 0 var(--color-border-default), 0 1px 1px var(--color-border-default);
box-shadow: 0 0 0 1px var(--color-border-default), 0 0 0 var(--color-border-default),
0 1px 1px var(--color-border-default);
background-color: var(--color-canvas-default);
z-index: 1;
display: none;

View File

@ -7,12 +7,7 @@ type ActiveLinkProps = LinkProps & {
activeClassName: string;
};
const ActiveLink = ({
children,
activeClassName,
className,
...props
}: PropsWithChildren<ActiveLinkProps>) => {
const ActiveLink = ({ children, activeClassName, className, ...props }: PropsWithChildren<ActiveLinkProps>) => {
const { asPath, isReady } = useRouter();
const [computedClassName, setComputedClassName] = useState(className);
@ -26,8 +21,7 @@ const ActiveLink = ({
// Using URL().pathname to get rid of query and hash
const activePathname = new URL(asPath, location.href).pathname;
const newClassName =
linkPathname === activePathname ? `${className} ${activeClassName}`.trim() : className;
const newClassName = linkPathname === activePathname ? `${className} ${activeClassName}`.trim() : className;
if (newClassName !== computedClassName) {
setComputedClassName(newClassName);

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import Image from 'next/image';
import { memo } from 'react';
@ -14,7 +13,8 @@ export const Account = memo(function Account({ user, current }: { user: any; cur
onClick={() => setCurrentUser()}
className={`relative h-11 w-11 shrink overflow-hidden rounded-full ${
current === user.pubkey ? 'ring-1 ring-fuchsia-500 ring-offset-4 ring-offset-black' : ''
}`}>
}`}
>
{userData?.picture !== undefined ? (
<Image src={userData.picture} alt="user's avatar" fill={true} className="rounded-full object-cover" />
) : (

View File

@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Account } from '@components/accountBar/account';
import { Account } from '@components/columns/account/account';
import LumeSymbol from '@assets/icons/Lume';
import { PlusIcon } from '@radix-ui/react-icons';
@ -8,7 +7,7 @@ import Link from 'next/link';
import { useCallback, useEffect, useState } from 'react';
import Database from 'tauri-plugin-sql-api';
export default function AccountBar() {
export default function AccountColumn() {
const [users, setUsers] = useState([]);
const [currentUser]: any = useLocalStorage('current-user');
@ -31,7 +30,8 @@ export default function AccountBar() {
))}
<Link
href="/onboarding"
className="group relative flex h-11 w-11 shrink cursor-pointer items-center justify-center overflow-hidden rounded-full border-2 border-dashed border-zinc-600 hover:border-zinc-400">
className="group relative flex h-11 w-11 shrink cursor-pointer items-center justify-center overflow-hidden rounded-full border-2 border-dashed border-zinc-600 hover:border-zinc-400"
>
<PlusIcon className="h-4 w-4 text-zinc-400 group-hover:text-zinc-200" />
</Link>
</div>

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { RelayContext } from '@components/contexts/relay';
import { dateToUnix } from '@utils/getDate';
@ -30,7 +29,9 @@ export default function CreatePost() {
buttonProps: { className: 'cta-btn', 'aria-label': 'Post a message' },
icon: (
<div className="relative inline-flex h-10 w-16 transform cursor-pointer overflow-hidden rounded bg-zinc-900 px-2.5 ring-zinc-500/50 ring-offset-zinc-900 will-change-transform focus:outline-none focus:ring-1 focus:ring-offset-2 active:translate-y-1">
<span className="absolute inset-px z-10 inline-flex items-center justify-center rounded bg-zinc-900 text-zinc-200">Post</span>
<span className="absolute inset-px z-10 inline-flex items-center justify-center rounded bg-zinc-900 text-zinc-200">
Post
</span>
<span className="absolute inset-0 z-0 scale-x-[2.0] blur before:absolute before:inset-0 before:top-1/2 before:aspect-square before:animate-disco before:bg-gradient-conic before:from-gray-300 before:via-fuchsia-600 before:to-orange-600"></span>
</div>
),

View File

@ -1,12 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ActiveLink from '@components/activeLink';
import CreatePost from '@components/navigatorBar/createPost';
import { ProfileMenu } from '@components/navigatorBar/profileMenu';
import CreatePost from '@components/columns/navigator/createPost';
import { UserDropdownMenu } from '@components/columns/navigator/userDropdownMenu';
import { PlusIcon } from '@radix-ui/react-icons';
import { useLocalStorage } from '@rehooks/local-storage';
export default function NavigatorBar() {
export default function NavigatorColumn() {
const [currentUser]: any = useLocalStorage('current-user');
const profile = JSON.parse(currentUser.metadata);
@ -18,7 +17,7 @@ export default function NavigatorBar() {
<div className="flex flex-col p-2">
<div className="flex items-center justify-between">
<h5 className="font-semibold leading-tight text-zinc-100">{profile.display_name || ''}</h5>
<ProfileMenu pubkey={currentUser.pubkey} />
<UserDropdownMenu pubkey={currentUser.pubkey} />
</div>
<span className="text-sm leading-tight text-zinc-500">@{profile.username || ''}</span>
</div>
@ -30,22 +29,27 @@ export default function NavigatorBar() {
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between px-2">
<h3 className="text-sm font-bold text-zinc-400">Newsfeed</h3>
<button type="button" className="group flex h-6 w-6 items-center justify-center rounded-full hover:bg-zinc-900">
<button
type="button"
className="group flex h-6 w-6 items-center justify-center rounded-full hover:bg-zinc-900"
>
<PlusIcon className="h-3 w-3 text-zinc-400 group-hover:text-zinc-100" />
</button>
</div>
<div className="flex flex-col gap-1 text-zinc-500">
<ActiveLink
href={`/feed/following`}
href={`/newsfeed/following`}
activeClassName="ring-1 ring-white/10 dark:bg-zinc-900 dark:text-white"
className="flex h-10 items-center gap-1 rounded-lg px-2.5 text-sm font-medium hover:bg-zinc-900">
className="flex h-10 items-center gap-1 rounded-lg px-2.5 text-sm font-medium hover:bg-zinc-900"
>
<span>#</span>
<span>following</span>
</ActiveLink>
<ActiveLink
href={`/feed/global`}
href={`/newsfeed/global`}
activeClassName="ring-1 ring-white/10 dark:bg-zinc-900 dark:text-white"
className="flex h-10 items-center gap-1 rounded-lg px-2.5 text-sm font-medium hover:bg-zinc-900">
className="flex h-10 items-center gap-1 rounded-lg px-2.5 text-sm font-medium hover:bg-zinc-900"
>
<span>#</span>
<span>global</span>
</ActiveLink>
@ -55,7 +59,10 @@ export default function NavigatorBar() {
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between px-2">
<h3 className="text-sm font-bold text-zinc-400">Direct Messages</h3>
<button type="button" className="group flex h-6 w-6 items-center justify-center rounded-full hover:bg-zinc-900">
<button
type="button"
className="group flex h-6 w-6 items-center justify-center rounded-full hover:bg-zinc-900"
>
<PlusIcon className="h-3 w-3 text-zinc-400 group-hover:text-zinc-100" />
</button>
</div>

View File

@ -5,7 +5,7 @@ import { useRouter } from 'next/router';
import { nip19 } from 'nostr-tools';
import { memo } from 'react';
export const ProfileMenu = memo(function ProfileMenu({ pubkey }: { pubkey: string }) {
export const UserDropdownMenu = memo(function ProfileMenu({ pubkey }: { pubkey: string }) {
const router = useRouter();
const viewProfile = () => {
@ -31,20 +31,24 @@ export const ProfileMenu = memo(function ProfileMenu({ pubkey }: { pubkey: strin
<DropdownMenu.Portal>
<DropdownMenu.Content
className="min-w-[220px] rounded-md border border-white/20 bg-zinc-800 p-1 shadow-lg shadow-black/30 will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade"
sideOffset={2}>
sideOffset={2}
>
<DropdownMenu.Item
onClick={() => viewProfile()}
className="group relative flex h-[30px] select-none items-center rounded px-1 pl-6 text-sm leading-none text-zinc-100 outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-zinc-700 data-[highlighted]:text-fuchsia-400 data-[disabled]:text-zinc-400">
className="group relative flex h-[30px] select-none items-center rounded px-1 pl-6 text-sm leading-none text-zinc-100 outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-zinc-700 data-[highlighted]:text-fuchsia-400 data-[disabled]:text-zinc-400"
>
View profile
</DropdownMenu.Item>
<DropdownMenu.Item
onClick={() => updateProfile()}
className="group relative flex h-[30px] select-none items-center rounded px-1 pl-6 text-sm leading-none text-zinc-100 outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-zinc-700 data-[highlighted]:text-fuchsia-400 data-[disabled]:text-zinc-400">
className="group relative flex h-[30px] select-none items-center rounded px-1 pl-6 text-sm leading-none text-zinc-100 outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-zinc-700 data-[highlighted]:text-fuchsia-400 data-[disabled]:text-zinc-400"
>
Update profile
</DropdownMenu.Item>
<DropdownMenu.Item
onClick={() => copyPubkey()}
className="group relative flex h-[30px] select-none items-center rounded px-1 pl-6 text-sm leading-none text-zinc-100 outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-zinc-700 data-[highlighted]:text-fuchsia-400 data-[disabled]:text-zinc-400">
className="group relative flex h-[30px] select-none items-center rounded px-1 pl-6 text-sm leading-none text-zinc-100 outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-zinc-700 data-[highlighted]:text-fuchsia-400 data-[disabled]:text-zinc-400"
>
Copy public key
</DropdownMenu.Item>
<DropdownMenu.Item className="group relative flex h-[30px] select-none items-center rounded px-1 pl-6 text-sm leading-none text-zinc-100 outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-zinc-700 data-[highlighted]:text-fuchsia-400 data-[disabled]:text-zinc-400">

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { writeStorage } from '@rehooks/local-storage';
import { createContext, useEffect, useState } from 'react';
import Database from 'tauri-plugin-sql-api';

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { RelayPool } from 'nostr-relaypool';
import { createContext, useMemo } from 'react';

View File

@ -1,100 +0,0 @@
import { EnvelopeClosedIcon, PlusIcon, UpdateIcon } from '@radix-ui/react-icons';
import Image from 'next/image';
const sampleData = [
{
name: 'Dick Whitman (🌎/21M)',
role: 'dickwhitman@nostrplebs.com',
imageUrl: 'https://pbs.twimg.com/profile_images/1594930968325984256/TjMXaXBE_400x400.jpg',
},
{
name: 'Jack',
role: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
imageUrl: 'https://pbs.twimg.com/profile_images/1115644092329758721/AFjOr-K8_400x400.jpg',
},
{
name: 'Sats Symbol',
role: 'npub1mqngkfwfyv2ckv7hshck9pqucpz08tktde2jukr3hheatup2y2tqnzc32w',
imageUrl: 'https://pbs.twimg.com/profile_images/1563388888860594177/7evrI1uB_400x400.jpg',
},
];
export default function Empty() {
return (
<div className="mx-auto max-w-lg pt-8">
<div>
<div className="text-center">
<svg
className="mx-auto h-12 w-12 text-zinc-300"
fill="none"
stroke="currentColor"
viewBox="0 0 48 48"
aria-hidden="true">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M34 40h10v-4a6 6 0 00-10.712-3.714M34 40H14m20 0v-4a9.971 9.971 0 00-.712-3.714M14 40H4v-4a6 6 0 0110.713-3.714M14 40v-4c0-1.313.253-2.566.713-3.714m0 0A10.003 10.003 0 0124 26c4.21 0 7.813 2.602 9.288 6.286M30 14a6 6 0 11-12 0 6 6 0 0112 0zm12 6a4 4 0 11-8 0 4 4 0 018 0zm-28 0a4 4 0 11-8 0 4 4 0 018 0z"></path>
</svg>
<h2 className="mt-2 text-lg font-medium text-zinc-100">
You haven&apos;t followed anyone yet
</h2>
<p className="mt-1 text-sm text-zinc-500">
You can send invite via email to your friend and lume will onboard them into nostr or
follow some people in suggested below
</p>
</div>
<form action="#" className="relative mt-6">
<input
type="email"
name="email"
id="email"
className="block h-11 w-full rounded-lg border-none px-4 shadow-md ring-1 ring-white/10 placeholder:text-zinc-500 dark:bg-zinc-800 dark:text-zinc-200"
placeholder="Enter an email"
/>
<button className="absolute right-0.5 top-1/2 inline-flex h-10 -translate-y-1/2 transform items-center gap-1 rounded-md border border-zinc-600 bg-zinc-700 px-4 text-sm font-medium text-zinc-200 shadow-md">
<EnvelopeClosedIcon className="h-4 w-4" />
Invite
</button>
</form>
</div>
<div className="mt-10 flex flex-col items-start gap-4">
<div className="flex w-full items-center justify-between">
<h3 className="text-sm font-medium text-zinc-500">Suggestions</h3>
<UpdateIcon className="h-4 w-4 text-zinc-600" />
</div>
<ul className="w-full divide-y divide-zinc-800 border-t border-b border-zinc-800">
{sampleData.map((person, index) => (
<li key={index} className="flex items-center justify-between space-x-3 py-4">
<div className="flex min-w-0 flex-1 items-center space-x-3">
<div className="relative h-10 w-10 flex-shrink-0">
<Image
className="rounded-full object-cover"
src={person.imageUrl}
alt={person.name}
fill={true}
/>
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-zinc-200">{person.name}</p>
<p className="w-56 truncate text-sm font-medium text-zinc-500">{person.role}</p>
</div>
</div>
<div className="flex-shrink-0">
<button
type="button"
className="inline-flex items-center rounded-full border border-zinc-700 bg-zinc-800 px-3 py-1 text-xs font-medium text-zinc-400 shadow-sm hover:bg-zinc-800 focus:outline-none focus:ring-2 focus:ring-fuchsia-600 focus:ring-offset-2">
<PlusIcon className="-ml-1 h-5 w-5" />
<span className="text-sm font-medium text-zinc-300">Follow</span>
</button>
</div>
</li>
))}
</ul>
<button className="bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 bg-clip-text text-sm font-bold text-transparent">
Explore more
</button>
</div>
</div>
);
}

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import Avatar from 'boring-avatars';
import Image from 'next/image';
import { memo, useEffect, useState } from 'react';
@ -9,7 +8,7 @@ export const ImageWithFallback = memo(function ImageWithFallback({
fill,
className,
}: {
src: any;
src: string;
alt: string;
fill: boolean;
className: string;

View File

@ -1,18 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export function IncomingList({ data }: { data: any }) {
const list: any = Array.from(new Set(data.map((item: any) => item.pubkey)));
if (list.length > 0) {
return (
<>
{list.map((item, index) => (
<div key={index}>
<p>{item}</p>
</div>
))}
</>
);
} else {
return <></>;
}
}

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { RelayContext } from '@components/contexts/relay';
import { dateToUnix } from '@utils/getDate';
@ -67,7 +66,11 @@ export default function Reaction({ eventID, eventPubkey }: { eventID: string; ev
return (
<button onClick={(e) => handleReaction(e)} className="group flex w-16 items-center gap-1.5 text-sm text-zinc-500">
<div className="rounded-lg p-1 group-hover:bg-zinc-600">
{isReact ? <HeartFilledIcon className="h-4 w-4 group-hover:text-red-400" /> : <HeartIcon className="h-4 w-4 text-zinc-500" />}
{isReact ? (
<HeartFilledIcon className="h-4 w-4 group-hover:text-red-400" />
) : (
<HeartIcon className="h-4 w-4 text-zinc-500" />
)}
</div>
<span>{reaction}</span>
</button>

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ChatBubbleIcon } from '@radix-ui/react-icons';
import { useState } from 'react';

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { DatabaseContext } from '@components/contexts/database';
import { ImageWithFallback } from '@components/imageWithFallback';
@ -17,7 +16,9 @@ export const User = memo(function User({ pubkey, time }: { pubkey: string; time:
async (event) => {
const metadata: any = JSON.parse(event.content);
await db.execute(`INSERT OR IGNORE INTO cache_profiles (id, metadata) VALUES ("${pubkey}", '${JSON.stringify(metadata)}')`);
await db.execute(
`INSERT OR IGNORE INTO cache_profiles (id, metadata) VALUES ("${pubkey}", '${JSON.stringify(metadata)}')`
);
setProfile(metadata);
},
[db, pubkey]
@ -53,13 +54,20 @@ export const User = memo(function User({ pubkey, time }: { pubkey: string; time:
{profile.picture ? (
<ImageWithFallback src={profile.picture} alt={pubkey} fill={true} className="rounded-full object-cover" />
) : (
<Avatar size={44} name={pubkey} variant="beam" colors={['#FEE2E2', '#FEF3C7', '#F59E0B', '#EC4899', '#D946EF', '#8B5CF6']} />
<Avatar
size={44}
name={pubkey}
variant="beam"
colors={['#FEE2E2', '#FEF3C7', '#F59E0B', '#EC4899', '#D946EF', '#8B5CF6']}
/>
)}
</div>
<div className="flex w-full flex-1 items-start justify-between">
<div className="flex w-full justify-between">
<div className="flex items-baseline gap-2 text-sm">
<span className="font-bold leading-tight">{profile.name ? profile.name : truncate(pubkey, 16, ' .... ')}</span>
<span className="font-bold leading-tight">
{profile.name ? profile.name : truncate(pubkey, 16, ' .... ')}
</span>
<span className="leading-tight text-zinc-500">·</span>
<Moment fromNow unix className="text-zinc-500">
{time}

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { truncate } from '@utils/truncate';
import { memo, useEffect, useState } from 'react';

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ImageWithFallback } from '@components/imageWithFallback';
import { truncate } from '@utils/truncate';
@ -25,14 +24,23 @@ export const UserWithUsername = memo(function UserWithUsername({ pubkey }: { pub
{profile.picture ? (
<ImageWithFallback src={profile.picture} alt={pubkey} fill={true} className="rounded-full object-cover" />
) : (
<Avatar size={44} name={pubkey} variant="beam" colors={['#FEE2E2', '#FEF3C7', '#F59E0B', '#EC4899', '#D946EF', '#8B5CF6']} />
<Avatar
size={44}
name={pubkey}
variant="beam"
colors={['#FEE2E2', '#FEF3C7', '#F59E0B', '#EC4899', '#D946EF', '#8B5CF6']}
/>
)}
</div>
<div className="flex w-full flex-1 items-start justify-between">
<div className="flex w-full justify-between">
<div className="flex flex-col gap-1 text-sm">
<span className="font-bold leading-tight">{profile.name ? profile.name : truncate(pubkey, 16, ' .... ')}</span>
<span className="text-zinc-500">{profile.username ? profile.username : truncate(pubkey, 16, ' .... ')}</span>
<span className="font-bold leading-tight">
{profile.name ? profile.name : truncate(pubkey, 16, ' .... ')}
</span>
<span className="text-zinc-500">
{profile.username ? profile.username : truncate(pubkey, 16, ' .... ')}
</span>
</div>
<div>
<DotsHorizontalIcon className="h-4 w-4 text-zinc-500" />

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { DatabaseContext } from '@components/contexts/database';
import { RelayContext } from '@components/contexts/relay';
@ -40,7 +39,14 @@ export const NoteConnector = memo(function NoteConnector({
`INSERT OR IGNORE INTO
cache_notes
(id, pubkey, created_at, kind, tags, content) VALUES
("${event.id}", "${event.pubkey}", "${event.created_at}", "${event.kind}", '${JSON.stringify(event.tags)}', "${event.content}");`
(
"${event.id}",
"${event.pubkey}",
"${event.created_at}",
"${event.kind}",
'${JSON.stringify(event.tags)}',
"${event.content}"
);`
);
},
[db]
@ -85,7 +91,10 @@ export const NoteConnector = memo(function NoteConnector({
<h3 className="text-sm font-semibold text-zinc-500"># following</h3>
</div>
<div className="flex items-center gap-2">
<button onClick={() => reloadNewsfeed()} className={`${reload ? 'animate-spin' : ''} rounded-full p-1 hover:bg-zinc-800`}>
<button
onClick={() => reloadNewsfeed()}
className={`${reload ? 'animate-spin' : ''} rounded-full p-1 hover:bg-zinc-800`}
>
<ReloadIcon className="h-3.5 w-3.5 text-zinc-500" />
</button>
<div className="inline-flex items-center gap-1 rounded-full border border-zinc-700 bg-zinc-800 px-2.5 py-1">

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import Reaction from '@components/note/atoms/reaction';
import Reply from '@components/note/atoms/reply';
import { User } from '@components/note/atoms/user';

View File

@ -7,7 +7,8 @@ export default function LinkCard({ data }: { data: object }) {
<Link
href={data['url']}
target={'_blank'}
className="relative mt-2 flex flex-col overflow-hidden rounded-xl border border-zinc-700">
className="relative mt-2 flex flex-col overflow-hidden rounded-xl border border-zinc-700"
>
<div className="relative aspect-video h-auto w-full">
<Image src={data['image']} alt="image preview" fill={true} className="object-cover" />
</div>

View File

@ -1,53 +0,0 @@
import { RelayContext } from '@components/contexts/relay';
import { Content } from '@components/note/content';
import NoteReply from '@components/note/modal/noteReply';
import useLocalStorage from '@rehooks/local-storage';
import { memo, useContext, useState } from 'react';
/* eslint-disable @typescript-eslint/no-explicit-any */
const Modal = ({ event }: { event: any }) => {
const relayPool: any = useContext(RelayContext);
const [relays]: any = useLocalStorage('relays');
const [events, setEvents] = useState([]);
relayPool.subscribe(
[
{
'#e': [event.id],
since: event.created_at,
kinds: [1],
},
],
relays,
(event: any) => {
setEvents((events) => [event, ...events]);
},
undefined,
(events: any, relayURL: any) => {
console.log(events, relayURL);
}
);
return (
<div className="flex min-h-full items-center justify-center p-4">
<div className="relative h-[90vh] w-full max-w-3xl transform overflow-hidden rounded-lg text-zinc-100 shadow-modal transition-all">
<div className="absolute top-0 left-0 h-full w-full bg-black bg-opacity-20 backdrop-blur-lg"></div>
<div className="relative z-10 h-full p-4">
<div className="relative h-full overflow-auto rounded-lg border-[0.5px] border-white/30 bg-zinc-800 p-4 shadow-inner">
<div className="flex flex-col gap-4">
<Content data={event} />
</div>
<div className="flex flex-col gap-2 divide-y divide-zinc-700">
{events.map((item) => (
<NoteReply key={item.id} event={item} />
))}
</div>
</div>
</div>
</div>
</div>
);
};
export default memo(Modal);

View File

@ -1,10 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Content } from '@components/note/content';
export default function NoteReply({ event }: { event: any }) {
return (
<div className="flex h-min min-h-min w-full select-text flex-col border-b border-zinc-800 py-4 hover:bg-zinc-800">
<Content data={event} />
</div>
);
}

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { RelayContext } from '@components/contexts/relay';
import { UserRepost } from '@components/note/atoms/userRepost';
import { Content } from '@components/note/content';

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Content } from '@components/note/content';
import { memo } from 'react';

3
src/layouts/base.tsx Normal file
View File

@ -0,0 +1,3 @@
export default function BaseLayout({ children }: { children: React.ReactNode }) {
return <div className="h-screen w-screen bg-white text-zinc-900 dark:bg-near-black dark:text-white">{children}</div>;
}

View File

@ -1,7 +0,0 @@
export default function BaseLayout({ children }: { children: React.ReactNode }) {
return (
<div className="h-screen w-screen bg-white text-zinc-900 dark:bg-near-black dark:text-white">
{children}
</div>
);
}

View File

@ -1,11 +1,8 @@
export default function FullLayout({ children }: { children: React.ReactNode }) {
export default function FullscreenLayout({ children }: { children: React.ReactNode }) {
return (
<div className="bg-gradient-radial-page relative h-full overflow-hidden">
{/* dragging area */}
<div
data-tauri-drag-region
className="absolute top-0 left-0 z-20 h-16 w-full bg-transparent"
/>
<div data-tauri-drag-region className="absolute top-0 left-0 z-20 h-16 w-full bg-transparent" />
{/* end dragging area */}
{/* content */}
<div className="relative z-10 h-full">{children}</div>

View File

@ -1,16 +1,16 @@
import AccountBar from '@components/accountBar';
import NavigatorBar from '@components/navigatorBar';
import AccountColumn from '@components/columns/account';
import NavigatorColumn from '@components/columns/navigator';
export default function NewsFeedLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex h-full w-full flex-row">
<div className="relative h-full w-[70px] shrink-0 border-r border-zinc-900">
<div data-tauri-drag-region className="absolute top-0 left-0 h-12 w-full" />
<AccountBar />
<AccountColumn />
</div>
<div className="grid grow grid-cols-4">
<div className="col-span-1">
<NavigatorBar />
<NavigatorColumn />
</div>
<div className="col-span-3 m-3 ml-0 overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900 shadow-input shadow-black/20">
<div className="h-full w-full rounded-lg">{children}</div>

View File

@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import AccountBar from '@components/accountBar';
import ActiveLink from '@components/activeLink';
import AccountColumn from '@components/columns/account';
import { useLocalStorage } from '@rehooks/local-storage';
@ -11,7 +10,7 @@ export default function UserLayout({ children }: { children: React.ReactNode })
<div className="flex h-full w-full flex-row">
<div className="relative h-full w-[70px] shrink-0 border-r border-zinc-900">
<div data-tauri-drag-region className="absolute top-0 left-0 h-12 w-full" />
<AccountBar />
<AccountColumn />
</div>
<div className="grid grow grid-cols-4">
<div className="col-span-1">
@ -27,13 +26,15 @@ export default function UserLayout({ children }: { children: React.ReactNode })
<ActiveLink
href={`/profile/${currentUser.pubkey}`}
activeClassName="ring-1 ring-white/10 dark:bg-zinc-900 dark:text-white"
className="flex h-10 items-center gap-1 rounded-lg px-2.5 text-sm font-medium hover:bg-zinc-900">
className="flex h-10 items-center gap-1 rounded-lg px-2.5 text-sm font-medium hover:bg-zinc-900"
>
<span>Personal Page</span>
</ActiveLink>
<ActiveLink
href={`/profile/update?pubkey=${currentUser.pubkey}`}
activeClassName="ring-1 ring-white/10 dark:bg-zinc-900 dark:text-white"
className="flex h-10 items-center gap-1 rounded-lg px-2.5 text-sm font-medium hover:bg-zinc-900">
className="flex h-10 items-center gap-1 rounded-lg px-2.5 text-sm font-medium hover:bg-zinc-900"
>
<span>Update Profile</span>
</ActiveLink>
</div>

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import DatabaseProvider from '@components/contexts/database';
import RelayProvider from '@components/contexts/relay';

View File

@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import BaseLayout from '@layouts/baseLayout';
import FullLayout from '@layouts/fullLayout';
import BaseLayout from '@layouts/base';
import FullscreenLayout from '@layouts/fullscreen';
import LumeSymbol from '@assets/icons/Lume';
import { useLocalStorage } from '@rehooks/local-storage';
@ -23,7 +22,7 @@ export default function Page() {
} else {
setTimeout(() => {
setLoading(false);
router.push('/feed/following');
router.push('/newsfeed/following');
}, 1500);
}
}, [currentUser, router]);
@ -36,12 +35,16 @@ export default function Page() {
<LumeSymbol className="h-16 w-16 text-white" />
</motion.div>
<div className="flex flex-col items-center gap-0.5">
<motion.h2 layoutId="subtitle" className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-4xl font-medium text-transparent">
<motion.h2
layoutId="subtitle"
className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-4xl font-medium text-transparent"
>
A censorship-resistant social network
</motion.h2>
<motion.h1
layoutId="title"
className="bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 bg-clip-text text-5xl font-bold text-transparent">
className="bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 bg-clip-text text-5xl font-bold text-transparent"
>
built on nostr
</motion.h1>
</div>
@ -49,12 +52,18 @@ export default function Page() {
<div className="flex items-center gap-2 pb-16">
<div className="h-10">
{loading ? (
<svg className="h-5 w-5 animate-spin text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<svg
className="h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : (
<></>
@ -65,7 +74,8 @@ export default function Page() {
<div className="absolute inset-0 bg-gradient-to-r from-fuchsia-400/10 to-orange-100/10 opacity-100 [mask-image:radial-gradient(farthest-side_at_top,white,transparent)]">
<svg
aria-hidden="true"
className="dark:fill-white/2.5 absolute inset-x-0 inset-y-[-50%] h-[200%] w-full skew-y-[-18deg] fill-black/40 stroke-black/50 mix-blend-overlay dark:stroke-white/5">
className="dark:fill-white/2.5 absolute inset-x-0 inset-y-[-50%] h-[200%] w-full skew-y-[-18deg] fill-black/40 stroke-black/50 mix-blend-overlay dark:stroke-white/5"
>
<defs>
<pattern id=":R11d6:" width="72" height="56" patternUnits="userSpaceOnUse" x="-12" y="4">
<path d="M.5 56V.5H72" fill="none"></path>
@ -86,11 +96,17 @@ export default function Page() {
}
Page.getLayout = function getLayout(
page: string | number | boolean | ReactElement<unknown, string | JSXElementConstructor<unknown>> | ReactFragment | ReactPortal
page:
| string
| number
| boolean
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
| ReactFragment
| ReactPortal
) {
return (
<BaseLayout>
<FullLayout>{page}</FullLayout>
<FullscreenLayout>{page}</FullscreenLayout>
</BaseLayout>
);
};

View File

@ -0,0 +1,28 @@
import BaseLayout from '@layouts/base';
import NewsFeedLayout from '@layouts/newsfeed';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal } from 'react';
export default function Page() {
return (
<div className="h-full w-full">
<p>Global</p>
</div>
);
}
Page.getLayout = function getLayout(
page:
| string
| number
| boolean
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
| ReactFragment
| ReactPortal
) {
return (
<BaseLayout>
<NewsFeedLayout>{page}</NewsFeedLayout>
</BaseLayout>
);
};

View File

@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import BaseLayout from '@layouts/baseLayout';
import NewsFeedLayout from '@layouts/newsfeedLayout';
import BaseLayout from '@layouts/base';
import NewsFeedLayout from '@layouts/newsfeed';
import { DatabaseContext } from '@components/contexts/database';
import { NoteConnector } from '@components/note/connector';
@ -69,7 +68,9 @@ export default function Page() {
useEffect(() => {
const getData = async () => {
const result = await db.select(
`SELECT * FROM cache_notes WHERE created_at <= ${dateToUnix(now.current)} ORDER BY created_at DESC LIMIT ${limit.current}`
`SELECT * FROM cache_notes WHERE created_at <= ${dateToUnix(now.current)} ORDER BY created_at DESC LIMIT ${
limit.current
}`
);
if (result) {
setData(result);
@ -94,7 +95,8 @@ export default function Page() {
<div className="absolute top-16 left-1/2 z-50 -translate-x-1/2 transform">
<button
onClick={() => loadNewest()}
className="inline-flex h-8 transform items-center justify-center gap-1 rounded-full bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 pl-3 pr-3.5 text-sm shadow-lg active:translate-y-1">
className="inline-flex h-8 transform items-center justify-center gap-1 rounded-full bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 pl-3 pr-3.5 text-sm shadow-lg active:translate-y-1"
>
<ArrowUpIcon className="h-4 w-4" />
<span className="drop-shadow-md">Load newest</span>
</button>
@ -125,7 +127,13 @@ export default function Page() {
}
Page.getLayout = function getLayout(
page: string | number | boolean | ReactElement<unknown, string | JSXElementConstructor<unknown>> | ReactFragment | ReactPortal
page:
| string
| number
| boolean
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
| ReactFragment
| ReactPortal
) {
return (
<BaseLayout>

View File

@ -1,5 +1,5 @@
import BaseLayout from '@layouts/baseLayout';
import NewsFeedLayout from '@layouts/newsfeedLayout';
import BaseLayout from '@layouts/base';
import NewsFeedLayout from '@layouts/newsfeed';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal } from 'react';
@ -12,7 +12,13 @@ export default function Page() {
}
Page.getLayout = function getLayout(
page: string | number | boolean | ReactElement<unknown, string | JSXElementConstructor<unknown>> | ReactFragment | ReactPortal
page:
| string
| number
| boolean
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
| ReactFragment
| ReactPortal
) {
return (
<BaseLayout>

View File

@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import BaseLayout from '@layouts/baseLayout';
import OnboardingLayout from '@layouts/onboardingLayout';
import BaseLayout from '@layouts/base';
import OnboardingLayout from '@layouts/onboarding';
import { DatabaseContext } from '@components/contexts/database';
import { RelayContext } from '@components/contexts/relay';
@ -11,7 +10,16 @@ import { motion } from 'framer-motion';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { generatePrivateKey, getEventHash, getPublicKey, nip19, signEvent } from 'nostr-tools';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useCallback, useContext, useMemo, useState } from 'react';
import {
JSXElementConstructor,
ReactElement,
ReactFragment,
ReactPortal,
useCallback,
useContext,
useMemo,
useState,
} from 'react';
import { Config, names, uniqueNamesGenerator } from 'unique-names-generator';
const config: Config = {
@ -58,7 +66,9 @@ export default function Page() {
const insertDB = useCallback(async () => {
await db.execute(
`INSERT INTO accounts (id, privkey, npub, nsec, metadata) VALUES ("${pubKey}", "${privKey}", "${npub}", "${nsec}", '${JSON.stringify(data)}')`
`INSERT INTO accounts (id, privkey, npub, nsec, metadata) VALUES ("${pubKey}", "${privKey}", "${npub}", "${nsec}", '${JSON.stringify(
data
)}')`
);
}, [data, db, npub, nsec, privKey, pubKey]);
@ -101,12 +111,15 @@ export default function Page() {
<div>{/* spacer */}</div>
<motion.div layoutId="form">
<div className="mb-8 flex flex-col gap-3">
<motion.h1 layoutId="title" className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
<motion.h1
layoutId="title"
className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent"
>
Create new key
</motion.h1>
<motion.h2 layoutId="subtitle" className="w-3/4 text-zinc-400">
Lume will generate key with default profile for you, you can edit it later, and please store your key safely so you can restore your
account or use other client
Lume will generate key with default profile for you, you can edit it later, and please store your key safely
so you can restore your account or use other client
</motion.h2>
</div>
<div className="flex flex-col gap-4">
@ -131,7 +144,8 @@ export default function Page() {
/>
<button
onClick={() => showPrivateKey()}
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700">
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
>
{type === 'password' ? (
<EyeClosedIcon className="h-5 w-5 text-zinc-500 group-hover:text-zinc-200" />
) : (
@ -170,18 +184,25 @@ export default function Page() {
<motion.div layoutId="action" className="pb-5">
<div className="flex h-10 items-center">
{loading === true ? (
<svg className="h-5 w-5 animate-spin text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<svg
className="h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : (
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
<button
onClick={() => createAccount()}
className="transform rounded-lg border border-white/10 bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 px-3.5 py-2 font-medium shadow-input shadow-black/5 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-50 dark:shadow-black/10">
className="transform rounded-lg border border-white/10 bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 px-3.5 py-2 font-medium shadow-input shadow-black/5 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-50 dark:shadow-black/10"
>
<span className="drop-shadow-lg">Continue </span>
</button>
</div>
@ -193,7 +214,13 @@ export default function Page() {
}
Page.getLayout = function getLayout(
page: string | number | boolean | ReactElement<unknown, string | JSXElementConstructor<unknown>> | ReactFragment | ReactPortal
page:
| string
| number
| boolean
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
| ReactFragment
| ReactPortal
) {
return (
<BaseLayout>

View File

@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import BaseLayout from '@layouts/baseLayout';
import OnboardingLayout from '@layouts/onboardingLayout';
import BaseLayout from '@layouts/base';
import OnboardingLayout from '@layouts/onboarding';
import { DatabaseContext } from '@components/contexts/database';
@ -33,7 +32,9 @@ export default function Page() {
const insertDB = async () => {
// self follow
await db.execute(`INSERT INTO follows (pubkey, account, kind) VALUES ("${currentUser.pubkey}", "${currentUser.pubkey}", "0")`);
await db.execute(
`INSERT INTO follows (pubkey, account, kind) VALUES ("${currentUser.pubkey}", "${currentUser.pubkey}", "0")`
);
// follow selected
follow.forEach(async (npub) => {
const { data } = nip19.decode(npub);
@ -57,11 +58,15 @@ export default function Page() {
<div>{/* spacer */}</div>
<motion.div layoutId="form" className="flex flex-col">
<div className="mb-8 flex flex-col gap-3">
<motion.h1 layoutId="title" className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
<motion.h1
layoutId="title"
className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent"
>
Choose 10 people you want to following
</motion.h1>
<motion.h2 layoutId="subtitle" className="w-3/4 text-zinc-400">
For better experiences, you should follow the people you care about to personalize your newsfeed, otherwise you will be very bored
For better experiences, you should follow the people you care about to personalize your newsfeed, otherwise
you will be very bored
</motion.h2>
</div>
<div className="h-full w-full shrink">
@ -73,7 +78,8 @@ export default function Page() {
data-npub={item.npub}
className={`col-span-1 inline-flex cursor-pointer items-center gap-3 rounded-lg p-2 hover:bg-zinc-700 ${
follow.includes(item.npub) ? 'bg-zinc-800' : ''
}`}>
}`}
>
<div className="relative h-10 w-10 flex-shrink-0">
<Image className="rounded-full object-cover" src={item.avatar} alt={item.name} fill={true} />
</div>
@ -82,7 +88,9 @@ export default function Page() {
<p className="truncate text-sm font-medium text-zinc-200">{item.name}</p>
<p className="text-sm leading-tight text-zinc-500">{truncate(item.npub, 16, ' .... ')}</p>
</div>
<div>{follow.includes(item.npub) ? <CheckCircledIcon className="h-4 w-4 text-green-500" /> : <></>}</div>
<div>
{follow.includes(item.npub) ? <CheckCircledIcon className="h-4 w-4 text-green-500" /> : <></>}
</div>
</div>
</div>
))}
@ -92,19 +100,26 @@ export default function Page() {
<motion.div layoutId="action" className="pb-5">
<div className="flex h-10 items-center">
{loading === true ? (
<svg className="h-5 w-5 animate-spin text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<svg
className="h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : (
<div className="relative shrink-0 before:pointer-events-none before:absolute before:-inset-1 before:rounded-[11px] before:border before:border-blue-500 before:opacity-0 before:ring-2 before:ring-blue-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[7px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-blue-500/100 dark:focus-within:after:shadow-blue-500/20">
<button
onClick={() => createFollowing()}
disabled={follow.length < 10 ? true : false}
className="transform rounded-lg border border-white/10 bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 px-3.5 py-2 font-medium shadow-input shadow-black/5 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-50 dark:shadow-black/10">
className="transform rounded-lg border border-white/10 bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 px-3.5 py-2 font-medium shadow-input shadow-black/5 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-50 dark:shadow-black/10"
>
<span className="drop-shadow-lg">Finish </span>
</button>
</div>
@ -116,7 +131,13 @@ export default function Page() {
}
Page.getLayout = function getLayout(
page: string | number | boolean | ReactElement<unknown, string | JSXElementConstructor<unknown>> | ReactFragment | ReactPortal
page:
| string
| number
| boolean
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
| ReactFragment
| ReactPortal
) {
return (
<BaseLayout>

View File

@ -1,5 +1,5 @@
import BaseLayout from '@layouts/baseLayout';
import OnboardingLayout from '@layouts/onboardingLayout';
import BaseLayout from '@layouts/base';
import OnboardingLayout from '@layouts/onboarding';
import { motion } from 'framer-motion';
import Link from 'next/link';
@ -10,7 +10,10 @@ export default function Page() {
<div className="flex h-full flex-col justify-between px-8">
<div>{/* spacer */}</div>
<div className="flex flex-col gap-3">
<motion.h1 layoutId="title" className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
<motion.h1
layoutId="title"
className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent"
>
Other social network require email/password
<br />
nostr use{' '}
@ -19,19 +22,21 @@ export default function Page() {
</span>
</motion.h1>
<motion.h2 layoutId="subtitle" className="w-3/4 text-zinc-400">
If you have used nostr before, you can import your own private key. Otherwise, you can create a new key or use auto-generated account
created by system.
If you have used nostr before, you can import your own private key. Otherwise, you can create a new key or use
auto-generated account created by system.
</motion.h2>
<motion.div layoutId="form"></motion.div>
<motion.div layoutId="action" className="mt-4 flex gap-2">
<Link
href="/onboarding/create"
className="hover:bg-zinc-900/2.5 transform rounded-lg border border-black/5 bg-zinc-800 px-3.5 py-2 font-medium ring-1 ring-inset ring-zinc-900/10 hover:text-zinc-900 active:translate-y-1 dark:text-zinc-300 dark:ring-white/10 dark:hover:bg-zinc-700 dark:hover:text-white">
className="hover:bg-zinc-900/2.5 transform rounded-lg border border-black/5 bg-zinc-800 px-3.5 py-2 font-medium ring-1 ring-inset ring-zinc-900/10 hover:text-zinc-900 active:translate-y-1 dark:text-zinc-300 dark:ring-white/10 dark:hover:bg-zinc-700 dark:hover:text-white"
>
Create new key
</Link>
<Link
href="/onboarding/login"
className="hover:bg-zinc-900/2.5 transform rounded-lg border border-black/5 bg-zinc-800 px-3.5 py-2 font-medium ring-1 ring-inset ring-zinc-900/10 hover:text-zinc-900 active:translate-y-1 dark:text-zinc-300 dark:ring-white/10 dark:hover:bg-zinc-700 dark:hover:text-white">
className="hover:bg-zinc-900/2.5 transform rounded-lg border border-black/5 bg-zinc-800 px-3.5 py-2 font-medium ring-1 ring-inset ring-zinc-900/10 hover:text-zinc-900 active:translate-y-1 dark:text-zinc-300 dark:ring-white/10 dark:hover:bg-zinc-700 dark:hover:text-white"
>
Login with private key
</Link>
</motion.div>
@ -42,7 +47,13 @@ export default function Page() {
}
Page.getLayout = function getLayout(
page: string | number | boolean | ReactElement<unknown, string | JSXElementConstructor<unknown>> | ReactFragment | ReactPortal
page:
| string
| number
| boolean
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
| ReactFragment
| ReactPortal
) {
return (
<BaseLayout>

View File

@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import BaseLayout from '@layouts/baseLayout';
import OnboardingLayout from '@layouts/onboardingLayout';
import BaseLayout from '@layouts/base';
import OnboardingLayout from '@layouts/onboarding';
import { DatabaseContext } from '@components/contexts/database';
import { RelayContext } from '@components/contexts/relay';
@ -10,7 +9,16 @@ import { motion } from 'framer-motion';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { getPublicKey, nip19 } from 'nostr-tools';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useCallback, useContext, useMemo, useState } from 'react';
import {
JSXElementConstructor,
ReactElement,
ReactFragment,
ReactPortal,
useCallback,
useContext,
useMemo,
useState,
} from 'react';
export default function Page() {
const { db }: any = useContext(DatabaseContext);
@ -44,7 +52,9 @@ export default function Page() {
async (follows) => {
follows.forEach(async (item) => {
if (item) {
await db.execute(`INSERT OR IGNORE INTO follows (pubkey, account, kind) VALUES ("${item[1]}", "${pubkey}", "0")`);
await db.execute(
`INSERT OR IGNORE INTO follows (pubkey, account, kind) VALUES ("${item[1]}", "${pubkey}", "0")`
);
}
});
},
@ -80,28 +90,39 @@ export default function Page() {
<div>{/* spacer */}</div>
<motion.div layoutId="form">
<div className="mb-8 flex flex-col gap-3">
<motion.h1 layoutId="title" className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
<motion.h1
layoutId="title"
className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent"
>
Fetching your profile...
</motion.h1>
<motion.h2 layoutId="subtitle" className="w-3/4 text-zinc-400">
As long as you have private key, you alway can sync your profile and follows list on every nostr client, so please keep your key safely
As long as you have private key, you alway can sync your profile and follows list on every nostr client, so
please keep your key safely
</motion.h2>
</div>
</motion.div>
<motion.div layoutId="action" className="pb-5">
<div className="flex h-10 items-center">
{loading === true ? (
<svg className="h-5 w-5 animate-spin text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<svg
className="h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : (
<Link
href="/"
className="transform rounded-lg bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 px-3.5 py-2 font-medium active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30">
className="transform rounded-lg bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 px-3.5 py-2 font-medium active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
>
<span className="drop-shadow-lg">Finish</span>
</Link>
)}
@ -112,7 +133,13 @@ export default function Page() {
}
Page.getLayout = function getLayout(
page: string | number | boolean | ReactElement<unknown, string | JSXElementConstructor<unknown>> | ReactFragment | ReactPortal
page:
| string
| number
| boolean
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
| ReactFragment
| ReactPortal
) {
return (
<BaseLayout>

View File

@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import BaseLayout from '@layouts/baseLayout';
import OnboardingLayout from '@layouts/onboardingLayout';
import BaseLayout from '@layouts/base';
import OnboardingLayout from '@layouts/onboarding';
import { motion } from 'framer-motion';
import { useRouter } from 'next/router';
@ -60,12 +59,15 @@ export default function Page() {
<div>{/* spacer */}</div>
<motion.div layoutId="form">
<div className="mb-8 flex flex-col gap-3">
<motion.h1 layoutId="title" className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
<motion.h1
layoutId="title"
className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent"
>
Import your private key
</motion.h1>
<motion.h2 layoutId="subtitle" className="w-3/4 text-zinc-400">
You can import private key format as hex string or nsec. If you have installed Nostr Connect compality wallet in your mobile, you can
connect by scan QR Code below
You can import private key format as hex string or nsec. If you have installed Nostr Connect compality
wallet in your mobile, you can connect by scan QR Code below
</motion.h2>
</div>
<div className="flex flex-col gap-2">
@ -83,18 +85,25 @@ export default function Page() {
<motion.div layoutId="action" className="pb-5">
<div className="flex h-10 items-center">
{isSubmitting ? (
<svg className="h-5 w-5 animate-spin text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<svg
className="h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : (
<button
type="submit"
disabled={!isDirty || !isValid}
className="transform rounded-lg bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 px-3.5 py-2 font-medium active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30">
className="transform rounded-lg bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 px-3.5 py-2 font-medium active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
>
<span className="drop-shadow-lg">Continue </span>
</button>
)}
@ -105,7 +114,13 @@ export default function Page() {
}
Page.getLayout = function getLayout(
page: string | number | boolean | ReactElement<unknown, string | JSXElementConstructor<unknown>> | ReactFragment | ReactPortal
page:
| string
| number
| boolean
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
| ReactFragment
| ReactPortal
) {
return (
<BaseLayout>

View File

@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import BaseLayout from '@layouts/baseLayout';
import BaseLayout from '@layouts/base';
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal } from 'react';

View File

@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import BaseLayout from '@layouts/baseLayout';
import UserLayout from '@layouts/userLayout';
import BaseLayout from '@layouts/base';
import UserLayout from '@layouts/user';
import { RelayContext } from '@components/contexts/relay';
@ -32,7 +31,8 @@ export default function Page() {
const [loading, setLoading] = useState(false);
const [currentUser]: any = useLocalStorage('current-user');
const profile = currentUser.metadata !== undefined ? JSON.parse(currentUser.metadata) : { display_name: null, username: null };
const profile =
currentUser.metadata !== undefined ? JSON.parse(currentUser.metadata) : { display_name: null, username: null };
const {
register,
@ -79,9 +79,12 @@ export default function Page() {
return (
<form onSubmit={handleSubmit(onSubmit)} className="flex h-full w-full flex-col justify-between px-6">
<div className="mb-8 flex flex-col gap-3 pt-8">
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">Update profile</h1>
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
Update profile
</h1>
<h2 className="w-3/4 text-zinc-400">
Your profile will be published to all relays, as long as you have the private key, you always can recover your profile in any client
Your profile will be published to all relays, as long as you have the private key, you always can recover your
profile in any client
</h2>
</div>
<fieldset className="flex flex-col gap-2">
@ -179,18 +182,25 @@ export default function Page() {
<div className="pb-5">
<div className="flex h-10 items-center">
{loading === true ? (
<svg className="h-5 w-5 animate-spin text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<svg
className="h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : (
<button
type="submit"
disabled={!isDirty || !isValid}
className="transform rounded-lg bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 px-3.5 py-2 font-medium active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30">
className="transform rounded-lg bg-[radial-gradient(ellipse_at_bottom_right,_var(--tw-gradient-stops))] from-gray-300 via-fuchsia-600 to-orange-600 px-3.5 py-2 font-medium active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
>
<span className="drop-shadow-lg">Update</span>
</button>
)}
@ -201,7 +211,13 @@ export default function Page() {
}
Page.getLayout = function getLayout(
page: string | number | boolean | ReactElement<unknown, string | JSXElementConstructor<unknown>> | ReactFragment | ReactPortal
page:
| string
| number
| boolean
| ReactElement<unknown, string | JSXElementConstructor<unknown>>
| ReactFragment
| ReactPortal
) {
return (
<BaseLayout>

View File

@ -1,3 +1,4 @@
// get X days ago with user provided date
export const daysAgo = (numOfDays, date = new Date()) => {
const daysAgo = new Date(date.getTime());
daysAgo.setDate(date.getDate() - numOfDays);
@ -5,6 +6,7 @@ export const daysAgo = (numOfDays, date = new Date()) => {
return daysAgo;
};
// get X hours ago with user provided date
export const hoursAgo = (numOfHours, date = new Date()) => {
const hoursAgo = new Date(date.getTime());
hoursAgo.setHours(date.getHours() - numOfHours);
@ -12,6 +14,7 @@ export const hoursAgo = (numOfHours, date = new Date()) => {
return hoursAgo;
};
// convert date to unix timestamp
export const dateToUnix = (_date?: Date) => {
const date = _date || new Date();