PurpleAccount.tsx (6778B)
1 import { ArrowUpRight, Star, Check, LogOut, X } from "lucide-react"; 2 import { Button } from "../ui/Button"; 3 import { FormattedMessage, useIntl } from "react-intl"; 4 import Link from "next/link"; 5 import Image from "next/image"; 6 import { useEffect, useRef, useState } from "react"; 7 import { AccountInfo, Profile, getProfile, getPurpleAccountInfo } from "@/utils/PurpleUtils"; 8 import { useLocalStorage } from "usehooks-ts"; 9 import { ErrorDialog } from "../ErrorDialog"; 10 import { PurpleLayout } from "../PurpleLayout"; 11 import { usePurpleLoginSession } from "@/hooks/usePurpleLoginSession"; 12 13 14 export function PurpleAccount() { 15 const intl = useIntl() 16 const [error, setError] = useState<string | null>(null) 17 const { accountInfo: loggedInAccountInfo, logout } = usePurpleLoginSession(setError) 18 const [profile, setProfile] = useState<Profile | null>(null) 19 const [pubkey, setPubkey] = useState<string | null>(null) 20 21 // MARK: - Functions 22 23 const fetchProfile = async () => { 24 if (!pubkey) { 25 return 26 } 27 try { 28 const profile = await getProfile(pubkey) 29 setProfile(profile) 30 } 31 catch (e) { 32 console.error(e) 33 setError("Failed to get profile info from the relay. Please wait a few minutes and refresh the page. If the problem persists, please contact support.") 34 } 35 } 36 37 // MARK: - Effects and hooks 38 39 // Load the profile when the pubkey changes 40 useEffect(() => { 41 if (pubkey) { 42 fetchProfile() 43 } 44 }, [pubkey]) 45 46 useEffect(() => { 47 if (loggedInAccountInfo) { 48 setPubkey(loggedInAccountInfo.pubkey) 49 } 50 else if (loggedInAccountInfo === null) { 51 // Redirect to the login page 52 window.location.href = "/purple/login?redirect=" + encodeURIComponent("/purple/account") 53 } 54 }, [loggedInAccountInfo]) 55 56 // MARK: - Render 57 58 return (<> 59 <ErrorDialog error={error} setError={setError} /> 60 <PurpleLayout> 61 {((profile || profile === null) && pubkey) && (<> 62 <div className="mt-2 mb-4 flex flex-col items-center"> 63 <div className="mt-4 flex flex-col gap-1 items-center justify-center mb-4"> 64 <Image src={profile?.picture || ("https://robohash.org/" + (profile?.pubkey || pubkey))} width={128} height={128} className="rounded-full" alt={profile?.name || intl.formatMessage({ id: "purple.account.unknown-user", defaultMessage: "Generic user avatar" })} /> 65 <div className="flex flex-wrap gap-1 items-center"> 66 <div className="text-purple-50 font-semibold text-3xl"> 67 {profile?.name || "No name"} 68 </div> 69 <div className="text-purple-200/70 font-normal text-xs flex gap-1"> 70 <Star strokeWidth={1.75} className="w-4 h-4 shrink-0 text-amber-400" fill="currentColor" /> 71 {loggedInAccountInfo?.created_at && unixTimestampToDateString(loggedInAccountInfo?.created_at)} 72 </div> 73 </div> 74 {loggedInAccountInfo?.active ? ( 75 <div className="flex gap-1 bg-gradient-to-r from-damuspink-500 to-damuspink-600 rounded-full px-3 py-1 items-center mt-3 mb-6"> 76 <div className="w-4 h-4 rounded-full bg-white flex justify-center items-center"> 77 <Check className="w-2 h-2 shrink-0 text-damuspink-500" strokeWidth={5} /> 78 </div> 79 <div className="text-white font-semibold text-sm"> 80 {intl.formatMessage({ id: "purple.account.active", defaultMessage: "Active account" })} 81 </div> 82 </div> 83 ) : ( 84 <div className="flex gap-1 bg-red-200 rounded-full px-3 py-1 items-center mt-3 mb-6"> 85 <div className="w-4 h-4 rounded-full bg-red-500 flex justify-center items-center"> 86 <X className="w-2 h-2 shrink-0 text-red-200" strokeWidth={5} /> 87 </div> 88 <div className="text-red-500 font-semibold text-sm"> 89 {intl.formatMessage({ id: "purple.account.expired", defaultMessage: "Expired account" })} 90 </div> 91 </div> 92 )} 93 </div> 94 <div className="flex flex-col bg-purple-50/10 rounded-xl px-4 text-purple-50 w-full"> 95 <AccountInfoRow label={intl.formatMessage({ id: "purple.account.expiry-date", defaultMessage: "Expiry date" })} value={(loggedInAccountInfo?.expiry && unixTimestampToDateString(loggedInAccountInfo?.expiry)) || "N/A"} /> 96 <AccountInfoRow label={intl.formatMessage({ id: "purple.account.account-creation", defaultMessage: "Account creation" })} value={(loggedInAccountInfo?.created_at && unixTimestampToDateString(loggedInAccountInfo?.created_at)) || "N/A"} /> 97 <AccountInfoRow label={intl.formatMessage({ id: "purple.account.subscriber-number", defaultMessage: "Subscriber number" })} value={(loggedInAccountInfo?.subscriber_number && "#" + loggedInAccountInfo?.subscriber_number) || "N/A"} last={loggedInAccountInfo?.testflight_url ? false : true} /> 98 {loggedInAccountInfo?.testflight_url && <Link href={loggedInAccountInfo?.testflight_url} target="_blank"> 99 <div className="py-2 border-b border-purple-200/20"> 100 <Button variant="link" className="w-full text-left"> 101 <ArrowUpRight className="text-damuspink-600 mr-2" /> 102 {intl.formatMessage({ id: "purple.account.testflight-link", defaultMessage: "Join TestFlight" })} 103 </Button> 104 </div> 105 </Link>} 106 <div className="py-2"> 107 <Link href="/notedeck/install" target="_blank"> 108 <Button variant="link" className="w-full text-left"> 109 <ArrowUpRight className="text-damuspink-600 mr-2" /> 110 {intl.formatMessage({ id: "purple.account.notedeck-install-link", defaultMessage: "Try Notedeck" })} 111 </Button> 112 </Link> 113 </div> 114 </div> 115 <Button className="w-full md:w-auto opacity-70 hover:opacity-100 transition mt-4 text-sm" onClick={() => logout()} variant="link"> 116 <LogOut className="text-damuspink-600 mr-2" /> 117 {intl.formatMessage({ id: "purple.account.sign-out", defaultMessage: "Sign out" })} 118 </Button> 119 </div> 120 </>)} 121 </PurpleLayout> 122 </>) 123 } 124 125 function AccountInfoRow({ label, value, last }: { label: string, value: string | number, last?: boolean }) { 126 return ( 127 <div className={`flex gap-2 items-center justify-between ${last ? '' : 'border-b border-purple-200/20'} py-4`}> 128 <div className="font-bold"> 129 {label} 130 </div> 131 <div> 132 {value} 133 </div> 134 </div> 135 ); 136 } 137 138 function unixTimestampToDateString(timestamp: number) { 139 return new Date(timestamp * 1000).toLocaleDateString() 140 }