mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-18 11:13:30 +00:00
migrated to vite and vite-plugin-ssr
This commit is contained in:
parent
c50e507c14
commit
b9bafc851e
@ -15,6 +15,7 @@
|
||||
"^@stores/(.*)$",
|
||||
"^@utils/(.*)$",
|
||||
"^@assets/(.*)$",
|
||||
"^@renderer/(.*)$",
|
||||
"<THIRD_PARTY_MODULES>",
|
||||
"^[./]"
|
||||
],
|
||||
|
@ -2,8 +2,8 @@
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"beforeBuildCommand": "pnpm build",
|
||||
"devPath": "http://localhost:1420",
|
||||
"distDir": "../out",
|
||||
"devPath": "http://localhost:3000",
|
||||
"distDir": "../dist",
|
||||
"withGlobalTauri": true
|
||||
},
|
||||
"package": {
|
||||
|
@ -1,13 +0,0 @@
|
||||
import '@assets/global.css';
|
||||
|
||||
import { Providers } from './providers';
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" className="dark">
|
||||
<body className="cursor-default select-none overflow-hidden font-sans antialiased">
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<p className="text-sm text-zinc-400">Sorry, this feature under development, it will come in the next version</p>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const id = searchParams.get('event-id');
|
||||
|
||||
return <div className="scrollbar-hide flex h-full flex-col gap-2 overflow-y-auto py-3">{id}</div>;
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import ProfileFollowers from '@components/profile/followers';
|
||||
import ProfileFollows from '@components/profile/follows';
|
||||
import ProfileMetadata from '@components/profile/metadata';
|
||||
import ProfileNotes from '@components/profile/notes';
|
||||
|
||||
import * as Tabs from '@radix-ui/react-tabs';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const pubkey = searchParams.get('pubkey');
|
||||
|
||||
return (
|
||||
<div className="scrollbar-hide h-full w-full overflow-y-auto">
|
||||
<ProfileMetadata id={pubkey} />
|
||||
<Tabs.Root className="flex w-full flex-col" defaultValue="notes">
|
||||
<Tabs.List className="flex border-b border-zinc-800">
|
||||
<Tabs.Trigger
|
||||
className="flex h-10 flex-1 cursor-default select-none items-center justify-center px-5 text-sm font-medium leading-none text-zinc-400 outline-none hover:text-fuchsia-400 data-[state=active]:text-fuchsia-500 data-[state=active]:shadow-[inset_0_-1px_0_0,0_1px_0_0] data-[state=active]:shadow-current"
|
||||
value="notes"
|
||||
>
|
||||
Notes
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
className="flex h-10 flex-1 cursor-default select-none items-center justify-center px-5 text-sm font-medium text-zinc-400 outline-none placeholder:leading-none hover:text-fuchsia-400 data-[state=active]:text-fuchsia-500 data-[state=active]:shadow-[inset_0_-1px_0_0,0_1px_0_0] data-[state=active]:shadow-current"
|
||||
value="followers"
|
||||
>
|
||||
Followers
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
className="flex h-10 flex-1 cursor-default select-none items-center justify-center px-5 text-sm font-medium leading-none text-zinc-400 outline-none hover:text-fuchsia-400 data-[state=active]:text-fuchsia-500 data-[state=active]:shadow-[inset_0_-1px_0_0,0_1px_0_0] data-[state=active]:shadow-current"
|
||||
value="following"
|
||||
>
|
||||
Following
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="notes">
|
||||
<ProfileNotes id={pubkey} />
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="followers">
|
||||
<ProfileFollowers id={pubkey} />
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="following">
|
||||
<ProfileFollows id={pubkey} />
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,169 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { createAccount } from '@utils/storage';
|
||||
|
||||
import { EyeClose, EyeEmpty } from 'iconoir-react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { generatePrivateKey, getEventHash, getPublicKey, nip19, signEvent } from 'nostr-tools';
|
||||
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { Config, names, uniqueNamesGenerator } from 'unique-names-generator';
|
||||
|
||||
const config: Config = {
|
||||
dictionaries: [names],
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
|
||||
const [type, setType] = useState('password');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const privkey = useMemo(() => generatePrivateKey(), []);
|
||||
const name = useMemo(() => uniqueNamesGenerator(config).toString(), []);
|
||||
|
||||
const pubkey = getPublicKey(privkey);
|
||||
const npub = nip19.npubEncode(pubkey);
|
||||
const nsec = nip19.nsecEncode(privkey);
|
||||
|
||||
// auto-generated profile metadata
|
||||
const metadata: any = useMemo(
|
||||
() => ({
|
||||
display_name: name,
|
||||
name: name,
|
||||
username: name.toLowerCase(),
|
||||
picture: 'https://void.cat/d/KmypFh2fBdYCEvyJrPiN89.webp',
|
||||
}),
|
||||
[name]
|
||||
);
|
||||
|
||||
// toggle privatek key
|
||||
const showPrivateKey = () => {
|
||||
if (type === 'password') {
|
||||
setType('text');
|
||||
} else {
|
||||
setType('password');
|
||||
}
|
||||
};
|
||||
|
||||
// create account and broadcast to all relays
|
||||
const submit = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
||||
// build event
|
||||
const event: any = {
|
||||
content: JSON.stringify(metadata),
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 0,
|
||||
pubkey: pubkey,
|
||||
tags: [],
|
||||
};
|
||||
event.id = getEventHash(event);
|
||||
event.sig = signEvent(event, privkey);
|
||||
// insert to database
|
||||
createAccount(pubkey, privkey, metadata);
|
||||
// broadcast
|
||||
pool.publish(event, relays);
|
||||
// redirect to next step
|
||||
router.push(`/onboarding/create/step-2?pubkey=${pubkey}&privkey=${privkey}`, { forceOptimisticNavigation: true });
|
||||
}, [pool, pubkey, privkey, metadata, relays, router]);
|
||||
|
||||
return (
|
||||
<div className="grid h-full w-full grid-rows-5">
|
||||
<div className="row-span-1 mx-auto flex w-full max-w-md items-center justify-center">
|
||||
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
|
||||
Create new account
|
||||
</h1>
|
||||
</div>
|
||||
<div className="row-span-4">
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-8 flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-sm font-semibold text-zinc-400">Public Key</label>
|
||||
<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">
|
||||
<input
|
||||
readOnly
|
||||
value={npub}
|
||||
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2.5 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-sm font-semibold text-zinc-400">Private Key</label>
|
||||
<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">
|
||||
<input
|
||||
readOnly
|
||||
type={type}
|
||||
value={nsec}
|
||||
className="relative w-full rounded-lg border border-black/5 py-2.5 pl-3.5 pr-11 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600"
|
||||
/>
|
||||
<button
|
||||
onClick={() => showPrivateKey()}
|
||||
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
|
||||
>
|
||||
{type === 'password' ? (
|
||||
<EyeClose width={20} height={20} className="text-zinc-500 group-hover:text-zinc-200" />
|
||||
) : (
|
||||
<EyeEmpty width={20} height={20} className="text-zinc-500 group-hover:text-zinc-200" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-sm font-semibold text-zinc-400">Default Profile (you can change it later)</label>
|
||||
<div className="relative w-full 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">
|
||||
<div className="relative w-full rounded-lg border border-black/5 px-3.5 py-4 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600">
|
||||
<div className="flex space-x-2">
|
||||
<div className="relative h-11 w-11 rounded-md">
|
||||
<Image className="inline-block rounded-md" src={metadata.picture} alt="" fill={true} />
|
||||
</div>
|
||||
<div className="flex-1 space-y-2 py-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="font-semibold">{metadata.display_name}</p>
|
||||
<p className="text-zinc-400">@{metadata.username}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-2 h-2 rounded bg-zinc-700"></div>
|
||||
<div className="col-span-1 h-2 rounded bg-zinc-700"></div>
|
||||
</div>
|
||||
<div className="h-2 rounded bg-zinc-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-10 items-center justify-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"
|
||||
>
|
||||
<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>
|
||||
</svg>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => submit()}
|
||||
className="w-full transform rounded-lg bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 px-3.5 py-2.5 font-medium text-zinc-800 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
>
|
||||
<span className="drop-shadow-lg">Continue →</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { CableTag } from 'iconoir-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { getPublicKey, nip19 } from 'nostr-tools';
|
||||
import { Resolver, useForm } from 'react-hook-form';
|
||||
|
||||
type FormValues = {
|
||||
key: string;
|
||||
};
|
||||
|
||||
const resolver: Resolver<FormValues> = async (values) => {
|
||||
return {
|
||||
values: values.key ? values : {},
|
||||
errors: !values.key
|
||||
? {
|
||||
key: {
|
||||
type: 'required',
|
||||
message: 'This is required.',
|
||||
},
|
||||
}
|
||||
: {},
|
||||
};
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const {
|
||||
register,
|
||||
setError,
|
||||
handleSubmit,
|
||||
formState: { errors, isDirty, isValid, isSubmitting },
|
||||
} = useForm<FormValues>({ resolver });
|
||||
|
||||
const onSubmit = async (data: any) => {
|
||||
try {
|
||||
let privkey = data['key'];
|
||||
|
||||
if (privkey.substring(0, 4) === 'nsec') {
|
||||
privkey = nip19.decode(privkey).data;
|
||||
}
|
||||
if (typeof getPublicKey(privkey) === 'string') {
|
||||
router.push(`/onboarding/login/step-2?privkey=${privkey}`, { forceOptimisticNavigation: true });
|
||||
}
|
||||
} catch (error) {
|
||||
setError('key', {
|
||||
type: 'custom',
|
||||
message: 'Private Key is invalid, please check again',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid h-full w-full grid-rows-5">
|
||||
<div className="row-span-1 mx-auto flex w-full max-w-md items-center justify-center">
|
||||
<h1 className="bg-gradient-to-br from-zinc-200 via-white to-zinc-300 bg-clip-text text-3xl font-semibold text-transparent">
|
||||
Login with Private Key
|
||||
</h1>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="row-span-4">
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
{/* #TODO: add function */}
|
||||
<button className="inline-flex w-full transform items-center justify-center gap-1.5 rounded-lg bg-zinc-700 px-3.5 py-2.5 font-medium text-zinc-200 shadow-input ring-1 ring-zinc-600 active:translate-y-1">
|
||||
{/* #TODO: change to nostr connect logo */}
|
||||
<CableTag width={20} height={20} className="text-fuchsia-500" />
|
||||
<span>Continue with Nostr Connect</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-zinc-800"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span className="bg-black px-2 text-sm text-zinc-500">or</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<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">
|
||||
<input
|
||||
{...register('key', { required: true, minLength: 32 })}
|
||||
type={'password'}
|
||||
placeholder="Paste private key here..."
|
||||
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2.5 text-center shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-red-400">{errors.key && <p>{errors.key.message}</p>}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex h-10 items-center justify-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"
|
||||
>
|
||||
<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>
|
||||
</svg>
|
||||
) : (
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isDirty || !isValid}
|
||||
className="w-full transform rounded-lg bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 px-3.5 py-2.5 font-medium text-zinc-800 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
>
|
||||
<span className="drop-shadow-lg">Continue →</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,145 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { fetchProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
||||
import { shortenKey } from '@utils/shortenKey';
|
||||
import { createAccount, createPleb, updateAccount } from '@utils/storage';
|
||||
import { nip02ToArray } from '@utils/transform';
|
||||
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { getPublicKey } from 'nostr-tools';
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const privkey = searchParams.get('privkey');
|
||||
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
const [profile, setProfile] = useState({ metadata: null });
|
||||
const [done, setDone] = useState(false);
|
||||
const timeout = useRef(null);
|
||||
|
||||
const pubkey = getPublicKey(privkey);
|
||||
|
||||
const createPlebs = useCallback(async (tags: string[]) => {
|
||||
for (const tag of tags) {
|
||||
fetchProfileMetadata(tag[1])
|
||||
.then((res: any) => createPleb(tag[1], res.content))
|
||||
.catch(console.error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = pool.subscribe(
|
||||
[
|
||||
{
|
||||
kinds: [0, 3],
|
||||
authors: [pubkey],
|
||||
},
|
||||
],
|
||||
relays,
|
||||
(event: any) => {
|
||||
if (event.kind === 0) {
|
||||
// create account
|
||||
createAccount(pubkey, privkey, event.content);
|
||||
// update state
|
||||
setProfile({
|
||||
metadata: JSON.parse(event.content),
|
||||
});
|
||||
} else {
|
||||
if (event.tags.length > 0) {
|
||||
createPlebs(event.tags);
|
||||
const arr = nip02ToArray(event.tags);
|
||||
// update account's folllows with NIP-02 tag list
|
||||
updateAccount('follows', arr, pubkey);
|
||||
}
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
() => {
|
||||
timeout.current = setTimeout(() => setDone(true), 5000);
|
||||
},
|
||||
{
|
||||
unsubscribeOnEose: true,
|
||||
logAllEvents: false,
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
clearTimeout(timeout.current);
|
||||
};
|
||||
}, [pool, relays, pubkey, privkey, createPlebs]);
|
||||
|
||||
return (
|
||||
<div className="grid h-full w-full grid-rows-5">
|
||||
<div className="row-span-1 flex items-center justify-center">
|
||||
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
|
||||
Bringing back your profile...
|
||||
</h1>
|
||||
</div>
|
||||
<div className="row-span-4 flex flex-col gap-8">
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-4 flex flex-col gap-2">
|
||||
<div className="w-full rounded-lg bg-zinc-900 p-4 shadow-input ring-1 ring-zinc-800">
|
||||
<div className="flex space-x-4">
|
||||
<div className="relative h-10 w-10 rounded-full">
|
||||
<Image
|
||||
className="inline-block rounded-full"
|
||||
src={profile.metadata?.picture || DEFAULT_AVATAR}
|
||||
alt=""
|
||||
fill={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 space-y-4 py-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-semibold">{profile.metadata?.display_name || profile.metadata?.name}</p>
|
||||
<span className="leading-tight text-zinc-500">·</span>
|
||||
<p className="text-zinc-500">@{profile.metadata?.username || (pubkey && shortenKey(pubkey))}</p>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-2 h-2 rounded bg-zinc-700"></div>
|
||||
<div className="col-span-1 h-2 rounded bg-zinc-700"></div>
|
||||
</div>
|
||||
<div className="h-2 rounded bg-zinc-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
{done === false ? (
|
||||
<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>
|
||||
</svg>
|
||||
) : (
|
||||
<Link
|
||||
prefetch={false}
|
||||
href="/"
|
||||
className="inline-flex w-full transform items-center justify-center rounded-lg bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 px-3.5 py-2.5 font-medium text-zinc-800 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
>
|
||||
<span className="drop-shadow-lg">Done! Go to newsfeed</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const RelayProvider = dynamic(() => import('@components/relaysProvider'), { ssr: false });
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return <RelayProvider>{children}</RelayProvider>;
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useSelectedLayoutSegments } from 'next/navigation';
|
||||
|
||||
export const ActiveLink = ({
|
||||
href,
|
||||
className,
|
||||
activeClassName,
|
||||
children,
|
||||
}: {
|
||||
href: string;
|
||||
className: string;
|
||||
activeClassName: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const segments = useSelectedLayoutSegments();
|
||||
const isActive = href.includes(segments[1]);
|
||||
|
||||
return (
|
||||
<Link prefetch={false} href={href} className={`${className}` + ' ' + (isActive ? `${activeClassName}` : '')}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
@ -1,24 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { platform } from '@tauri-apps/api/os';
|
||||
import { ArrowLeft, ArrowRight, Refresh } from 'iconoir-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
|
||||
export default function AppActions() {
|
||||
const router = useRouter();
|
||||
const [os, setOS] = useState('');
|
||||
|
||||
const goBack = () => {
|
||||
router.back();
|
||||
window.history.back();
|
||||
};
|
||||
|
||||
const goForward = () => {
|
||||
router.forward();
|
||||
window.history.forward();
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
router.refresh();
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
|
@ -1,12 +1,5 @@
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const AppActions = dynamic(() => import('@components/appHeader/actions'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const EventCollector = dynamic(() => import('@components/eventCollector'), {
|
||||
ssr: false,
|
||||
});
|
||||
import AppActions from '@components/appHeader/actions';
|
||||
import EventCollector from '@components/eventCollector';
|
||||
|
||||
export default function AppHeader({ collector }: { collector: boolean }) {
|
||||
return (
|
||||
|
@ -10,19 +10,6 @@ export default function ChannelList() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-px">
|
||||
{/*
|
||||
<Link
|
||||
href="/explore/channels"
|
||||
className="group inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-900"
|
||||
>
|
||||
<div className="inline-flex h-5 w-5 shrink items-center justify-center rounded bg-zinc-900 group-hover:bg-zinc-800">
|
||||
<Globe width={12} height={12} className="text-zinc-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-zinc-500 group-hover:text-zinc-400">Browse channels</h5>
|
||||
</div>
|
||||
</Link>
|
||||
*/}
|
||||
{list.map((item) => (
|
||||
<ChannelListItem key={item.event_id} data={item} />
|
||||
))}
|
||||
|
@ -1,31 +1,21 @@
|
||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { useChannelMetadata } from '@utils/hooks/useChannelMetadata';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
export const ChannelListItem = ({ data }: { data: any }) => {
|
||||
const channel = useChannelMetadata(data.event_id, data.metadata);
|
||||
|
||||
return (
|
||||
<Link
|
||||
prefetch={false}
|
||||
href={`/nostr/channel?channel-id=${data.event_id}`}
|
||||
<a
|
||||
href={`channel?id=${data.event_id}`}
|
||||
className="inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-900"
|
||||
>
|
||||
<div className="relative h-5 w-5 shrink-0 overflow-hidden rounded">
|
||||
<ImageWithFallback
|
||||
src={channel?.picture || DEFAULT_AVATAR}
|
||||
alt={data.event_id}
|
||||
fill={true}
|
||||
className="rounded object-cover"
|
||||
/>
|
||||
<div className="relative h-5 w-5 shrink-0 rounded">
|
||||
<img src={channel?.picture || DEFAULT_AVATAR} alt={data.event_id} className="h-5 w-5 rounded object-cover" />
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="truncate text-sm font-medium text-zinc-400">{channel?.name.toLowerCase()}</h5>
|
||||
</div>
|
||||
</Link>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
@ -1,13 +1,11 @@
|
||||
import { ChatListItem } from '@components/chats/chatListItem';
|
||||
import { ChatModal } from '@components/chats/chatModal';
|
||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { getChats } from '@utils/storage';
|
||||
|
||||
import useLocalStorage from '@rehooks/local-storage';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function ChatList() {
|
||||
@ -33,17 +31,15 @@ export default function ChatList() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-px">
|
||||
<Link
|
||||
prefetch={false}
|
||||
href={`/nostr/chat?pubkey=${activeAccount.pubkey}`}
|
||||
<a
|
||||
href={`/chat?pubkey=${activeAccount.pubkey}`}
|
||||
className="inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-900"
|
||||
>
|
||||
<div className="relative h-5 w-5 shrink overflow-hidden rounded bg-white">
|
||||
<ImageWithFallback
|
||||
<div className="relative h-5 w-5 shrink rounded bg-white">
|
||||
<img
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={activeAccount.pubkey}
|
||||
fill={true}
|
||||
className="rounded object-cover"
|
||||
className="h-5 w-5 rounded object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@ -51,7 +47,7 @@ export default function ChatList() {
|
||||
{profile?.display_name || profile?.name} <span className="text-zinc-500">(you)</span>
|
||||
</h5>
|
||||
</div>
|
||||
</Link>
|
||||
</a>
|
||||
{list.map((item) => (
|
||||
<ChatListItem key={item.id} pubkey={item.pubkey} />
|
||||
))}
|
||||
|
@ -1,34 +1,24 @@
|
||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
||||
import { shortenKey } from '@utils/shortenKey';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
export const ChatListItem = ({ pubkey }: { pubkey: string }) => {
|
||||
const profile = useProfileMetadata(pubkey);
|
||||
|
||||
return (
|
||||
<Link
|
||||
prefetch={false}
|
||||
href={`/nostr/chat?pubkey=${pubkey}`}
|
||||
<a
|
||||
href={`/chat?pubkey=${pubkey}`}
|
||||
className="inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-zinc-900"
|
||||
>
|
||||
<div className="relative h-5 w-5 shrink overflow-hidden rounded">
|
||||
<ImageWithFallback
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
fill={true}
|
||||
className="rounded object-cover"
|
||||
/>
|
||||
<div className="relative h-5 w-5 shrink rounded">
|
||||
<img src={profile?.picture || DEFAULT_AVATAR} alt={pubkey} className="h-5 w-5 rounded object-cover" />
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-zinc-400">
|
||||
{profile?.display_name || profile?.name || shortenKey(pubkey)}
|
||||
</h5>
|
||||
</div>
|
||||
</Link>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
@ -1,28 +1,24 @@
|
||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { shortenKey } from '@utils/shortenKey';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { navigate } from 'vite-plugin-ssr/client/router';
|
||||
|
||||
export const ChatModalUser = ({ data }: { data: any }) => {
|
||||
const router = useRouter();
|
||||
const profile = JSON.parse(data.metadata);
|
||||
|
||||
const openNewChat = () => {
|
||||
router.push(`/nostr/chat?pubkey=${data.pubkey}`, { forceOptimisticNavigation: true });
|
||||
navigate(`/chat?pubkey=${data.pubkey}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="group flex items-center justify-between px-3 py-2 hover:bg-zinc-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative h-10 w-10 shrink overflow-hidden rounded-md">
|
||||
<ImageWithFallback
|
||||
<div className="relative h-10 w-10 shrink-0 rounded-md">
|
||||
<img
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={data.pubkey}
|
||||
fill={true}
|
||||
className="rounded-md object-cover"
|
||||
className="h-10 w-10 rounded-md object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-1 flex-col items-start text-start">
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
||||
@ -15,13 +13,8 @@ export const MessageUser = ({ pubkey, time }: { pubkey: string; time: number })
|
||||
|
||||
return (
|
||||
<div className="group flex items-start gap-3">
|
||||
<div className="relative h-9 w-9 shrink overflow-hidden rounded-md">
|
||||
<ImageWithFallback
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
fill={true}
|
||||
className="rounded-md object-cover"
|
||||
/>
|
||||
<div className="relative h-9 w-9 shrink rounded-md">
|
||||
<img src={profile?.picture || DEFAULT_AVATAR} alt={pubkey} className="h-9 w-9 rounded-md object-cover" />
|
||||
</div>
|
||||
<div className="flex w-full flex-1 items-start justify-between">
|
||||
<div className="flex items-baseline gap-2 text-sm">
|
||||
|
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import { NetworkStatusIndicator } from '@components/networkStatusIndicator';
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
|
@ -1,36 +0,0 @@
|
||||
import { noteContentAtom } from '@stores/note';
|
||||
|
||||
import data from '@emoji-mart/data';
|
||||
import Picker from '@emoji-mart/react';
|
||||
import * as Popover from '@radix-ui/react-popover';
|
||||
import { Emoji } from 'iconoir-react';
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
export default function EmojiPicker() {
|
||||
const [value, setValue] = useAtom(noteContentAtom);
|
||||
|
||||
return (
|
||||
<Popover.Root>
|
||||
<Popover.Trigger asChild>
|
||||
<button className="inline-flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-zinc-700">
|
||||
<Emoji width={16} height={16} className="text-zinc-400" />
|
||||
</button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
className="rounded-md will-change-[transform,opacity] data-[state=open]:data-[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-slideRightAndFade data-[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-[side=top]:animate-slideDownAndFade"
|
||||
sideOffset={5}
|
||||
>
|
||||
<Picker
|
||||
data={data}
|
||||
emojiSize={16}
|
||||
navPosition={'none'}
|
||||
skinTonePosition={'none'}
|
||||
onEmojiSelect={(res) => setValue(value + ' ' + res.native)}
|
||||
/>
|
||||
<Popover.Arrow className="fill-[#141516]" />
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
);
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
|
||||
export const ImageWithFallback = memo(function ImageWithFallback({
|
||||
src,
|
||||
alt,
|
||||
fill,
|
||||
className,
|
||||
}: {
|
||||
src: string;
|
||||
alt: string;
|
||||
fill: boolean;
|
||||
className: string;
|
||||
}) {
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
setError(null);
|
||||
}, [src]);
|
||||
|
||||
return (
|
||||
<Image src={error ? DEFAULT_AVATAR : src} alt={alt} fill={fill} className={className} onError={setError} priority />
|
||||
);
|
||||
});
|
@ -2,7 +2,7 @@ import AppHeader from '@components/appHeader';
|
||||
import MultiAccounts from '@components/multiAccounts';
|
||||
import Navigation from '@components/navigation';
|
||||
|
||||
export default function NostrLayout({ children }: { children: React.ReactNode }) {
|
||||
export default function NewsfeedLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white">
|
||||
<div className="flex h-screen w-full flex-col">
|
12
src/components/link.tsx
Normal file
12
src/components/link.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { usePageContext } from '@utils/hooks/usePageContext';
|
||||
|
||||
import { AnchorHTMLAttributes, ClassAttributes } from 'react';
|
||||
|
||||
export function Link(
|
||||
props: JSX.IntrinsicAttributes & ClassAttributes<HTMLAnchorElement> & AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||
activeClass: string
|
||||
) {
|
||||
const pageContext = usePageContext();
|
||||
const className = [props.className, pageContext.urlPathname === props.href && activeClass].filter(Boolean).join(' ');
|
||||
return <a {...props} className={className} />;
|
||||
}
|
@ -1,20 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import { writeText } from '@tauri-apps/api/clipboard';
|
||||
import { LogOut, ProfileCircle, Settings } from 'iconoir-react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { navigate } from 'vite-plugin-ssr/client/router';
|
||||
|
||||
export const ActiveAccount = ({ user }: { user: any }) => {
|
||||
const router = useRouter();
|
||||
const userData = JSON.parse(user.metadata);
|
||||
|
||||
const openProfilePage = () => {
|
||||
router.push(`/nostr/user?pubkey=${user.pubkey}`, { forceOptimisticNavigation: true });
|
||||
navigate(`/user?pubkey=${user.pubkey}`);
|
||||
};
|
||||
|
||||
const copyPublicKey = async () => {
|
||||
@ -25,12 +21,10 @@ export const ActiveAccount = ({ user }: { user: any }) => {
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button className="relative h-11 w-11 rounded-lg">
|
||||
<Image
|
||||
<img
|
||||
src={userData.picture || DEFAULT_AVATAR}
|
||||
alt="user's avatar"
|
||||
fill={true}
|
||||
className="rounded-lg object-cover"
|
||||
priority
|
||||
className="h-11 w-11 rounded-lg object-cover"
|
||||
/>
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const InactiveAccount = memo(function InactiveAccount({ user }: { user: any }) {
|
||||
@ -12,13 +11,7 @@ export const InactiveAccount = memo(function InactiveAccount({ user }: { user: a
|
||||
|
||||
return (
|
||||
<button onClick={() => setCurrentUser()} className="relative h-11 w-11 shrink rounded-lg">
|
||||
<Image
|
||||
src={userData.picture || DEFAULT_AVATAR}
|
||||
alt="user's avatar"
|
||||
fill={true}
|
||||
className="rounded-lg object-cover"
|
||||
priority
|
||||
/>
|
||||
<img src={userData.picture || DEFAULT_AVATAR} alt="user's avatar" className="h-11 w-11 rounded-lg object-cover" />
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import { ActiveAccount } from '@components/multiAccounts/activeAccount';
|
||||
import { InactiveAccount } from '@components/multiAccounts/inactiveAccount';
|
||||
|
||||
@ -11,7 +9,6 @@ import LumeSymbol from '@assets/icons/Lume';
|
||||
|
||||
import useLocalStorage from '@rehooks/local-storage';
|
||||
import { Plus } from 'iconoir-react';
|
||||
import Link from 'next/link';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export default function MultiAccounts() {
|
||||
@ -38,21 +35,19 @@ export default function MultiAccounts() {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-between px-2 pb-4 pt-3">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Link
|
||||
prefetch={false}
|
||||
<a
|
||||
href="/explore"
|
||||
className="group relative flex h-11 w-11 shrink cursor-pointer items-center justify-center rounded-lg bg-zinc-900 hover:bg-zinc-800"
|
||||
>
|
||||
<LumeSymbol className="h-6 w-auto text-zinc-400 group-hover:text-zinc-200" />
|
||||
</Link>
|
||||
</a>
|
||||
<div>{users.map((user) => renderAccount(user))}</div>
|
||||
<Link
|
||||
prefetch={false}
|
||||
<a
|
||||
href="/onboarding"
|
||||
className="group relative flex h-11 w-11 shrink cursor-pointer items-center justify-center rounded-lg border-2 border-dashed border-zinc-600 hover:border-zinc-400"
|
||||
>
|
||||
<Plus width={16} height={16} className="text-zinc-400 group-hover:text-zinc-200" />
|
||||
</Link>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 text-center">
|
||||
<span className="animate-moveBg from-fuchsia-300 via-orange-100 to-amber-300 text-sm font-black uppercase leading-tight text-zinc-600 hover:bg-gradient-to-r hover:bg-clip-text hover:text-transparent">
|
||||
|
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import ChannelList from '@components/channels/channelList';
|
||||
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
|
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import ChatList from '@components/chats/chatList';
|
||||
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
|
@ -1,7 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import { ActiveLink } from '@components/activeLink';
|
||||
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { Bonfire, NavArrowUp, PeopleTag } from 'iconoir-react';
|
||||
import { useState } from 'react';
|
||||
@ -23,22 +19,22 @@ export default function Newsfeed() {
|
||||
<h3 className="text-[11px] font-bold uppercase tracking-widest text-zinc-600">Newsfeed</h3>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content className="flex flex-col text-zinc-400">
|
||||
<ActiveLink
|
||||
href="/nostr/newsfeed/following"
|
||||
activeClassName="dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800"
|
||||
<a
|
||||
href="/newsfeed/following"
|
||||
//activeClassName="dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800"
|
||||
className="flex h-8 items-center gap-2.5 rounded-md px-2.5 text-sm font-medium hover:text-zinc-200"
|
||||
>
|
||||
<PeopleTag width={16} height={16} className="text-zinc-500" />
|
||||
<span>Following</span>
|
||||
</ActiveLink>
|
||||
<ActiveLink
|
||||
href="/nostr/newsfeed/circle"
|
||||
activeClassName="dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800"
|
||||
</a>
|
||||
<a
|
||||
href="/newsfeed/circle"
|
||||
//activeClassName="dark:bg-zinc-900 dark:text-zinc-100 hover:dark:bg-zinc-800"
|
||||
className="flex h-8 items-center gap-2.5 rounded-md px-2.5 text-sm font-medium hover:text-zinc-200"
|
||||
>
|
||||
<Bonfire width={16} height={16} className="text-zinc-500" />
|
||||
<span>Circle</span>
|
||||
</ActiveLink>
|
||||
</a>
|
||||
</Collapsible.Content>
|
||||
</div>
|
||||
</Collapsible.Root>
|
||||
|
@ -4,11 +4,10 @@ import { UserExtend } from '@components/user/extend';
|
||||
|
||||
import { contentParser } from '@utils/parser';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { memo } from 'react';
|
||||
import { navigate } from 'vite-plugin-ssr/client/router';
|
||||
|
||||
export const NoteBase = memo(function NoteBase({ event }: { event: any }) {
|
||||
const router = useRouter();
|
||||
const content = contentParser(event.content, event.tags);
|
||||
|
||||
const parentNote = () => {
|
||||
@ -22,13 +21,13 @@ export const NoteBase = memo(function NoteBase({ event }: { event: any }) {
|
||||
|
||||
const openUserPage = (e) => {
|
||||
e.stopPropagation();
|
||||
router.push(`/nostr/user?pubkey=${event.pubkey}`, { forceOptimisticNavigation: true });
|
||||
navigate(`/user?pubkey=${event.pubkey}`);
|
||||
};
|
||||
|
||||
const openThread = (e) => {
|
||||
const selection = window.getSelection();
|
||||
if (selection.toString().length === 0) {
|
||||
router.push(`/nostr/newsfeed/note?id=${event.parent_id}`, { forceOptimisticNavigation: true });
|
||||
navigate(`/newsfeed/note?id=${event.parent_id}`);
|
||||
} else {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
import { UserExtend } from '@components/user/extend';
|
||||
|
||||
@ -7,9 +6,9 @@ import { dateToUnix } from '@utils/getDate';
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import useLocalStorage from '@rehooks/local-storage';
|
||||
import { ChatLines, OpenNewWindow } from 'iconoir-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { getEventHash, signEvent } from 'nostr-tools';
|
||||
import { useContext, useState } from 'react';
|
||||
import { navigate } from 'vite-plugin-ssr/client/router';
|
||||
|
||||
export const NoteComment = ({
|
||||
count,
|
||||
@ -24,7 +23,6 @@ export const NoteComment = ({
|
||||
eventTime: number;
|
||||
eventContent: any;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
@ -34,7 +32,7 @@ export const NoteComment = ({
|
||||
const profile = activeAccount.metadata ? JSON.parse(activeAccount.metadata) : null;
|
||||
|
||||
const openThread = () => {
|
||||
router.push(`/nostr/newsfeed/note?id=${eventID}`, { forceOptimisticNavigation: true });
|
||||
navigate(`/newsfeed/note?id=${eventID}`);
|
||||
};
|
||||
|
||||
const submitEvent = () => {
|
||||
@ -84,12 +82,7 @@ export const NoteComment = ({
|
||||
<div className="flex gap-2">
|
||||
<div>
|
||||
<div className="relative h-11 w-11 shrink-0 overflow-hidden rounded-md border border-white/10">
|
||||
<ImageWithFallback
|
||||
src={profile?.picture}
|
||||
alt="user's avatar"
|
||||
fill={true}
|
||||
className="rounded-md object-cover"
|
||||
/>
|
||||
<img src={profile?.picture} alt="user's avatar" className="h-11 w-11 rounded-md object-cover" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative h-36 w-full flex-1 overflow-hidden 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">
|
||||
|
@ -1,18 +1,9 @@
|
||||
import Image from 'next/image';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const ImagePreview = memo(function ImagePreview({ url, size }: { url: string; size: string }) {
|
||||
return (
|
||||
<div className={`relative h-full ${size === 'large' ? 'w-4/5' : 'w-1/2'} mt-2 rounded-lg border border-zinc-800`}>
|
||||
<Image
|
||||
src={url}
|
||||
alt={url}
|
||||
width="0"
|
||||
height="0"
|
||||
sizes="100vw"
|
||||
className="h-auto w-full rounded-lg object-cover"
|
||||
priority
|
||||
/>
|
||||
<img src={url} alt={url} className="h-auto w-full rounded-lg object-cover" />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -1,23 +0,0 @@
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function LinkCard({ data }: { data: any }) {
|
||||
return (
|
||||
<Link
|
||||
href={data['url']}
|
||||
target={'_blank'}
|
||||
className="relative flex flex-col overflow-hidden rounded-lg 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>
|
||||
<div className="flex flex-col gap-2 p-4">
|
||||
<div>
|
||||
<h5 className="font-semibold leading-tight">{data['title']}</h5>
|
||||
<p className="text-sm text-zinc-300">{data['description']}</p>
|
||||
</div>
|
||||
<span className="text-sm text-zinc-500">{data['url']}</span>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
@ -4,11 +4,10 @@ import { UserExtend } from '@components/user/extend';
|
||||
|
||||
import { contentParser } from '@utils/parser';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { memo, useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { navigate } from 'vite-plugin-ssr/client/router';
|
||||
|
||||
export const RootNote = memo(function RootNote({ event }: { event: any }) {
|
||||
const router = useRouter();
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
|
||||
const [data, setData] = useState(null);
|
||||
@ -16,13 +15,13 @@ export const RootNote = memo(function RootNote({ event }: { event: any }) {
|
||||
|
||||
const openUserPage = (e) => {
|
||||
e.stopPropagation();
|
||||
router.push(`/nostr/user?pubkey=${event.pubkey}`, { forceOptimisticNavigation: true });
|
||||
navigate(`/user?pubkey=${event.pubkey}`);
|
||||
};
|
||||
|
||||
const openThread = (e) => {
|
||||
const selection = window.getSelection();
|
||||
if (selection.toString().length === 0) {
|
||||
router.push(`/nostr/newsfeed/note?id=${event.parent_id}`, { forceOptimisticNavigation: true });
|
||||
navigate(`/newsfeed/note?id=${event.parent_id}`);
|
||||
} else {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
import { UserFollow } from '@components/user/follow';
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
import { UserFollow } from '@components/user/follow';
|
||||
|
||||
|
@ -1,6 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
@ -8,7 +5,6 @@ import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
import { shortenKey } from '@utils/shortenKey';
|
||||
|
||||
import destr from 'destr';
|
||||
import Image from 'next/image';
|
||||
import { Author } from 'nostr-relaypool';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
|
||||
@ -27,21 +23,11 @@ export default function ProfileMetadata({ id }: { id: string }) {
|
||||
<>
|
||||
<div className="relative">
|
||||
<div className="relative h-56 w-full rounded-t-lg bg-zinc-800">
|
||||
<Image
|
||||
src={profile?.banner || DEFAULT_BANNER}
|
||||
alt="user's banner"
|
||||
fill={true}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<img src={profile?.banner || DEFAULT_BANNER} alt="user's banner" className="h-58 w-full object-cover" />
|
||||
</div>
|
||||
<div className="relative -top-8 z-10 px-4">
|
||||
<div className="relative h-16 w-16 rounded-lg bg-zinc-900 ring-2 ring-zinc-900">
|
||||
<ImageWithFallback
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={id}
|
||||
fill={true}
|
||||
className="rounded-lg object-cover"
|
||||
/>
|
||||
<img src={profile?.picture || DEFAULT_AVATAR} alt={id} className="h-16 w-16 rounded-lg object-cover" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import { NoteBase } from '@components/note/base';
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import { DEFAULT_RELAYS } from '@stores/constants';
|
||||
|
||||
import { RelayPool } from 'nostr-relaypool';
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
||||
@ -13,12 +11,7 @@ export const UserBase = memo(function UserBase({ pubkey }: { pubkey: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full border border-white/10">
|
||||
<ImageWithFallback
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
fill={true}
|
||||
className="rounded-full object-cover"
|
||||
/>
|
||||
<img src={profile?.picture || DEFAULT_AVATAR} alt={pubkey} className="h-11 w-11 rounded-full object-cover" />
|
||||
</div>
|
||||
<div className="flex w-full flex-1 flex-col items-start text-start">
|
||||
<span className="truncate font-medium leading-tight text-zinc-200">
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
||||
@ -16,12 +14,7 @@ export const UserExtend = ({ pubkey, time }: { pubkey: string; time: number }) =
|
||||
return (
|
||||
<div className="group flex h-11 items-center gap-2">
|
||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
|
||||
<ImageWithFallback
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
fill={true}
|
||||
className="rounded-md object-cover"
|
||||
/>
|
||||
<img src={profile?.picture || DEFAULT_AVATAR} alt={pubkey} className="h-11 w-11 rounded-md object-cover" />
|
||||
</div>
|
||||
<div className="flex w-full flex-1 items-start justify-between">
|
||||
<div className="flex flex-col gap-1">
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
||||
@ -11,12 +9,7 @@ export const UserFollow = ({ pubkey }: { pubkey: string }) => {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-full border border-white/10">
|
||||
<ImageWithFallback
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
fill={true}
|
||||
className="rounded-full object-cover"
|
||||
/>
|
||||
<img src={profile?.picture || DEFAULT_AVATAR} alt={pubkey} className="h-11 w-11 rounded-full object-cover" />
|
||||
</div>
|
||||
<div className="flex w-full flex-1 flex-col items-start text-start">
|
||||
<span className="truncate font-medium leading-tight text-zinc-200">
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
||||
@ -17,11 +15,10 @@ export const UserLarge = ({ pubkey, time }: { pubkey: string; time: number }) =>
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
|
||||
<ImageWithFallback
|
||||
<img
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
fill={true}
|
||||
className="rounded-md border border-white/10 object-cover"
|
||||
className="h-11 w-11 rounded-md border border-white/10 object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full flex-1">
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
||||
@ -11,12 +9,7 @@ export const UserMini = ({ pubkey }: { pubkey: string }) => {
|
||||
return (
|
||||
<div className="group flex items-start gap-1">
|
||||
<div className="relative h-7 w-7 shrink overflow-hidden rounded border border-white/10">
|
||||
<ImageWithFallback
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
fill={true}
|
||||
className="rounded object-cover"
|
||||
/>
|
||||
<img src={profile?.picture || DEFAULT_AVATAR} alt={pubkey} className="h-7 w-7 rounded object-cover" />
|
||||
</div>
|
||||
<span className="text-xs font-medium leading-none text-zinc-500">
|
||||
Replying to {profile?.name || shortenKey(pubkey)}
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { ImageWithFallback } from '@components/imageWithFallback';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { useProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
||||
@ -16,12 +14,7 @@ export const UserQuoteRepost = ({ pubkey, time }: { pubkey: string; time: number
|
||||
return (
|
||||
<div className="group flex items-center gap-2">
|
||||
<div className="relative h-11 w-11 shrink overflow-hidden rounded-md bg-white">
|
||||
<ImageWithFallback
|
||||
src={profile?.picture || DEFAULT_AVATAR}
|
||||
alt={pubkey}
|
||||
fill={true}
|
||||
className="rounded-md object-cover"
|
||||
/>
|
||||
<img src={profile?.picture || DEFAULT_AVATAR} alt={pubkey} className="h-11 w-11 rounded-md object-cover" />
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2 text-sm">
|
||||
<h5 className="font-semibold leading-tight group-hover:underline">
|
||||
|
@ -1,24 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { ChannelMessages } from '@components/channels/messages';
|
||||
import { FormChannel } from '@components/form/channel';
|
||||
import NewsfeedLayout from '@components/layouts/newsfeed';
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { channelMessagesAtom, channelReplyAtom } from '@stores/channel';
|
||||
import { FULL_RELAYS } from '@stores/constants';
|
||||
|
||||
import { dateToUnix, hoursAgo } from '@utils/getDate';
|
||||
import { usePageContext } from '@utils/hooks/usePageContext';
|
||||
|
||||
import useLocalStorage from '@rehooks/local-storage';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useResetAtom } from 'jotai/utils';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useContext, useEffect, useRef } from 'react';
|
||||
import useSWRSubscription from 'swr/subscription';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const id = searchParams.get('channel-id');
|
||||
export function Page() {
|
||||
const pageContext = usePageContext();
|
||||
const searchParams: any = pageContext.urlParsed.search;
|
||||
|
||||
const id = searchParams.id;
|
||||
|
||||
const [pool]: any = useContext(RelayContext);
|
||||
const [activeAccount]: any = useLocalStorage('account', {});
|
||||
@ -84,11 +85,13 @@ export default function Page() {
|
||||
}, [resetChannelReply, resetChannelMessages]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col justify-between">
|
||||
<ChannelMessages />
|
||||
<div className="shrink-0 p-3">
|
||||
<FormChannel eventId={id} />
|
||||
<NewsfeedLayout>
|
||||
<div className="flex h-full w-full flex-col justify-between">
|
||||
<ChannelMessages />
|
||||
<div className="shrink-0 p-3">
|
||||
<FormChannel eventId={id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NewsfeedLayout>
|
||||
);
|
||||
}
|
@ -1,22 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { MessageList } from '@components/chats/messageList';
|
||||
import FormChat from '@components/form/chat';
|
||||
import NewsfeedLayout from '@components/layouts/newsfeed';
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { chatMessagesAtom } from '@stores/chat';
|
||||
import { FULL_RELAYS } from '@stores/constants';
|
||||
|
||||
import { usePageContext } from '@utils/hooks/usePageContext';
|
||||
|
||||
import useLocalStorage from '@rehooks/local-storage';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useResetAtom } from 'jotai/utils';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import useSWRSubscription from 'swr/subscription';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const pubkey = searchParams.get('pubkey');
|
||||
export function Page() {
|
||||
const pageContext = usePageContext();
|
||||
const searchParams: any = pageContext.urlParsed.search;
|
||||
|
||||
const pubkey = searchParams.pubkey;
|
||||
|
||||
const [pool]: any = useContext(RelayContext);
|
||||
const [activeAccount]: any = useLocalStorage('account', {});
|
||||
@ -62,11 +64,13 @@ export default function Page() {
|
||||
}, [resetChatMessages]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col justify-between">
|
||||
<MessageList />
|
||||
<div className="shrink-0 p-3">
|
||||
<FormChat receiverPubkey={pubkey} />
|
||||
<NewsfeedLayout>
|
||||
<div className="flex h-full w-full flex-col justify-between">
|
||||
<MessageList />
|
||||
<div className="shrink-0 p-3">
|
||||
<FormChat receiverPubkey={pubkey} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NewsfeedLayout>
|
||||
);
|
||||
}
|
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { dateToUnix, hoursAgo } from '@utils/getDate';
|
||||
@ -19,11 +17,10 @@ import { getParentID } from '@utils/transform';
|
||||
import LumeSymbol from '@assets/icons/Lume';
|
||||
|
||||
import { writeStorage } from '@rehooks/local-storage';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useCallback, useContext, useEffect, useRef } from 'react';
|
||||
import { navigate } from 'vite-plugin-ssr/client/router';
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
export function Page() {
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
|
||||
const now = useRef(new Date());
|
||||
@ -126,7 +123,7 @@ export default function Page() {
|
||||
() => {
|
||||
updateLastLogin(dateToUnix(now.current));
|
||||
timeout.current = setTimeout(() => {
|
||||
router.replace('/nostr/newsfeed/following', { forceOptimisticNavigation: true });
|
||||
navigate('/newsfeed/following', { overwriteLastHistoryEntry: true });
|
||||
}, 5000);
|
||||
},
|
||||
{
|
||||
@ -139,39 +136,41 @@ export default function Page() {
|
||||
unsubscribe();
|
||||
};
|
||||
},
|
||||
[router, pool, relays]
|
||||
[pool, relays]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
|
||||
getPlebs()
|
||||
.then((res) => {
|
||||
if (res && !ignore) {
|
||||
writeStorage('plebs', res);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
if (!ignore) {
|
||||
getActiveAccount()
|
||||
.then((res: any) => {
|
||||
if (res) {
|
||||
const account = res;
|
||||
// update local storage
|
||||
writeStorage('account', account);
|
||||
// fetch data
|
||||
fetchData(account, account.follows);
|
||||
} else {
|
||||
navigate('/onboarding', { overwriteLastHistoryEntry: true });
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
getActiveAccount()
|
||||
.then((res: any) => {
|
||||
if (res && !ignore) {
|
||||
const account = res;
|
||||
// update local storage
|
||||
writeStorage('account', account);
|
||||
// fetch data
|
||||
fetchData(account, account.follows);
|
||||
} else {
|
||||
router.replace('/onboarding', { forceOptimisticNavigation: true });
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
getPlebs()
|
||||
.then((res) => {
|
||||
if (res && !ignore) {
|
||||
writeStorage('plebs', res);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
return () => {
|
||||
ignore = true;
|
||||
clearTimeout(timeout.current);
|
||||
};
|
||||
}, [fetchData, router]);
|
||||
}, [fetchData]);
|
||||
|
||||
return (
|
||||
<div className="h-screen w-screen bg-zinc-50 text-zinc-900 dark:bg-black dark:text-white">
|
3
src/pages/newsfeed/circle/index.page.tsx
Normal file
3
src/pages/newsfeed/circle/index.page.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export function Page() {
|
||||
return <></>;
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import FormBase from '@components/form/base';
|
||||
import NewsfeedLayout from '@components/layouts/newsfeed';
|
||||
import { NoteBase } from '@components/note/base';
|
||||
import { Placeholder } from '@components/note/placeholder';
|
||||
import { NoteQuoteRepost } from '@components/note/quoteRepost';
|
||||
@ -15,7 +14,7 @@ import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
|
||||
export default function Page() {
|
||||
export function Page() {
|
||||
const [hasNewerNote, setHasNewerNote] = useAtom(hasNewerNoteAtom);
|
||||
const setData = useSetAtom(notesAtom);
|
||||
const data = useAtomValue(filteredNotesAtom);
|
||||
@ -82,29 +81,31 @@ export default function Page() {
|
||||
}, [initialData]);
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
{hasNewerNote && (
|
||||
<div className="absolute left-1/2 top-2 z-50 -translate-x-1/2 transform">
|
||||
<button
|
||||
onClick={() => loadLatest()}
|
||||
className="inline-flex h-8 transform items-center justify-center gap-1 rounded-full bg-fuchsia-500 pl-3 pr-3.5 text-sm shadow-md shadow-fuchsia-800/20 active:translate-y-1"
|
||||
>
|
||||
<ArrowUp width={14} height={14} />
|
||||
Load latest
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={data}
|
||||
itemContent={itemContent}
|
||||
computeItemKey={computeItemKey}
|
||||
components={COMPONENTS}
|
||||
overscan={200}
|
||||
endReached={loadMore}
|
||||
className="scrollbar-hide h-full w-full overflow-y-auto"
|
||||
/>
|
||||
</div>
|
||||
<NewsfeedLayout>
|
||||
<div className="relative h-full w-full">
|
||||
{hasNewerNote && (
|
||||
<div className="absolute left-1/2 top-2 z-50 -translate-x-1/2 transform">
|
||||
<button
|
||||
onClick={() => loadLatest()}
|
||||
className="inline-flex h-8 transform items-center justify-center gap-1 rounded-full bg-fuchsia-500 pl-3 pr-3.5 text-sm shadow-md shadow-fuchsia-800/20 active:translate-y-1"
|
||||
>
|
||||
<ArrowUp width={14} height={14} />
|
||||
Load latest
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={data}
|
||||
itemContent={itemContent}
|
||||
computeItemKey={computeItemKey}
|
||||
components={COMPONENTS}
|
||||
overscan={200}
|
||||
endReached={loadMore}
|
||||
className="scrollbar-hide h-full w-full overflow-y-auto"
|
||||
/>
|
||||
</div>
|
||||
</NewsfeedLayout>
|
||||
);
|
||||
}
|
||||
|
172
src/pages/onboarding/create/index.page.tsx
Normal file
172
src/pages/onboarding/create/index.page.tsx
Normal file
@ -0,0 +1,172 @@
|
||||
import OnboardingLayout from '@components/layouts/onboarding';
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { createAccount } from '@utils/storage';
|
||||
|
||||
import { EyeClose, EyeEmpty } from 'iconoir-react';
|
||||
import { generatePrivateKey, getEventHash, getPublicKey, nip19, signEvent } from 'nostr-tools';
|
||||
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { Config, names, uniqueNamesGenerator } from 'unique-names-generator';
|
||||
import { navigate } from 'vite-plugin-ssr/client/router';
|
||||
|
||||
const config: Config = {
|
||||
dictionaries: [names],
|
||||
};
|
||||
|
||||
export function Page() {
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
|
||||
const [type, setType] = useState('password');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const privkey = useMemo(() => generatePrivateKey(), []);
|
||||
const name = useMemo(() => uniqueNamesGenerator(config).toString(), []);
|
||||
|
||||
const pubkey = getPublicKey(privkey);
|
||||
const npub = nip19.npubEncode(pubkey);
|
||||
const nsec = nip19.nsecEncode(privkey);
|
||||
|
||||
// auto-generated profile metadata
|
||||
const metadata: any = useMemo(
|
||||
() => ({
|
||||
display_name: name,
|
||||
name: name,
|
||||
username: name.toLowerCase(),
|
||||
picture: 'https://void.cat/d/KmypFh2fBdYCEvyJrPiN89.webp',
|
||||
}),
|
||||
[name]
|
||||
);
|
||||
|
||||
// toggle privatek key
|
||||
const showPrivateKey = () => {
|
||||
if (type === 'password') {
|
||||
setType('text');
|
||||
} else {
|
||||
setType('password');
|
||||
}
|
||||
};
|
||||
|
||||
// create account and broadcast to all relays
|
||||
const submit = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
||||
// build event
|
||||
const event: any = {
|
||||
content: JSON.stringify(metadata),
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 0,
|
||||
pubkey: pubkey,
|
||||
tags: [],
|
||||
};
|
||||
event.id = getEventHash(event);
|
||||
event.sig = signEvent(event, privkey);
|
||||
// insert to database
|
||||
createAccount(pubkey, privkey, metadata);
|
||||
// broadcast
|
||||
pool.publish(event, relays);
|
||||
// redirect to next step
|
||||
navigate(`/onboarding/create/step-2?pubkey=${pubkey}&privkey=${privkey}`);
|
||||
}, [pool, pubkey, privkey, metadata, relays]);
|
||||
|
||||
return (
|
||||
<OnboardingLayout>
|
||||
<div className="grid h-full w-full grid-rows-5">
|
||||
<div className="row-span-1 mx-auto flex w-full max-w-md items-center justify-center">
|
||||
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
|
||||
Create new account
|
||||
</h1>
|
||||
</div>
|
||||
<div className="row-span-4">
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-8 flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-sm font-semibold text-zinc-400">Public Key</label>
|
||||
<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">
|
||||
<input
|
||||
readOnly
|
||||
value={npub}
|
||||
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2.5 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-sm font-semibold text-zinc-400">Private Key</label>
|
||||
<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">
|
||||
<input
|
||||
readOnly
|
||||
type={type}
|
||||
value={nsec}
|
||||
className="relative w-full rounded-lg border border-black/5 py-2.5 pl-3.5 pr-11 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600"
|
||||
/>
|
||||
<button
|
||||
onClick={() => showPrivateKey()}
|
||||
className="group absolute right-2 top-1/2 -translate-y-1/2 transform rounded p-1 hover:bg-zinc-700"
|
||||
>
|
||||
{type === 'password' ? (
|
||||
<EyeClose width={20} height={20} className="text-zinc-500 group-hover:text-zinc-200" />
|
||||
) : (
|
||||
<EyeEmpty width={20} height={20} className="text-zinc-500 group-hover:text-zinc-200" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-sm font-semibold text-zinc-400">Default Profile (you can change it later)</label>
|
||||
<div className="relative w-full 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">
|
||||
<div className="relative w-full rounded-lg border border-black/5 px-3.5 py-4 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-600">
|
||||
<div className="flex space-x-2">
|
||||
<div className="relative h-11 w-11 rounded-md">
|
||||
<img
|
||||
src={metadata.picture}
|
||||
alt="default avatar"
|
||||
className="h-11 w-11 rounded-md object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 space-y-2 py-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="font-semibold">{metadata.display_name}</p>
|
||||
<p className="text-zinc-400">@{metadata.username}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-2 h-2 rounded bg-zinc-700"></div>
|
||||
<div className="col-span-1 h-2 rounded bg-zinc-700"></div>
|
||||
</div>
|
||||
<div className="h-2 rounded bg-zinc-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-10 items-center justify-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"
|
||||
>
|
||||
<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>
|
||||
</svg>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => submit()}
|
||||
className="w-full transform rounded-lg bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 px-3.5 py-2.5 font-medium text-zinc-800 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
>
|
||||
<span className="drop-shadow-lg">Continue →</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</OnboardingLayout>
|
||||
);
|
||||
}
|
@ -1,17 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import OnboardingLayout from '@components/layouts/onboarding';
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
import { UserBase } from '@components/user/base';
|
||||
|
||||
import { usePageContext } from '@utils/hooks/usePageContext';
|
||||
import { fetchProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
||||
import { createPleb, updateAccount } from '@utils/storage';
|
||||
import { arrayToNIP02 } from '@utils/transform';
|
||||
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { CheckCircle } from 'iconoir-react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { getEventHash, signEvent } from 'nostr-tools';
|
||||
import { Key, useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { navigate } from 'vite-plugin-ssr/client/router';
|
||||
|
||||
const supabase = createClient(
|
||||
'https://niwaazauwnrwiwmnocnn.supabase.co',
|
||||
@ -53,12 +53,12 @@ const initialList = [
|
||||
{ pubkey: 'ff04a0e6cd80c141b0b55825fed127d4532a6eecdb7e743a38a3c28bf9f44609' },
|
||||
];
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
export function Page() {
|
||||
const pageContext = usePageContext();
|
||||
const searchParams: any = pageContext.urlParsed.search;
|
||||
|
||||
const pubkey = searchParams.get('pubkey');
|
||||
const privkey = searchParams.get('privkey');
|
||||
const pubkey = searchParams.pubkey;
|
||||
const privkey = searchParams.privkey;
|
||||
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@ -100,8 +100,8 @@ export default function Page() {
|
||||
// broadcast
|
||||
pool.publish(event, relays);
|
||||
// redirect to splashscreen
|
||||
router.replace('/');
|
||||
}, [pubkey, privkey, follows, pool, relays, router]);
|
||||
navigate('/');
|
||||
}, [pubkey, privkey, follows, pool, relays]);
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
@ -122,65 +122,67 @@ export default function Page() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative grid h-full w-full grid-rows-5">
|
||||
<div className="row-span-1 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium leading-tight text-transparent">
|
||||
Personalized your newsfeed
|
||||
</h1>
|
||||
<h3 className="text-lg text-zinc-500">
|
||||
Follow at least{' '}
|
||||
<span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent">
|
||||
{follows.length}/10
|
||||
</span>{' '}
|
||||
plebs
|
||||
</h3>
|
||||
<OnboardingLayout>
|
||||
<div className="relative grid h-full w-full grid-rows-5">
|
||||
<div className="row-span-1 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium leading-tight text-transparent">
|
||||
Personalized your newsfeed
|
||||
</h1>
|
||||
<h3 className="text-lg text-zinc-500">
|
||||
Follow at least{' '}
|
||||
<span className="bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-transparent">
|
||||
{follows.length}/10
|
||||
</span>{' '}
|
||||
plebs
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row-span-4 h-full w-full overflow-y-auto">
|
||||
<div className="grid grid-cols-4 gap-4 px-8 py-4">
|
||||
{list.map((item: { pubkey: string }, index: Key) => (
|
||||
<div className="row-span-4 h-full w-full overflow-y-auto">
|
||||
<div className="grid grid-cols-4 gap-4 px-8 py-4">
|
||||
{list.map((item: { pubkey: string }, index: Key) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => toggleFollow(item.pubkey)}
|
||||
className="flex transform items-center justify-between rounded-lg bg-zinc-900 p-2 ring-amber-100 hover:ring-1 active:translate-y-1"
|
||||
>
|
||||
<UserBase pubkey={item.pubkey} />
|
||||
{follows.includes(item.pubkey) && (
|
||||
<div>
|
||||
<CheckCircle width={16} height={16} className="text-zinc-400" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{follows.length >= 10 && (
|
||||
<div className="fixed bottom-0 left-0 z-10 flex h-24 w-full items-center justify-center">
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => toggleFollow(item.pubkey)}
|
||||
className="flex transform items-center justify-between rounded-lg bg-zinc-900 p-2 ring-amber-100 hover:ring-1 active:translate-y-1"
|
||||
onClick={() => submit()}
|
||||
className="relative z-20 inline-flex w-36 transform items-center justify-center rounded-full bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 px-3.5 py-2.5 font-medium text-zinc-800 shadow-xl active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
>
|
||||
<UserBase pubkey={item.pubkey} />
|
||||
{follows.includes(item.pubkey) && (
|
||||
<div>
|
||||
<CheckCircle width={16} height={16} className="text-zinc-400" />
|
||||
</div>
|
||||
{loading === true ? (
|
||||
<svg
|
||||
className="h-5 w-5 animate-spin text-zinc-900"
|
||||
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>
|
||||
</svg>
|
||||
) : (
|
||||
<span className="drop-shadow-lg">Done! Go to newsfeed</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{follows.length >= 10 && (
|
||||
<div className="fixed bottom-0 left-0 z-10 flex h-24 w-full items-center justify-center">
|
||||
<button
|
||||
onClick={() => submit()}
|
||||
className="relative z-20 inline-flex w-36 transform items-center justify-center rounded-full bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 px-3.5 py-2.5 font-medium text-zinc-800 shadow-xl active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
>
|
||||
{loading === true ? (
|
||||
<svg
|
||||
className="h-5 w-5 animate-spin text-zinc-900"
|
||||
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>
|
||||
</svg>
|
||||
) : (
|
||||
<span className="drop-shadow-lg">Done! Go to newsfeed</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</OnboardingLayout>
|
||||
);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import OnboardingLayout from '@components/layouts/onboarding';
|
||||
|
||||
import { ArrowRight } from 'iconoir-react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
const PLEBS = [
|
||||
'https://133332.xyz/p.jpg',
|
||||
@ -72,58 +72,47 @@ const InfiniteLoopSlider = ({ children, duration, reverse }: { children: any; du
|
||||
);
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
export function Page() {
|
||||
return (
|
||||
<div className="grid h-full w-full grid-rows-5">
|
||||
<div className="row-span-3 overflow-hidden">
|
||||
<div className="relaive flex w-full max-w-full shrink-0 flex-col gap-4 overflow-hidden p-4">
|
||||
{[...new Array(ROWS)].map((_, i) => (
|
||||
<InfiniteLoopSlider key={i} duration={random(DURATION - 5000, DURATION + 20000)} reverse={i % 2}>
|
||||
{shuffle(PLEBS)
|
||||
.slice(0, PLEBS_PER_ROW)
|
||||
.map((tag) => (
|
||||
<div
|
||||
key={tag}
|
||||
className="relative mr-4 flex h-11 w-11 items-center gap-2 rounded-md bg-zinc-900 px-4 py-1.5 shadow-xl"
|
||||
>
|
||||
<Image
|
||||
src={tag}
|
||||
alt={tag}
|
||||
fill={true}
|
||||
className="rounded-md border border-zinc-900"
|
||||
placeholder="blur"
|
||||
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkqAcAAIUAgUW0RjgAAAAASUVORK5CYII="
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</InfiniteLoopSlider>
|
||||
))}
|
||||
<div className="pointer-events-none absolute inset-0 bg-fade" />
|
||||
<OnboardingLayout>
|
||||
<div className="grid h-full w-full grid-rows-5">
|
||||
<div className="row-span-3 overflow-hidden">
|
||||
<div className="relaive flex w-full max-w-full shrink-0 flex-col gap-4 overflow-hidden p-4">
|
||||
{[...new Array(ROWS)].map((_, i) => (
|
||||
<InfiniteLoopSlider key={i} duration={random(DURATION - 5000, DURATION + 20000)} reverse={i % 2}>
|
||||
{shuffle(PLEBS)
|
||||
.slice(0, PLEBS_PER_ROW)
|
||||
.map((tag) => (
|
||||
<div key={tag} className="relative mr-4 h-11 w-11 gap-2 rounded-md bg-zinc-900 shadow-xl">
|
||||
<img src={tag} alt={tag} className="h-11 w-11 rounded-md border border-zinc-900" />
|
||||
</div>
|
||||
))}
|
||||
</InfiniteLoopSlider>
|
||||
))}
|
||||
<div className="pointer-events-none absolute inset-0 bg-fade" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="row-span-2 flex w-full flex-col items-center gap-4 overflow-hidden pt-6 min-[1050px]:gap-8 min-[1050px]:pt-10">
|
||||
<h1 className="animate-moveBg bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-5xl font-bold leading-none text-transparent">
|
||||
Let's start!
|
||||
</h1>
|
||||
<div className="mt-4 flex flex-col items-center gap-1.5">
|
||||
<a
|
||||
href="/onboarding/create"
|
||||
className="relative inline-flex h-14 w-64 items-center justify-center gap-2 rounded-full bg-zinc-900 px-6 text-lg font-medium ring-1 ring-zinc-800 hover:bg-zinc-800"
|
||||
>
|
||||
Create new key
|
||||
<ArrowRight width={20} height={20} />
|
||||
</a>
|
||||
<a
|
||||
href="/onboarding/login"
|
||||
className="inline-flex h-14 w-64 items-center justify-center gap-2 rounded-full px-6 text-base font-medium text-zinc-300 hover:bg-zinc-800"
|
||||
>
|
||||
Login with private key
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row-span-2 flex w-full flex-col items-center gap-4 overflow-hidden pt-6 min-[1050px]:gap-8 min-[1050px]:pt-10">
|
||||
<h1 className="animate-moveBg bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 bg-clip-text text-5xl font-bold leading-none text-transparent">
|
||||
Let's start!
|
||||
</h1>
|
||||
<div className="mt-4 flex flex-col items-center gap-1.5">
|
||||
<Link
|
||||
prefetch={false}
|
||||
href="/onboarding/create"
|
||||
className="relative inline-flex h-14 w-64 items-center justify-center gap-2 rounded-full bg-zinc-900 px-6 text-lg font-medium ring-1 ring-zinc-800 hover:bg-zinc-800"
|
||||
>
|
||||
Create new key
|
||||
<ArrowRight width={20} height={20} />
|
||||
</Link>
|
||||
<Link
|
||||
prefetch={false}
|
||||
href="/onboarding/login"
|
||||
className="inline-flex h-14 w-64 items-center justify-center gap-2 rounded-full px-6 text-base font-medium text-zinc-300 hover:bg-zinc-800"
|
||||
>
|
||||
Login with private key
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</OnboardingLayout>
|
||||
);
|
||||
}
|
121
src/pages/onboarding/login/index.page.tsx
Normal file
121
src/pages/onboarding/login/index.page.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import OnboardingLayout from '@components/layouts/onboarding';
|
||||
|
||||
import { CableTag } from 'iconoir-react';
|
||||
import { getPublicKey, nip19 } from 'nostr-tools';
|
||||
import { Resolver, useForm } from 'react-hook-form';
|
||||
import { navigate } from 'vite-plugin-ssr/client/router';
|
||||
|
||||
type FormValues = {
|
||||
key: string;
|
||||
};
|
||||
|
||||
const resolver: Resolver<FormValues> = async (values) => {
|
||||
return {
|
||||
values: values.key ? values : {},
|
||||
errors: !values.key
|
||||
? {
|
||||
key: {
|
||||
type: 'required',
|
||||
message: 'This is required.',
|
||||
},
|
||||
}
|
||||
: {},
|
||||
};
|
||||
};
|
||||
|
||||
export function Page() {
|
||||
const {
|
||||
register,
|
||||
setError,
|
||||
handleSubmit,
|
||||
formState: { errors, isDirty, isValid, isSubmitting },
|
||||
} = useForm<FormValues>({ resolver });
|
||||
|
||||
const onSubmit = async (data: any) => {
|
||||
try {
|
||||
let privkey = data['key'];
|
||||
|
||||
if (privkey.substring(0, 4) === 'nsec') {
|
||||
privkey = nip19.decode(privkey).data;
|
||||
}
|
||||
if (typeof getPublicKey(privkey) === 'string') {
|
||||
navigate(`/onboarding/login/step-2?privkey=${privkey}`);
|
||||
}
|
||||
} catch (error) {
|
||||
setError('key', {
|
||||
type: 'custom',
|
||||
message: 'Private Key is invalid, please check again',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<OnboardingLayout>
|
||||
<div className="grid h-full w-full grid-rows-5">
|
||||
<div className="row-span-1 mx-auto flex w-full max-w-md items-center justify-center">
|
||||
<h1 className="bg-gradient-to-br from-zinc-200 via-white to-zinc-300 bg-clip-text text-3xl font-semibold text-transparent">
|
||||
Login with Private Key
|
||||
</h1>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="row-span-4">
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
{/* #TODO: add function */}
|
||||
<button className="inline-flex w-full transform items-center justify-center gap-1.5 rounded-lg bg-zinc-700 px-3.5 py-2.5 font-medium text-zinc-200 shadow-input ring-1 ring-zinc-600 active:translate-y-1">
|
||||
{/* #TODO: change to nostr connect logo */}
|
||||
<CableTag width={20} height={20} className="text-fuchsia-500" />
|
||||
<span>Continue with Nostr Connect</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-zinc-800"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span className="bg-black px-2 text-sm text-zinc-500">or</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<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">
|
||||
<input
|
||||
{...register('key', { required: true, minLength: 32 })}
|
||||
type={'password'}
|
||||
placeholder="Paste private key here..."
|
||||
className="relative w-full rounded-lg border border-black/5 px-3.5 py-2.5 text-center shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-200 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-red-400">{errors.key && <p>{errors.key.message}</p>}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex h-10 items-center justify-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"
|
||||
>
|
||||
<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>
|
||||
</svg>
|
||||
) : (
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isDirty || !isValid}
|
||||
className="w-full transform rounded-lg bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 px-3.5 py-2.5 font-medium text-zinc-800 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
>
|
||||
<span className="drop-shadow-lg">Continue →</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</OnboardingLayout>
|
||||
);
|
||||
}
|
144
src/pages/onboarding/login/step-2/index.page.tsx
Normal file
144
src/pages/onboarding/login/step-2/index.page.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
import OnboardingLayout from '@components/layouts/onboarding';
|
||||
import { RelayContext } from '@components/relaysProvider';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||
|
||||
import { usePageContext } from '@utils/hooks/usePageContext';
|
||||
import { fetchProfileMetadata } from '@utils/hooks/useProfileMetadata';
|
||||
import { shortenKey } from '@utils/shortenKey';
|
||||
import { createAccount, createPleb, updateAccount } from '@utils/storage';
|
||||
import { nip02ToArray } from '@utils/transform';
|
||||
|
||||
import { getPublicKey } from 'nostr-tools';
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
|
||||
export function Page() {
|
||||
const pageContext = usePageContext();
|
||||
const searchParams = pageContext.urlParsed.search;
|
||||
|
||||
const privkey = searchParams.privkey;
|
||||
const pubkey = getPublicKey(privkey);
|
||||
|
||||
const [pool, relays]: any = useContext(RelayContext);
|
||||
const [profile, setProfile] = useState({ metadata: null });
|
||||
const [done, setDone] = useState(false);
|
||||
|
||||
const timeout = useRef(null);
|
||||
|
||||
const createPlebs = useCallback(async (tags: string[]) => {
|
||||
for (const tag of tags) {
|
||||
fetchProfileMetadata(tag[1])
|
||||
.then((res: any) => createPleb(tag[1], res.content))
|
||||
.catch(console.error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = pool.subscribe(
|
||||
[
|
||||
{
|
||||
kinds: [0, 3],
|
||||
authors: [pubkey],
|
||||
},
|
||||
],
|
||||
relays,
|
||||
(event: any) => {
|
||||
if (event.kind === 0) {
|
||||
// create account
|
||||
createAccount(pubkey, privkey, event.content);
|
||||
// update state
|
||||
setProfile({
|
||||
metadata: JSON.parse(event.content),
|
||||
});
|
||||
} else {
|
||||
if (event.tags.length > 0) {
|
||||
createPlebs(event.tags);
|
||||
const arr = nip02ToArray(event.tags);
|
||||
// update account's folllows with NIP-02 tag list
|
||||
updateAccount('follows', arr, pubkey);
|
||||
}
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
() => {
|
||||
timeout.current = setTimeout(() => setDone(true), 5000);
|
||||
},
|
||||
{
|
||||
unsubscribeOnEose: true,
|
||||
logAllEvents: false,
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
clearTimeout(timeout.current);
|
||||
};
|
||||
}, [pool, relays, pubkey, privkey, createPlebs]);
|
||||
|
||||
return (
|
||||
<OnboardingLayout>
|
||||
<div className="grid h-full w-full grid-rows-5">
|
||||
<div className="row-span-1 flex items-center justify-center">
|
||||
<h1 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-3xl font-medium text-transparent">
|
||||
Bringing back your profile...
|
||||
</h1>
|
||||
</div>
|
||||
<div className="row-span-4 flex flex-col gap-8">
|
||||
<div className="mx-auto w-full max-w-md">
|
||||
<div className="mb-4 flex flex-col gap-2">
|
||||
<div className="w-full rounded-lg bg-zinc-900 p-4 shadow-input ring-1 ring-zinc-800">
|
||||
<div className="flex space-x-4">
|
||||
<div className="relative h-10 w-10 rounded-full">
|
||||
<img
|
||||
className="h-10 w-10 rounded-full object-cover"
|
||||
src={profile.metadata?.picture || DEFAULT_AVATAR}
|
||||
alt="avatar"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 space-y-4 py-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-semibold">{profile.metadata?.display_name || profile.metadata?.name}</p>
|
||||
<span className="leading-tight text-zinc-500">·</span>
|
||||
<p className="text-zinc-500">@{profile.metadata?.username || (pubkey && shortenKey(pubkey))}</p>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-2 h-2 rounded bg-zinc-700"></div>
|
||||
<div className="col-span-1 h-2 rounded bg-zinc-700"></div>
|
||||
</div>
|
||||
<div className="h-2 rounded bg-zinc-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
{done === false ? (
|
||||
<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>
|
||||
</svg>
|
||||
) : (
|
||||
<a
|
||||
href="/"
|
||||
className="inline-flex w-full transform items-center justify-center rounded-lg bg-gradient-to-r from-fuchsia-300 via-orange-100 to-amber-300 px-3.5 py-2.5 font-medium text-zinc-800 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
>
|
||||
<span className="drop-shadow-lg">Done! Go to newsfeed</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</OnboardingLayout>
|
||||
);
|
||||
}
|
57
src/pages/user/index.page.tsx
Normal file
57
src/pages/user/index.page.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import NewsfeedLayout from '@components/layouts/newsfeed';
|
||||
import ProfileFollowers from '@components/profile/followers';
|
||||
import ProfileFollows from '@components/profile/follows';
|
||||
import ProfileMetadata from '@components/profile/metadata';
|
||||
import ProfileNotes from '@components/profile/notes';
|
||||
|
||||
import { usePageContext } from '@utils/hooks/usePageContext';
|
||||
|
||||
import * as Tabs from '@radix-ui/react-tabs';
|
||||
|
||||
export function Page() {
|
||||
const pageContext = usePageContext();
|
||||
const searchParams: any = pageContext.urlParsed.search;
|
||||
|
||||
const pubkey = searchParams.pubkey;
|
||||
|
||||
return (
|
||||
<NewsfeedLayout>
|
||||
<div className="scrollbar-hide h-full w-full overflow-y-auto">
|
||||
<ProfileMetadata id={pubkey} />
|
||||
<Tabs.Root className="flex w-full flex-col" defaultValue="notes">
|
||||
<Tabs.List className="flex border-b border-zinc-800">
|
||||
<Tabs.Trigger
|
||||
className="flex h-10 flex-1 cursor-default select-none items-center justify-center px-5 text-sm font-medium leading-none text-zinc-400 outline-none hover:text-fuchsia-400 data-[state=active]:text-fuchsia-500 data-[state=active]:shadow-[inset_0_-1px_0_0,0_1px_0_0] data-[state=active]:shadow-current"
|
||||
value="notes"
|
||||
>
|
||||
Notes
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
className="flex h-10 flex-1 cursor-default select-none items-center justify-center px-5 text-sm font-medium text-zinc-400 outline-none placeholder:leading-none hover:text-fuchsia-400 data-[state=active]:text-fuchsia-500 data-[state=active]:shadow-[inset_0_-1px_0_0,0_1px_0_0] data-[state=active]:shadow-current"
|
||||
value="followers"
|
||||
>
|
||||
Followers
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
className="flex h-10 flex-1 cursor-default select-none items-center justify-center px-5 text-sm font-medium leading-none text-zinc-400 outline-none hover:text-fuchsia-400 data-[state=active]:text-fuchsia-500 data-[state=active]:shadow-[inset_0_-1px_0_0,0_1px_0_0] data-[state=active]:shadow-current"
|
||||
value="following"
|
||||
>
|
||||
Following
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="notes">
|
||||
<ProfileNotes id={pubkey} />
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="followers">
|
||||
<ProfileFollowers id={pubkey} />
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="following">
|
||||
<ProfileFollows id={pubkey} />
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
</div>
|
||||
</NewsfeedLayout>
|
||||
);
|
||||
}
|
20
src/renderer/_default.page.client.tsx
Normal file
20
src/renderer/_default.page.client.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import '@renderer/index.css';
|
||||
import { Shell } from '@renderer/shell';
|
||||
import { PageContextClient } from '@renderer/types';
|
||||
|
||||
import { hydrateRoot } from 'react-dom/client';
|
||||
|
||||
export const clientRouting = true;
|
||||
|
||||
export async function render(pageContext: PageContextClient) {
|
||||
const { Page, pageProps } = pageContext;
|
||||
|
||||
if (!Page) throw new Error('Client-side render() hook expects pageContext.Page to be defined');
|
||||
|
||||
hydrateRoot(
|
||||
document.getElementById('app')!,
|
||||
<Shell pageContext={pageContext}>
|
||||
<Page {...pageProps} />
|
||||
</Shell>
|
||||
);
|
||||
}
|
30
src/renderer/_default.page.server.tsx
Normal file
30
src/renderer/_default.page.server.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { Shell } from '@renderer/shell';
|
||||
import { PageContextServer } from '@renderer/types';
|
||||
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import { dangerouslySkipEscape, escapeInject } from 'vite-plugin-ssr/server';
|
||||
|
||||
export const passToClient = ['pageProps'];
|
||||
|
||||
export function render(pageContext: PageContextServer) {
|
||||
const { Page, pageProps } = pageContext;
|
||||
|
||||
if (!Page) throw new Error('My render() hook expects pageContext.Page to be defined');
|
||||
|
||||
const pageHtml = ReactDOMServer.renderToString(
|
||||
<Shell pageContext={pageContext}>
|
||||
<Page {...pageProps} />
|
||||
</Shell>
|
||||
);
|
||||
|
||||
return escapeInject`<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</head>
|
||||
<body class="cursor-default select-none overflow-hidden font-sans antialiased">
|
||||
<div id="app">${dangerouslySkipEscape(pageHtml)}</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
17
src/renderer/_error.page.tsx
Normal file
17
src/renderer/_error.page.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
export function Page({ is404 }: { is404: boolean }) {
|
||||
if (is404) {
|
||||
return (
|
||||
<>
|
||||
<h1>404 Page Not Found</h1>
|
||||
<p>This page could not be found.</p>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<h1>500 Internal Server Error</h1>
|
||||
<p>Something went wrong.</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
17
src/renderer/shell.tsx
Normal file
17
src/renderer/shell.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import RelayProvider from '@components/relaysProvider';
|
||||
|
||||
import { PageContextProvider } from '@utils/hooks/usePageContext';
|
||||
|
||||
import { PageContext } from '@renderer/types';
|
||||
|
||||
import { StrictMode } from 'react';
|
||||
|
||||
export function Shell({ children, pageContext }: { children: React.ReactNode; pageContext: PageContext }) {
|
||||
return (
|
||||
<StrictMode>
|
||||
<PageContextProvider pageContext={pageContext}>
|
||||
<RelayProvider>{children}</RelayProvider>
|
||||
</PageContextProvider>
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
35
src/renderer/types.ts
Normal file
35
src/renderer/types.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import type {
|
||||
PageContextBuiltIn,
|
||||
/*
|
||||
// When using Client Routing https://vite-plugin-ssr.com/clientRouting
|
||||
PageContextBuiltInClientWithClientRouting as PageContextBuiltInClient
|
||||
/*/
|
||||
// When using Server Routing
|
||||
PageContextBuiltInClientWithServerRouting as PageContextBuiltInClient, //*/
|
||||
} from 'vite-plugin-ssr/types';
|
||||
|
||||
export type { PageContextServer };
|
||||
export type { PageContextClient };
|
||||
export type { PageContext };
|
||||
export type { PageProps };
|
||||
|
||||
type Page = (pageProps: PageProps) => React.ReactElement;
|
||||
type PageProps = Record<string, never>;
|
||||
|
||||
export type PageContextCustom = {
|
||||
Page: Page;
|
||||
pageProps?: PageProps;
|
||||
redirectTo?: string;
|
||||
urlPathname: string;
|
||||
exports: {
|
||||
documentProps?: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type PageContextServer = PageContextBuiltIn<Page> & PageContextCustom;
|
||||
type PageContextClient = PageContextBuiltInClient<Page> & PageContextCustom;
|
||||
|
||||
type PageContext = PageContextClient | PageContextServer;
|
22
src/utils/hooks/usePageContext.tsx
Normal file
22
src/utils/hooks/usePageContext.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
// `usePageContext` allows us to access `pageContext` in any React component.
|
||||
// See https://vite-plugin-ssr.com/pageContext-anywhere
|
||||
import type { PageContext } from '@renderer/types';
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
const Context = createContext<PageContext>(undefined as any);
|
||||
|
||||
export function PageContextProvider({
|
||||
pageContext,
|
||||
children,
|
||||
}: {
|
||||
pageContext: PageContext;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <Context.Provider value={pageContext}>{children}</Context.Provider>;
|
||||
}
|
||||
|
||||
export function usePageContext() {
|
||||
const pageContext = useContext(Context);
|
||||
return pageContext;
|
||||
}
|
@ -6,7 +6,8 @@
|
||||
"@components/*": ["src/components/*"],
|
||||
"@stores/*": ["src/stores/*"],
|
||||
"@utils/*": ["src/utils/*"],
|
||||
"@assets/*": ["src/assets/*"]
|
||||
"@assets/*": ["src/assets/*"],
|
||||
"@renderer/*": ["src/renderer/*"]
|
||||
},
|
||||
"target": "es2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
|
@ -4,5 +4,5 @@ import ssr from 'vite-plugin-ssr/plugin';
|
||||
import viteTsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), ssr(), viteTsconfigPaths()],
|
||||
plugins: [react(), ssr({ prerender: true }), viteTsconfigPaths()],
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user