60 changed files with 944 additions and 554 deletions
@ -1,33 +0,0 @@ |
|||||||
import { TDraftEvent, TRelayGroup, TTheme, TThemeSetting } from '@common/types' |
|
||||||
import { ElectronAPI } from '@electron-toolkit/preload' |
|
||||||
import { Event } from 'nostr-tools' |
|
||||||
|
|
||||||
declare global { |
|
||||||
interface Window { |
|
||||||
electron: ElectronAPI |
|
||||||
api: { |
|
||||||
system: { |
|
||||||
isEncryptionAvailable: () => Promise<boolean> |
|
||||||
} |
|
||||||
theme: { |
|
||||||
onChange: (cb: (theme: TTheme) => void) => void |
|
||||||
current: () => Promise<TTheme> |
|
||||||
themeSetting: () => Promise<TThemeSetting> |
|
||||||
set: (themeSetting: TThemeSetting) => Promise<void> |
|
||||||
} |
|
||||||
storage: { |
|
||||||
getRelayGroups: () => Promise<TRelayGroup[]> |
|
||||||
setRelayGroups: (relayGroups: TRelayGroup[]) => Promise<void> |
|
||||||
} |
|
||||||
nostr: { |
|
||||||
login: (nsec: string) => Promise<{ |
|
||||||
pubkey?: string |
|
||||||
reason?: string |
|
||||||
}> |
|
||||||
logout: () => Promise<void> |
|
||||||
getPublicKey: () => Promise<string | null> |
|
||||||
signEvent: (draftEvent: TDraftEvent) => Promise<Event | null> |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -0,0 +1,29 @@ |
|||||||
|
import { Button } from '@renderer/components/ui/button' |
||||||
|
import { useNostr } from '@renderer/providers/NostrProvider' |
||||||
|
import { LogIn } from 'lucide-react' |
||||||
|
|
||||||
|
export default function LoginButton({ |
||||||
|
variant = 'titlebar' |
||||||
|
}: { |
||||||
|
variant?: 'titlebar' | 'sidebar' |
||||||
|
}) { |
||||||
|
const { checkLogin } = useNostr() |
||||||
|
|
||||||
|
let triggerComponent: React.ReactNode |
||||||
|
if (variant === 'titlebar') { |
||||||
|
triggerComponent = <LogIn /> |
||||||
|
} else { |
||||||
|
triggerComponent = ( |
||||||
|
<> |
||||||
|
<LogIn size={16} /> |
||||||
|
<div>Login</div> |
||||||
|
</> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<Button variant={variant} size={variant} onClick={() => checkLogin()}> |
||||||
|
{triggerComponent} |
||||||
|
</Button> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,70 @@ |
|||||||
|
import { Avatar, AvatarFallback, AvatarImage } from '@renderer/components/ui/avatar' |
||||||
|
import { Button } from '@renderer/components/ui/button' |
||||||
|
import { |
||||||
|
DropdownMenu, |
||||||
|
DropdownMenuContent, |
||||||
|
DropdownMenuItem, |
||||||
|
DropdownMenuTrigger |
||||||
|
} from '@renderer/components/ui/dropdown-menu' |
||||||
|
import { useFetchProfile } from '@renderer/hooks' |
||||||
|
import { toProfile } from '@renderer/lib/link' |
||||||
|
import { generateImageByPubkey } from '@renderer/lib/pubkey' |
||||||
|
import { useSecondaryPage } from '@renderer/PageManager' |
||||||
|
import { useNostr } from '@renderer/providers/NostrProvider' |
||||||
|
|
||||||
|
export default function ProfileButton({ |
||||||
|
pubkey, |
||||||
|
variant = 'titlebar' |
||||||
|
}: { |
||||||
|
pubkey: string |
||||||
|
variant?: 'titlebar' | 'sidebar' |
||||||
|
}) { |
||||||
|
const { logout } = useNostr() |
||||||
|
const { |
||||||
|
profile: { avatar, username } |
||||||
|
} = useFetchProfile(pubkey) |
||||||
|
const { push } = useSecondaryPage() |
||||||
|
const defaultAvatar = generateImageByPubkey(pubkey) |
||||||
|
|
||||||
|
let triggerComponent: React.ReactNode |
||||||
|
if (variant === 'titlebar') { |
||||||
|
triggerComponent = ( |
||||||
|
<button> |
||||||
|
<Avatar className="w-6 h-6 hover:opacity-90"> |
||||||
|
<AvatarImage src={avatar} /> |
||||||
|
<AvatarFallback> |
||||||
|
<img src={defaultAvatar} /> |
||||||
|
</AvatarFallback> |
||||||
|
</Avatar> |
||||||
|
</button> |
||||||
|
) |
||||||
|
} else { |
||||||
|
triggerComponent = ( |
||||||
|
<Button variant="sidebar" size="sidebar" className="border hover:bg-muted px-2"> |
||||||
|
<div className="flex gap-2 items-center flex-1 w-0"> |
||||||
|
<Avatar className="w-10 h-10"> |
||||||
|
<AvatarImage src={avatar} /> |
||||||
|
<AvatarFallback> |
||||||
|
<img src={defaultAvatar} /> |
||||||
|
</AvatarFallback> |
||||||
|
</Avatar> |
||||||
|
<div className="truncate font-semibold text-lg">{username}</div> |
||||||
|
</div> |
||||||
|
</Button> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<DropdownMenu> |
||||||
|
<DropdownMenuTrigger className="non-draggable" asChild> |
||||||
|
{triggerComponent} |
||||||
|
</DropdownMenuTrigger> |
||||||
|
<DropdownMenuContent> |
||||||
|
<DropdownMenuItem onClick={() => push(toProfile(pubkey))}>Profile</DropdownMenuItem> |
||||||
|
<DropdownMenuItem className="text-destructive focus:text-destructive" onClick={logout}> |
||||||
|
Logout |
||||||
|
</DropdownMenuItem> |
||||||
|
</DropdownMenuContent> |
||||||
|
</DropdownMenu> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
import { useNostr } from '@renderer/providers/NostrProvider' |
||||||
|
import LoginButton from './LoginButton' |
||||||
|
import ProfileButton from './ProfileButton' |
||||||
|
|
||||||
|
export default function AccountButton({ |
||||||
|
variant = 'titlebar' |
||||||
|
}: { |
||||||
|
variant?: 'titlebar' | 'sidebar' |
||||||
|
}) { |
||||||
|
const { pubkey } = useNostr() |
||||||
|
|
||||||
|
if (pubkey) { |
||||||
|
return <ProfileButton variant={variant} pubkey={pubkey} /> |
||||||
|
} else { |
||||||
|
return <LoginButton variant={variant} /> |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,65 @@ |
|||||||
|
import { Button } from '@renderer/components/ui/button' |
||||||
|
import { |
||||||
|
Dialog, |
||||||
|
DialogContent, |
||||||
|
DialogDescription, |
||||||
|
DialogHeader, |
||||||
|
DialogTitle |
||||||
|
} from '@renderer/components/ui/dialog' |
||||||
|
import { Input } from '@renderer/components/ui/input' |
||||||
|
import { useNostr } from '@renderer/providers/NostrProvider' |
||||||
|
import { Dispatch, useState } from 'react' |
||||||
|
|
||||||
|
export default function LoginDialog({ |
||||||
|
open, |
||||||
|
setOpen |
||||||
|
}: { |
||||||
|
open: boolean |
||||||
|
setOpen: Dispatch<boolean> |
||||||
|
}) { |
||||||
|
const { login, canLogin } = useNostr() |
||||||
|
const [nsec, setNsec] = useState('') |
||||||
|
const [errMsg, setErrMsg] = useState<string | null>(null) |
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
||||||
|
setNsec(e.target.value) |
||||||
|
setErrMsg(null) |
||||||
|
} |
||||||
|
|
||||||
|
const handleLogin = () => { |
||||||
|
if (nsec === '') return |
||||||
|
|
||||||
|
login(nsec) |
||||||
|
.then(() => setOpen(false)) |
||||||
|
.catch((err) => { |
||||||
|
setErrMsg(err.message) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<Dialog open={open} onOpenChange={setOpen}> |
||||||
|
<DialogContent className="w-80"> |
||||||
|
<DialogHeader> |
||||||
|
<DialogTitle>Sign in</DialogTitle> |
||||||
|
<DialogDescription className="text-destructive"> |
||||||
|
{!canLogin && 'Encryption is not available in your device.'} |
||||||
|
</DialogDescription> |
||||||
|
</DialogHeader> |
||||||
|
<div className="space-y-1"> |
||||||
|
<Input |
||||||
|
type="password" |
||||||
|
placeholder="nsec1.." |
||||||
|
value={nsec} |
||||||
|
onChange={handleInputChange} |
||||||
|
className={errMsg ? 'border-destructive' : ''} |
||||||
|
disabled={!canLogin} |
||||||
|
/> |
||||||
|
{errMsg && <div className="text-xs text-destructive pl-3">{errMsg}</div>} |
||||||
|
</div> |
||||||
|
<Button onClick={handleLogin} disabled={!canLogin}> |
||||||
|
Login |
||||||
|
</Button> |
||||||
|
</DialogContent> |
||||||
|
</Dialog> |
||||||
|
) |
||||||
|
} |
||||||
@ -1,16 +1,13 @@ |
|||||||
import PostDialog from '@renderer/components/PostDialog' |
import PostDialog from '@renderer/components/PostDialog' |
||||||
import { Button } from '@renderer/components/ui/button' |
import { Button } from '@renderer/components/ui/button' |
||||||
import { useNostr } from '@renderer/providers/NostrProvider' |
|
||||||
import { PencilLine } from 'lucide-react' |
import { PencilLine } from 'lucide-react' |
||||||
|
|
||||||
export default function PostButton() { |
export default function PostButton({ variant = 'titlebar' }: { variant?: 'titlebar' | 'sidebar' }) { |
||||||
const { pubkey } = useNostr() |
|
||||||
if (!pubkey) return null |
|
||||||
|
|
||||||
return ( |
return ( |
||||||
<PostDialog> |
<PostDialog> |
||||||
<Button variant="titlebar" size="titlebar" title="new post"> |
<Button variant={variant} size={variant} title="new post"> |
||||||
<PencilLine /> |
<PencilLine /> |
||||||
|
{variant === 'sidebar' && <div>Post</div>} |
||||||
</Button> |
</Button> |
||||||
</PostDialog> |
</PostDialog> |
||||||
) |
) |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
import { toHome } from '@renderer/lib/link' |
||||||
|
import { SecondaryPageLink } from '@renderer/PageManager' |
||||||
|
import AccountButton from '../AccountButton' |
||||||
|
import PostButton from '../PostButton' |
||||||
|
import RefreshButton from '../RefreshButton' |
||||||
|
import RelaySettingsPopover from '../RelaySettingsPopover' |
||||||
|
|
||||||
|
export default function PrimaryPageSidebar() { |
||||||
|
return ( |
||||||
|
<div className="draggable w-52 h-full shrink-0 hidden xl:flex flex-col pb-8 pt-9 pl-4 justify-between"> |
||||||
|
<div className="space-y-2"> |
||||||
|
<div className="text-3xl font-extrabold font-mono text-center mb-4"> |
||||||
|
<SecondaryPageLink to={toHome()}>Jumble</SecondaryPageLink> |
||||||
|
</div> |
||||||
|
<PostButton variant="sidebar" /> |
||||||
|
<RelaySettingsPopover variant="sidebar" /> |
||||||
|
<RefreshButton variant="sidebar" /> |
||||||
|
</div> |
||||||
|
<AccountButton variant="sidebar" /> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
@ -1,79 +1,56 @@ |
|||||||
import * as React from "react" |
import * as React from 'react' |
||||||
|
|
||||||
import { cn } from "@renderer/lib/utils" |
import { cn } from '@renderer/lib/utils' |
||||||
|
|
||||||
const Card = React.forwardRef< |
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( |
||||||
HTMLDivElement, |
({ className, ...props }, ref) => ( |
||||||
React.HTMLAttributes<HTMLDivElement> |
<div |
||||||
>(({ className, ...props }, ref) => ( |
ref={ref} |
||||||
<div |
className={cn('rounded-lg border bg-card text-card-foreground', className)} |
||||||
ref={ref} |
{...props} |
||||||
className={cn( |
/> |
||||||
"rounded-lg border bg-card text-card-foreground shadow-sm", |
) |
||||||
className |
) |
||||||
)} |
Card.displayName = 'Card' |
||||||
{...props} |
|
||||||
/> |
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( |
||||||
)) |
({ className, ...props }, ref) => ( |
||||||
Card.displayName = "Card" |
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} /> |
||||||
|
) |
||||||
const CardHeader = React.forwardRef< |
) |
||||||
HTMLDivElement, |
CardHeader.displayName = 'CardHeader' |
||||||
React.HTMLAttributes<HTMLDivElement> |
|
||||||
>(({ className, ...props }, ref) => ( |
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>( |
||||||
<div |
({ className, ...props }, ref) => ( |
||||||
ref={ref} |
<h3 |
||||||
className={cn("flex flex-col space-y-1.5 p-6", className)} |
ref={ref} |
||||||
{...props} |
className={cn('text-2xl font-semibold leading-none tracking-tight', className)} |
||||||
/> |
{...props} |
||||||
)) |
/> |
||||||
CardHeader.displayName = "CardHeader" |
) |
||||||
|
) |
||||||
const CardTitle = React.forwardRef< |
CardTitle.displayName = 'CardTitle' |
||||||
HTMLParagraphElement, |
|
||||||
React.HTMLAttributes<HTMLHeadingElement> |
|
||||||
>(({ className, ...props }, ref) => ( |
|
||||||
<h3 |
|
||||||
ref={ref} |
|
||||||
className={cn( |
|
||||||
"text-2xl font-semibold leading-none tracking-tight", |
|
||||||
className |
|
||||||
)} |
|
||||||
{...props} |
|
||||||
/> |
|
||||||
)) |
|
||||||
CardTitle.displayName = "CardTitle" |
|
||||||
|
|
||||||
const CardDescription = React.forwardRef< |
const CardDescription = React.forwardRef< |
||||||
HTMLParagraphElement, |
HTMLParagraphElement, |
||||||
React.HTMLAttributes<HTMLParagraphElement> |
React.HTMLAttributes<HTMLParagraphElement> |
||||||
>(({ className, ...props }, ref) => ( |
>(({ className, ...props }, ref) => ( |
||||||
<p |
<p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} /> |
||||||
ref={ref} |
|
||||||
className={cn("text-sm text-muted-foreground", className)} |
|
||||||
{...props} |
|
||||||
/> |
|
||||||
)) |
|
||||||
CardDescription.displayName = "CardDescription" |
|
||||||
|
|
||||||
const CardContent = React.forwardRef< |
|
||||||
HTMLDivElement, |
|
||||||
React.HTMLAttributes<HTMLDivElement> |
|
||||||
>(({ className, ...props }, ref) => ( |
|
||||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> |
|
||||||
)) |
|
||||||
CardContent.displayName = "CardContent" |
|
||||||
|
|
||||||
const CardFooter = React.forwardRef< |
|
||||||
HTMLDivElement, |
|
||||||
React.HTMLAttributes<HTMLDivElement> |
|
||||||
>(({ className, ...props }, ref) => ( |
|
||||||
<div |
|
||||||
ref={ref} |
|
||||||
className={cn("flex items-center p-6 pt-0", className)} |
|
||||||
{...props} |
|
||||||
/> |
|
||||||
)) |
)) |
||||||
CardFooter.displayName = "CardFooter" |
CardDescription.displayName = 'CardDescription' |
||||||
|
|
||||||
|
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( |
||||||
|
({ className, ...props }, ref) => ( |
||||||
|
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} /> |
||||||
|
) |
||||||
|
) |
||||||
|
CardContent.displayName = 'CardContent' |
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( |
||||||
|
({ className, ...props }, ref) => ( |
||||||
|
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} /> |
||||||
|
) |
||||||
|
) |
||||||
|
CardFooter.displayName = 'CardFooter' |
||||||
|
|
||||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } |
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } |
||||||
|
|||||||
@ -1 +1,12 @@ |
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
import { TDraftEvent } from '@common/types' |
||||||
|
import { Event } from 'nostr-tools' |
||||||
|
|
||||||
|
declare global { |
||||||
|
interface Window { |
||||||
|
nostr?: { |
||||||
|
getPublicKey: () => Promise<string | null> |
||||||
|
signEvent: (draftEvent: TDraftEvent) => Promise<Event | null> |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|||||||
@ -1,114 +0,0 @@ |
|||||||
import { Avatar, AvatarFallback, AvatarImage } from '@renderer/components/ui/avatar' |
|
||||||
import { Button } from '@renderer/components/ui/button' |
|
||||||
import { |
|
||||||
Dialog, |
|
||||||
DialogContent, |
|
||||||
DialogDescription, |
|
||||||
DialogHeader, |
|
||||||
DialogTitle, |
|
||||||
DialogTrigger |
|
||||||
} from '@renderer/components/ui/dialog' |
|
||||||
import { |
|
||||||
DropdownMenu, |
|
||||||
DropdownMenuContent, |
|
||||||
DropdownMenuItem, |
|
||||||
DropdownMenuTrigger |
|
||||||
} from '@renderer/components/ui/dropdown-menu' |
|
||||||
import { Input } from '@renderer/components/ui/input' |
|
||||||
import { useFetchProfile } from '@renderer/hooks' |
|
||||||
import { generateImageByPubkey } from '@renderer/lib/pubkey' |
|
||||||
import { toProfile } from '@renderer/lib/link' |
|
||||||
import { useSecondaryPage } from '@renderer/PageManager' |
|
||||||
import { useNostr } from '@renderer/providers/NostrProvider' |
|
||||||
import { LogIn } from 'lucide-react' |
|
||||||
import { useState } from 'react' |
|
||||||
|
|
||||||
export default function AccountButton() { |
|
||||||
const { pubkey } = useNostr() |
|
||||||
|
|
||||||
if (pubkey) { |
|
||||||
return <ProfileButton pubkey={pubkey} /> |
|
||||||
} else { |
|
||||||
return <LoginButton /> |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function ProfileButton({ pubkey }: { pubkey: string }) { |
|
||||||
const { logout } = useNostr() |
|
||||||
const { avatar } = useFetchProfile(pubkey) |
|
||||||
const { push } = useSecondaryPage() |
|
||||||
const defaultAvatar = generateImageByPubkey(pubkey) |
|
||||||
|
|
||||||
return ( |
|
||||||
<DropdownMenu> |
|
||||||
<DropdownMenuTrigger className="non-draggable"> |
|
||||||
<Avatar className="w-6 h-6 hover:opacity-90"> |
|
||||||
<AvatarImage src={avatar} /> |
|
||||||
<AvatarFallback> |
|
||||||
<img src={defaultAvatar} /> |
|
||||||
</AvatarFallback> |
|
||||||
</Avatar> |
|
||||||
</DropdownMenuTrigger> |
|
||||||
<DropdownMenuContent> |
|
||||||
<DropdownMenuItem onClick={() => push(toProfile(pubkey))}>Profile</DropdownMenuItem> |
|
||||||
<DropdownMenuItem className="text-destructive focus:text-destructive" onClick={logout}> |
|
||||||
Logout |
|
||||||
</DropdownMenuItem> |
|
||||||
</DropdownMenuContent> |
|
||||||
</DropdownMenu> |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
function LoginButton() { |
|
||||||
const { canLogin, login } = useNostr() |
|
||||||
const [open, setOpen] = useState(false) |
|
||||||
const [nsec, setNsec] = useState('') |
|
||||||
const [errMsg, setErrMsg] = useState<string | null>(null) |
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
|
||||||
setNsec(e.target.value) |
|
||||||
setErrMsg(null) |
|
||||||
} |
|
||||||
|
|
||||||
const handleLogin = () => { |
|
||||||
if (nsec === '') return |
|
||||||
|
|
||||||
login(nsec).catch((err) => { |
|
||||||
setErrMsg(err.message) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<Dialog open={open} onOpenChange={setOpen}> |
|
||||||
<DialogTrigger asChild> |
|
||||||
<Button variant="titlebar" size="titlebar"> |
|
||||||
<LogIn /> |
|
||||||
</Button> |
|
||||||
</DialogTrigger> |
|
||||||
<DialogContent className="w-80"> |
|
||||||
<DialogHeader> |
|
||||||
<DialogTitle>Sign in</DialogTitle> |
|
||||||
{!canLogin && ( |
|
||||||
<DialogDescription className="text-destructive"> |
|
||||||
Encryption is not available in your device. |
|
||||||
</DialogDescription> |
|
||||||
)} |
|
||||||
</DialogHeader> |
|
||||||
<div className="space-y-1"> |
|
||||||
<Input |
|
||||||
type="password" |
|
||||||
placeholder="nsec1.." |
|
||||||
value={nsec} |
|
||||||
onChange={handleInputChange} |
|
||||||
className={errMsg ? 'border-destructive' : ''} |
|
||||||
disabled={!canLogin} |
|
||||||
/> |
|
||||||
{errMsg && <div className="text-xs text-destructive pl-3">{errMsg}</div>} |
|
||||||
</div> |
|
||||||
<Button onClick={handleLogin} disabled={!canLogin}> |
|
||||||
Login |
|
||||||
</Button> |
|
||||||
</DialogContent> |
|
||||||
</Dialog> |
|
||||||
) |
|
||||||
} |
|
||||||
@ -0,0 +1,11 @@ |
|||||||
|
import { TElectronWindow } from '@common/types' |
||||||
|
|
||||||
|
export function isElectron(w: any): w is TElectronWindow { |
||||||
|
return !!w.electron && !!w.api |
||||||
|
} |
||||||
|
|
||||||
|
export function isMacOS() { |
||||||
|
return isElectron(window) && window.electron.process.platform === 'darwin' |
||||||
|
} |
||||||
|
|
||||||
|
export const IS_ELECTRON = isElectron(window) |
||||||
@ -1,3 +0,0 @@ |
|||||||
export function isMacOS() { |
|
||||||
return window.electron.process.platform === 'darwin' |
|
||||||
} |
|
||||||
@ -1,21 +1,18 @@ |
|||||||
import NoteList from '@renderer/components/NoteList' |
import NoteList from '@renderer/components/NoteList' |
||||||
import SecondaryPageLayout from '@renderer/layouts/SecondaryPageLayout' |
import SecondaryPageLayout from '@renderer/layouts/SecondaryPageLayout' |
||||||
import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider' |
import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider' |
||||||
|
import NotFoundPage from '../NotFoundPage' |
||||||
|
|
||||||
export default function HashtagPage({ hashtag }: { hashtag?: string }) { |
export default function HashtagPage({ id }: { id?: string }) { |
||||||
const { relayUrls } = useRelaySettings() |
const { relayUrls } = useRelaySettings() |
||||||
if (!hashtag) { |
if (!id) { |
||||||
return null |
return <NotFoundPage /> |
||||||
} |
} |
||||||
const normalizedHashtag = hashtag.toLowerCase() |
const hashtag = id.toLowerCase() |
||||||
|
|
||||||
return ( |
return ( |
||||||
<SecondaryPageLayout titlebarContent={`# ${normalizedHashtag}`}> |
<SecondaryPageLayout titlebarContent={`# ${hashtag}`}> |
||||||
<NoteList |
<NoteList key={hashtag} filter={{ '#t': [hashtag] }} relayUrls={relayUrls} /> |
||||||
key={normalizedHashtag} |
|
||||||
filter={{ '#t': [normalizedHashtag] }} |
|
||||||
relayUrls={relayUrls} |
|
||||||
/> |
|
||||||
</SecondaryPageLayout> |
</SecondaryPageLayout> |
||||||
) |
) |
||||||
} |
} |
||||||
|
|||||||
@ -1,6 +1,6 @@ |
|||||||
import SecondaryPageLayout from '@renderer/layouts/SecondaryPageLayout' |
import SecondaryPageLayout from '@renderer/layouts/SecondaryPageLayout' |
||||||
|
|
||||||
export default function BlankPage() { |
export default function HomePage() { |
||||||
return ( |
return ( |
||||||
<SecondaryPageLayout hideBackButton> |
<SecondaryPageLayout hideBackButton> |
||||||
<div className="text-muted-foreground w-full h-full flex items-center justify-center"> |
<div className="text-muted-foreground w-full h-full flex items-center justify-center"> |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
import SecondaryPageLayout from '@renderer/layouts/SecondaryPageLayout' |
||||||
|
|
||||||
|
export default function LoadingPage({ title }: { title?: string }) { |
||||||
|
return ( |
||||||
|
<SecondaryPageLayout titlebarContent={title}> |
||||||
|
<div className="text-muted-foreground text-center"> |
||||||
|
<div>Loading...</div> |
||||||
|
</div> |
||||||
|
</SecondaryPageLayout> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,19 @@ |
|||||||
|
import { Button } from '@renderer/components/ui/button' |
||||||
|
import SecondaryPageLayout from '@renderer/layouts/SecondaryPageLayout' |
||||||
|
import { toHome } from '@renderer/lib/link' |
||||||
|
import { useSecondaryPage } from '@renderer/PageManager' |
||||||
|
|
||||||
|
export default function NotFoundPage() { |
||||||
|
const { push } = useSecondaryPage() |
||||||
|
return ( |
||||||
|
<SecondaryPageLayout hideBackButton> |
||||||
|
<div className="text-muted-foreground w-full h-full flex flex-col items-center justify-center gap-2"> |
||||||
|
<div>Lost in the void 🌌</div> |
||||||
|
<div>(404)</div> |
||||||
|
<Button variant="secondary" onClick={() => push(toHome())}> |
||||||
|
Carry me home |
||||||
|
</Button> |
||||||
|
</div> |
||||||
|
</SecondaryPageLayout> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,21 @@ |
|||||||
|
import { match } from 'path-to-regexp' |
||||||
|
import { isValidElement } from 'react' |
||||||
|
import FollowingListPage from './pages/secondary/FollowingListPage' |
||||||
|
import HashtagPage from './pages/secondary/HashtagPage' |
||||||
|
import HomePage from './pages/secondary/HomePage' |
||||||
|
import NotePage from './pages/secondary/NotePage' |
||||||
|
import ProfilePage from './pages/secondary/ProfilePage' |
||||||
|
|
||||||
|
const ROUTES = [ |
||||||
|
{ path: '/', element: <HomePage /> }, |
||||||
|
{ path: '/note/:id', element: <NotePage /> }, |
||||||
|
{ path: '/user/:id', element: <ProfilePage /> }, |
||||||
|
{ path: '/user/:id/following', element: <FollowingListPage /> }, |
||||||
|
{ path: '/hashtag/:id', element: <HashtagPage /> } |
||||||
|
] |
||||||
|
|
||||||
|
export const routes = ROUTES.map(({ path, element }) => ({ |
||||||
|
path, |
||||||
|
element: isValidElement(element) ? element : null, |
||||||
|
matcher: match(path) |
||||||
|
})) |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
import { defineConfig } from 'vite' |
||||||
|
import react from '@vitejs/plugin-react' |
||||||
|
import path from 'path' |
||||||
|
|
||||||
|
export default defineConfig({ |
||||||
|
root: path.resolve(__dirname, 'src/renderer'), |
||||||
|
build: { |
||||||
|
outDir: path.resolve(__dirname, 'dist/web'), |
||||||
|
emptyOutDir: true |
||||||
|
}, |
||||||
|
resolve: { |
||||||
|
alias: { |
||||||
|
'@renderer': path.resolve('src/renderer/src') |
||||||
|
} |
||||||
|
}, |
||||||
|
plugins: [react()] |
||||||
|
}) |
||||||
Loading…
Reference in new issue