| 1 | import { Card } from "@/components/ui/card"; |
| 2 | import { Avatar, AvatarFallback } from "@/components/ui/avatar"; |
| 3 | import { |
| 4 | Settings, |
| 5 | Bell, |
| 6 | Shield, |
| 7 | HelpCircle, |
| 8 | FileText, |
| 9 | LogOut, |
| 10 | ChevronRight, |
| 11 | } from "lucide-react"; |
| 12 | import { Button } from "@/components/ui/button"; |
| 13 | import { useLang } from "@/context/LanguageContext"; |
| 14 | import { useAccount } from "@/context/AccountContext"; |
| 15 | import haptics from "@/utils/haptics"; |
| 16 | import { useEffect } from "react"; |
| 17 | import { debug, error } from "@tauri-apps/plugin-log"; |
| 18 | import { invoke } from "@tauri-apps/api/core"; |
| 19 | import { OauthRedirect, TokenResponse } from "@/lib/crate/generated"; |
| 20 | import { listen } from "@tauri-apps/api/event"; |
| 21 | import { openUrl } from "@tauri-apps/plugin-opener"; |
| 22 | |
| 23 | const Profile = () => { |
| 24 | const { t, lang } = useLang(); |
| 25 | const isRTL = lang === "fa"; |
| 26 | const { onLoggedIn, loggedIn, user, onLoggedOut } = useAccount(); |
| 27 | |
| 28 | const menuItems = [ |
| 29 | { |
| 30 | section: t.accountSection, |
| 31 | items: [ |
| 32 | { icon: Settings, label: t.settings, to: "/settings" }, |
| 33 | { icon: Bell, label: t.notifications, to: "/notifications" }, |
| 34 | { icon: Shield, label: t.security, to: "/security" }, |
| 35 | ], |
| 36 | }, |
| 37 | { |
| 38 | section: t.supportSection, |
| 39 | items: [ |
| 40 | { icon: HelpCircle, label: t.helpCenter, to: "/help" }, |
| 41 | { icon: FileText, label: t.termsPrivacy, to: "/terms" }, |
| 42 | ], |
| 43 | }, |
| 44 | ]; |
| 45 | |
| 46 | const handleOAuthLogin = async (provider: "google" | "apple") => { |
| 47 | try { |
| 48 | // Handle all operations in the Rust backend. |
| 49 | const port = await invoke<number>("start_server"); |
| 50 | debug(`OAuth server started on port ${port}`); |
| 51 | // Tauri OAuth plugin server. |
| 52 | const redirectUri = `http://localhost:${port}`; |
| 53 | if (provider === "google") { |
| 54 | const baseUrl = await invoke<string>("get_consent_url", { |
| 55 | redirectUri, |
| 56 | }); |
| 57 | debug(`Google OAuth URL: ${baseUrl}`); |
| 58 | await openUrl(baseUrl); |
| 59 | } |
| 60 | } catch (e) { |
| 61 | error(`Error: ${e}`); |
| 62 | } |
| 63 | }; |
| 64 | |
| 65 | const handleLogout = async () => { |
| 66 | onLoggedOut(); |
| 67 | }; |
| 68 | |
| 69 | const init = async () => { |
| 70 | try { |
| 71 | // Listen for OAuth emitted events from the Rust backend. |
| 72 | await listen<OauthRedirect>("oauth_redirect_uri", async (event) => { |
| 73 | debug( |
| 74 | `Received OAuth redirect event: ${JSON.stringify(event.payload)}`, |
| 75 | ); |
| 76 | const oauthRedirect = event.payload; |
| 77 | // Handle the redirect event here |
| 78 | try { |
| 79 | const res = await invoke<TokenResponse>("get_token", { |
| 80 | oauthRedirect, |
| 81 | }); |
| 82 | debug(`OAuth response: ${JSON.stringify(res)}`); |
| 83 | onLoggedIn("google", res.access_token); |
| 84 | } catch (e) { |
| 85 | error(`Error processing OAuth response: ${JSON.stringify(e)}`); |
| 86 | } |
| 87 | }); |
| 88 | } catch (e) { |
| 89 | error(`Error listening OAuth event: ${e}`); |
| 90 | } |
| 91 | }; |
| 92 | |
| 93 | useEffect(() => { |
| 94 | init(); |
| 95 | // eslint-disable-next-line react-hooks/exhaustive-deps |
| 96 | }, [loggedIn]); |
| 97 | |
| 98 | return ( |
| 99 | <div className="p-4 space-y-6" dir={isRTL ? "rtl" : "ltr"}> |
| 100 | {!loggedIn && ( |
| 101 | <div className="px-8 py-4 mt-2 w-full max-w-xs"> |
| 102 | <button |
| 103 | onClick={async () => { |
| 104 | await haptics.buttonPress(); |
| 105 | await handleOAuthLogin("google"); |
| 106 | }} |
| 107 | className="w-full bg-white hover:bg-gray-50 active:bg-gray-100 text-gray-700 font-medium rounded-lg px-4 py-3 flex items-center justify-center gap-3 transition-colors border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 shadow-sm" |
| 108 | aria-label={t.googleOAuthLogin} |
| 109 | > |
| 110 | <svg |
| 111 | className="w-5 h-5" |
| 112 | viewBox="0 0 24 24" |
| 113 | xmlns="http://www.w3.org/2000/svg" |
| 114 | > |
| 115 | <path |
| 116 | d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" |
| 117 | fill="#4285F4" |
| 118 | /> |
| 119 | <path |
| 120 | d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" |
| 121 | fill="#34A853" |
| 122 | /> |
| 123 | <path |
| 124 | d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" |
| 125 | fill="#FBBC05" |
| 126 | /> |
| 127 | <path |
| 128 | d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" |
| 129 | fill="#EA4335" |
| 130 | /> |
| 131 | </svg> |
| 132 | <span className="text-sm font-medium leading-none"> |
| 133 | {t.googleOAuthLogin} |
| 134 | </span> |
| 135 | </button> |
| 136 | </div> |
| 137 | )} |
| 138 | {/* LOGGED IN */} |
| 139 | {user && ( |
| 140 | <> |
| 141 | {/* Profile Header */} |
| 142 | <Card className="p-8 border-0 shadow-lg"> |
| 143 | <div className="flex items-center gap-5"> |
| 144 | <Avatar className="h-24 w-24"> |
| 145 | <AvatarFallback className="bg-primary text-primary-foreground text-3xl font-bold"> |
| 146 | <img |
| 147 | src={user.picture} |
| 148 | alt="Avatar" |
| 149 | className="w-24 h-24 mb-4 rounded-full border-4 border-food-orange bg-gradient-to-br from-food-orange to-food-red " |
| 150 | /> |
| 151 | </AvatarFallback> |
| 152 | </Avatar> |
| 153 | <div className="flex-1"> |
| 154 | <h2 className="text-2xl font-bold mb-1">{user.name}</h2> |
| 155 | <p className="text-sm text-muted-foreground font-medium"> |
| 156 | {user.email} |
| 157 | </p> |
| 158 | <p className="text-sm text-muted-foreground font-medium mt-1"> |
| 159 | ID: EMP-001 |
| 160 | </p> |
| 161 | </div> |
| 162 | </div> |
| 163 | </Card> |
| 164 | |
| 165 | {/* Stats */} |
| 166 | <div className="grid grid-cols-3 gap-4"> |
| 167 | <Card className="p-5 text-center border-0 shadow-md"> |
| 168 | <p className="text-3xl font-bold text-foreground">24</p> |
| 169 | <p className="text-xs text-muted-foreground mt-2 font-medium"> |
| 170 | {t.expenses} |
| 171 | </p> |
| 172 | </Card> |
| 173 | <Card className="p-5 text-center border-0 shadow-md"> |
| 174 | <p className="text-xl font-bold text-expense-positive">$2.4K</p> |
| 175 | <p className="text-xs text-muted-foreground mt-2 font-medium"> |
| 176 | {t.approved} |
| 177 | </p> |
| 178 | </Card> |
| 179 | <Card className="p-5 text-center border-0 shadow-md"> |
| 180 | <p className="text-3xl font-bold text-foreground">3</p> |
| 181 | <p className="text-xs text-muted-foreground mt-2 font-medium"> |
| 182 | {t.pending} |
| 183 | </p> |
| 184 | </Card> |
| 185 | </div> |
| 186 | |
| 187 | {/* Menu Sections */} |
| 188 | {menuItems.map((section) => ( |
| 189 | <div key={section.section} className="space-y-3"> |
| 190 | <h3 className="text-sm font-bold text-muted-foreground px-1 uppercase tracking-wider"> |
| 191 | {section.section} |
| 192 | </h3> |
| 193 | <Card className="divide-y border-0 shadow-md"> |
| 194 | {section.items.map((item) => ( |
| 195 | <button |
| 196 | key={item.label} |
| 197 | className="w-full px-5 py-4 flex items-center justify-between hover:bg-muted/50 transition-colors first:rounded-t-lg last:rounded-b-lg" |
| 198 | > |
| 199 | <div className="flex items-center gap-4"> |
| 200 | <item.icon className="h-5 w-5 text-muted-foreground" /> |
| 201 | <span className="font-semibold">{item.label}</span> |
| 202 | </div> |
| 203 | <ChevronRight |
| 204 | className={`h-5 w-5 text-muted-foreground ${isRTL ? "rotate-180" : ""}`} |
| 205 | /> |
| 206 | </button> |
| 207 | ))} |
| 208 | </Card> |
| 209 | </div> |
| 210 | ))} |
| 211 | |
| 212 | {/* Logout Button */} |
| 213 | <Button |
| 214 | variant="outline" |
| 215 | className="w-full" |
| 216 | size="lg" |
| 217 | onClick={handleLogout} |
| 218 | > |
| 219 | <LogOut className={`h-5 w-5 ${isRTL ? "ml-2" : "mr-2"}`} /> |
| 220 | {t.logOut} |
| 221 | </Button> |
| 222 | </> |
| 223 | )} |
| 224 | <div className="text-center text-sm text-muted-foreground font-medium pb-4"> |
| 225 | {t.version} 1.0.0 |
| 226 | </div> |
| 227 | </div> |
| 228 | ); |
| 229 | }; |
| 230 | |
| 231 | export default Profile; |