EditPage.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. import React, { useState, useEffect } from 'react';
  2. import { useNavigate } from 'react-router-dom';
  3. import type { ContactData, SocialLinksData } from './types';
  4. import LinkedInIcon from './components/icons/LinkedInIcon';
  5. import XIcon from './components/icons/XIcon';
  6. import GithubIcon from './components/icons/GithubIcon';
  7. import TiktokIcon from './components/icons/TiktokIcon';
  8. import InstagramIcon from './components/icons/InstagramIcon';
  9. import FacebookIcon from './components/icons/FacebookIcon';
  10. import DiscordIcon from './components/icons/DiscordIcon';
  11. import VerifiedIcon from './components/icons/VerifiedIcon';
  12. import VirtualIdCard from './components/VirtualIdCard';
  13. import UploadIcon from './components/icons/UploadIcon';
  14. import AuthenticityScore from './components/AuthenticityScore';
  15. import BrainCircuitIcon from './components/icons/BrainCircuitIcon';
  16. import ArrowRightIcon from './components/icons/ArrowRightIcon';
  17. interface EditPageProps {
  18. initialData: ContactData;
  19. onSave: (data: ContactData) => void;
  20. }
  21. // Reusable Section Component for editable fields
  22. const EditSection: React.FC<{title: string, children: React.ReactNode}> = ({ title, children }) => (
  23. <div className="bg-white rounded-2xl border border-slate-200 p-6 space-y-4">
  24. <h2 className="text-xl font-bold text-slate-800 mb-4">{title}</h2>
  25. {children}
  26. </div>
  27. );
  28. const socialPlatforms = [
  29. {
  30. key: 'linkedin', name: 'LinkedIn', icon: <LinkedInIcon />,
  31. brandClasses: {
  32. iconBg: 'bg-gradient-to-br from-blue-500 to-sky-400',
  33. button: 'bg-blue-600 hover:bg-blue-700',
  34. }
  35. },
  36. {
  37. key: 'x', name: 'X (Twitter)', icon: <XIcon />,
  38. brandClasses: {
  39. iconBg: 'bg-gradient-to-br from-slate-900 to-slate-700',
  40. button: 'bg-slate-800 hover:bg-slate-900',
  41. }
  42. },
  43. {
  44. key: 'github', name: 'GitHub', icon: <GithubIcon />,
  45. brandClasses: {
  46. iconBg: 'bg-gradient-to-br from-slate-900 to-slate-700',
  47. button: 'bg-slate-800 hover:bg-slate-900',
  48. }
  49. },
  50. {
  51. key: 'tiktok', name: 'TikTok', icon: <TiktokIcon />,
  52. brandClasses: {
  53. iconBg: 'bg-gradient-to-br from-black via-rose-500 to-cyan-400',
  54. button: 'bg-black hover:bg-gray-800',
  55. }
  56. },
  57. {
  58. key: 'instagram', name: 'Instagram', icon: <InstagramIcon />,
  59. brandClasses: {
  60. iconBg: 'bg-gradient-to-br from-purple-500 via-pink-500 to-yellow-500',
  61. button: 'bg-pink-600 hover:bg-pink-700',
  62. }
  63. },
  64. {
  65. key: 'facebook', name: 'Facebook', icon: <FacebookIcon />,
  66. brandClasses: {
  67. iconBg: 'bg-gradient-to-br from-blue-700 to-blue-500',
  68. button: 'bg-blue-700 hover:bg-blue-800',
  69. }
  70. },
  71. {
  72. key: 'discord', name: 'Discord', icon: <DiscordIcon />,
  73. brandClasses: {
  74. iconBg: 'bg-gradient-to-br from-indigo-600 to-purple-500',
  75. button: 'bg-indigo-600 hover:bg-indigo-700',
  76. }
  77. },
  78. ] as const;
  79. const EditPage: React.FC<EditPageProps> = ({ initialData, onSave }) => {
  80. const navigate = useNavigate();
  81. const [formData, setFormData] = useState<ContactData>(initialData);
  82. useEffect(() => {
  83. const handleOauthMessage = (event: MessageEvent) => {
  84. if (event.origin !== window.location.origin) {
  85. return;
  86. }
  87. const { type, platform, profileUrl, error } = event.data;
  88. if (type === 'oauth-success' && platform && profileUrl) {
  89. setFormData(prev => ({
  90. ...prev,
  91. socials: {
  92. ...prev.socials,
  93. [platform]: profileUrl
  94. }
  95. }));
  96. } else if (type === 'oauth-error') {
  97. console.error(`OAuth Error for ${platform}:`, error);
  98. alert(`Failed to connect ${platform}. Please try again.`);
  99. }
  100. };
  101. window.addEventListener('message', handleOauthMessage);
  102. return () => {
  103. window.removeEventListener('message', handleOauthMessage);
  104. };
  105. }, []);
  106. useEffect(() => {
  107. // Recalculate authenticity score when dependencies change
  108. const connectedSocials = Object.values(formData.socials).filter(link => !!link).length;
  109. let socialScore = 0;
  110. if (connectedSocials === 1) {
  111. socialScore = 15;
  112. } else if (connectedSocials === 2) {
  113. socialScore = 30;
  114. } else if (connectedSocials > 2) {
  115. socialScore = 30 + (connectedSocials - 2) * 5;
  116. }
  117. const finalSocialScore = Math.min(socialScore, 45);
  118. const videoScore = formData.videoIntroUrl ? 15 : 0;
  119. // Only update if the scores have changed to prevent infinite loops
  120. if (
  121. finalSocialScore !== formData.authenticityScore.socialBinding ||
  122. videoScore !== formData.authenticityScore.videoVerification
  123. ) {
  124. setFormData(prev => ({
  125. ...prev,
  126. authenticityScore: {
  127. ...prev.authenticityScore,
  128. socialBinding: finalSocialScore,
  129. videoVerification: videoScore,
  130. }
  131. }));
  132. }
  133. }, [formData.socials, formData.videoIntroUrl, formData.authenticityScore]);
  134. const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
  135. const { name, value } = e.target;
  136. setFormData(prev => ({
  137. ...prev,
  138. [name]: value,
  139. }));
  140. };
  141. const handleVideoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  142. const file = e.target.files?.[0];
  143. if (file) {
  144. const videoUrl = URL.createObjectURL(file);
  145. setFormData(prev => ({
  146. ...prev,
  147. videoIntroUrl: videoUrl,
  148. videoPosterUrl: '' // Reset poster for new video
  149. }));
  150. }
  151. };
  152. const handleSocialConnect = (platform: keyof SocialLinksData) => {
  153. const isConnected = !!formData.socials[platform];
  154. if (isConnected) {
  155. // Disconnect logic
  156. const newSocials = { ...formData.socials, [platform]: '' };
  157. setFormData(prev => ({ ...prev, socials: newSocials }));
  158. return;
  159. }
  160. // --- Real OAuth URLs ---
  161. const redirectUri = `${window.location.origin}/oauth-callback.html`;
  162. let oauthUrl = '';
  163. switch (platform) {
  164. case 'github':
  165. oauthUrl = `https://github.com/login/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=${encodeURIComponent(redirectUri)}&scope=read:user&state=github`;
  166. break;
  167. case 'linkedin':
  168. oauthUrl = `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=${encodeURIComponent(redirectUri)}&state=linkedin&scope=profile%20email%20openid`;
  169. break;
  170. case 'x':
  171. oauthUrl = `https://twitter.com/i/oauth2/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=${encodeURIComponent(redirectUri)}&scope=users.read%20tweet.read&state=x&code_challenge=challenge&code_challenge_method=plain`;
  172. break;
  173. case 'tiktok':
  174. oauthUrl = `https://www.tiktok.com/v2/auth/authorize?client_key=YOUR_CLIENT_KEY&scope=user.info.basic&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&state=tiktok`;
  175. break;
  176. case 'instagram':
  177. oauthUrl = `https://api.instagram.com/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=${encodeURIComponent(redirectUri)}&scope=user_profile,user_media&response_type=code&state=instagram`;
  178. break;
  179. case 'facebook':
  180. oauthUrl = `https://www.facebook.com/v19.0/dialog/oauth?client_id=YOUR_CLIENT_ID&redirect_uri=${encodeURIComponent(redirectUri)}&state=facebook&scope=public_profile,email`;
  181. break;
  182. case 'discord':
  183. oauthUrl = `https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=identify%20email&state=discord`;
  184. break;
  185. }
  186. // --- Popup Window Logic ---
  187. const width = 600;
  188. const height = 700;
  189. const left = (window.innerWidth - width) / 2;
  190. const top = (window.innerHeight - height) / 2;
  191. window.open(
  192. oauthUrl,
  193. 'socialLogin',
  194. `width=${width},height=${height},left=${left},top=${top},toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes`
  195. );
  196. };
  197. const handleSubmit = (e: React.FormEvent) => {
  198. e.preventDefault();
  199. onSave(formData);
  200. navigate('/contact');
  201. };
  202. const totalScore = formData.authenticityScore.videoVerification + formData.authenticityScore.socialBinding + formData.authenticityScore.cloneMaturity;
  203. return (
  204. <main className="max-w-3xl mx-auto p-4 sm:p-6 md:p-8">
  205. <form onSubmit={handleSubmit} className="space-y-8">
  206. <div className="flex justify-between items-center mb-4">
  207. <h1 className="text-2xl font-bold text-slate-800">Certify My Homepage</h1>
  208. <div>
  209. <button type="button" onClick={() => navigate('/contact')} className="px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-md hover:bg-slate-50 mr-2">Cancel</button>
  210. <button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700">Save Changes</button>
  211. </div>
  212. </div>
  213. <VirtualIdCard
  214. name={formData.name}
  215. avatarUrl={formData.avatarUrl}
  216. virtualId={formData.virtualId}
  217. totalScore={totalScore}
  218. />
  219. <AuthenticityScore
  220. videoVerificationScore={formData.authenticityScore.videoVerification}
  221. socialBindingScore={formData.authenticityScore.socialBinding}
  222. cloneMaturityScore={formData.authenticityScore.cloneMaturity}
  223. />
  224. <div className="bg-sky-50 border border-sky-200 text-sky-800 rounded-xl p-3 text-center text-sm -mt-4">
  225. <p>At your current score, you'll earn <span className="font-bold">100 tokens/minute</span> from calls.</p>
  226. </div>
  227. <div
  228. onClick={() => navigate('/clone-chat')}
  229. className="bg-white rounded-2xl border border-slate-200 p-6 group cursor-pointer transition-colors duration-300 ease-in-out hover:bg-slate-50"
  230. role="button"
  231. aria-label="Train My Digital Clone"
  232. >
  233. <div className="flex items-center gap-4">
  234. <div className="flex-shrink-0 w-12 h-12 rounded-2xl flex items-center justify-center bg-gradient-to-br from-indigo-500 to-purple-600 text-white group-hover:scale-110 transition-transform duration-300">
  235. <BrainCircuitIcon />
  236. </div>
  237. <div className="flex-grow">
  238. <h2 className="text-xl font-bold text-slate-800">Train My Digital Clone</h2>
  239. <p className="text-sm text-slate-600 mt-1">
  240. Improve your clone's quality score by continuously chatting with it.
  241. </p>
  242. <p className="text-sm text-indigo-600 font-medium mt-1">
  243. A higher score leads to greater call earnings.
  244. </p>
  245. </div>
  246. <div className="text-slate-400 group-hover:text-slate-800 transition-transform duration-300 group-hover:translate-x-1">
  247. <ArrowRightIcon className="w-5 h-5" />
  248. </div>
  249. </div>
  250. </div>
  251. <EditSection title="About Me">
  252. <label htmlFor="bio" className="sr-only">About Me</label>
  253. <textarea
  254. id="bio"
  255. name="bio"
  256. rows={5}
  257. className="w-full px-3 py-2 text-slate-700 bg-white border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition"
  258. placeholder="Tell us about yourself..."
  259. value={formData.bio}
  260. onChange={handleInputChange}
  261. />
  262. </EditSection>
  263. <EditSection title="Video Introduction">
  264. <label htmlFor="video-upload" className="block aspect-video w-full rounded-lg bg-slate-200 relative overflow-hidden group cursor-pointer">
  265. <video key={formData.videoIntroUrl} poster={formData.videoPosterUrl} src={formData.videoIntroUrl} className="w-full h-full object-cover" />
  266. <div className="absolute inset-0 bg-black/60 flex flex-col items-center justify-center text-white opacity-0 group-hover:opacity-100 transition-opacity duration-300">
  267. <UploadIcon />
  268. <span className="mt-2 block font-semibold">Upload New Video</span>
  269. <span className="text-xs text-slate-300">Click or drag & drop</span>
  270. </div>
  271. <input id="video-upload" type="file" onChange={handleVideoChange} accept="video/*" className="sr-only"/>
  272. </label>
  273. </EditSection>
  274. <EditSection title="Verified Social Accounts">
  275. <p className="text-sm text-slate-600">Connect your social accounts to verify your identity. This enhances your Authenticity Score.</p>
  276. <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
  277. {socialPlatforms.map((platform) => {
  278. const isConnected = !!formData.socials[platform.key];
  279. if (!isConnected) {
  280. return (
  281. <div key={platform.key} className="flex items-center p-4 rounded-xl border-2 border-dashed border-slate-200 bg-slate-50 transition-colors hover:border-slate-300 hover:bg-slate-100">
  282. <div className="flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center text-slate-400 bg-slate-200">
  283. {platform.icon}
  284. </div>
  285. <div className="ml-4 flex-grow">
  286. <h3 className="font-bold text-slate-700 text-sm">{platform.name}</h3>
  287. <p className="text-xs text-slate-500">Not Connected</p>
  288. </div>
  289. <button
  290. type="button"
  291. onClick={() => handleSocialConnect(platform.key)}
  292. className={`px-3 py-1.5 text-xs font-semibold text-white ${platform.brandClasses.button} rounded-md transition-transform transform hover:scale-105`}
  293. >
  294. Connect
  295. </button>
  296. </div>
  297. );
  298. }
  299. return (
  300. <div key={platform.key} className="flex items-center p-4 rounded-xl border border-slate-200 bg-white">
  301. <div className={`flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center text-white ${platform.brandClasses.iconBg}`}>
  302. {platform.icon}
  303. </div>
  304. <div className="ml-4 flex-grow overflow-hidden">
  305. <div className="flex items-center">
  306. <h3 className="font-bold text-slate-800 text-sm">{platform.name}</h3>
  307. <div className="ml-2 flex items-center bg-green-100 text-green-800 text-xs font-medium px-2 py-0.5 rounded-full">
  308. <VerifiedIcon className="w-3 h-3 mr-1" />
  309. Verified
  310. </div>
  311. </div>
  312. <p className="text-xs text-slate-500 font-mono truncate max-w-[120px] sm:max-w-xs">{formData.socials[platform.key].replace(/^https?:\/\/(www\.)?/, '')}</p>
  313. </div>
  314. <button
  315. type="button"
  316. onClick={() => handleSocialConnect(platform.key)}
  317. className="text-xs font-semibold text-red-600 hover:text-red-800 hover:bg-red-100 px-2 py-1 rounded-md transition-colors"
  318. >
  319. Disconnect
  320. </button>
  321. </div>
  322. );
  323. })}
  324. </div>
  325. </EditSection>
  326. </form>
  327. </main>
  328. );
  329. };
  330. export default EditPage;