main
tsx 231 lines 8.24 KB
Raw
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;