diff --git a/nepalingo-web/package.json b/nepalingo-web/package.json index 3638f47..be10438 100644 --- a/nepalingo-web/package.json +++ b/nepalingo-web/package.json @@ -23,6 +23,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-ga4": "^2.1.0", + "react-icons": "^5.2.1", "react-router-dom": "^6.24.1", "react-supabase": "^0.2.0", "swr": "^2.2.5" diff --git a/nepalingo-web/pnpm-lock.yaml b/nepalingo-web/pnpm-lock.yaml index 2e3eeaa..760d250 100644 --- a/nepalingo-web/pnpm-lock.yaml +++ b/nepalingo-web/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: react-ga4: specifier: ^2.1.0 version: 2.1.0 + react-icons: + specifier: ^5.2.1 + version: 5.2.1(react@18.3.1) react-router-dom: specifier: ^6.24.1 version: 6.24.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1842,6 +1845,11 @@ packages: react-ga4@2.1.0: resolution: {integrity: sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ==} + react-icons@5.2.1: + resolution: {integrity: sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==} + peerDependencies: + react: '*' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -4166,6 +4174,10 @@ snapshots: react-ga4@2.1.0: {} + react-icons@5.2.1(react@18.3.1): + dependencies: + react: 18.3.1 + react-is@16.13.1: {} react-refresh@0.14.2: {} diff --git a/nepalingo-web/src/App.tsx b/nepalingo-web/src/App.tsx index c4dcd31..6f1f737 100644 --- a/nepalingo-web/src/App.tsx +++ b/nepalingo-web/src/App.tsx @@ -12,6 +12,7 @@ import { PrivateRoutes } from "@/components/PrivateRoutes"; import FeedbackForm from "@/components/FeedbackForm"; import TestYourself from "@/pages/TestYourself"; import SignUp from "./pages/SignUp"; +import ProfilePage from "@/pages/ProfilePage"; const App: React.FC = () => { const TrackingID = import.meta.env.VITE_GOOGLE_ANALYTICS_TRACKING_ID; @@ -38,6 +39,7 @@ const App: React.FC = () => { } /> } /> } /> + } /> diff --git a/nepalingo-web/src/components/ProfileEditForm.tsx b/nepalingo-web/src/components/ProfileEditForm.tsx new file mode 100644 index 0000000..4684040 --- /dev/null +++ b/nepalingo-web/src/components/ProfileEditForm.tsx @@ -0,0 +1,153 @@ +import React, { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { supabaseClient } from "@/config/supabase-client"; +import { useAuth} from "@/hooks/Auth"; +import CustomTextInput from "./CustomTextInput"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faUser, faPen, faCamera } from "@fortawesome/free-solid-svg-icons"; + +const ProfileEditForm: React.FC = () => { + const navigate = useNavigate(); + const { user, refetchUser } = useAuth(); + const [avatarUrl, setAvatarUrl] = useState( + user?.user_metadata.avatar_url || "" + ); + const [username, setUsername] = useState(user?.user_metadata.username || ""); + const [status, setStatus] = useState(user?.user_metadata.status || ""); + const [isDragging, setIsDragging] = useState(false); + + const handleAvatarChange = async (e: React.ChangeEvent) => { + if (e.target.files && e.target.files[0]) { + const file = e.target.files[0]; + const filePath = `public/${file.name}`; + + // Upload file to Supabase Storage + const { data, error: uploadError } = await supabaseClient.storage + .from('Avatars') + .upload(filePath, file); + + if (uploadError) { + console.error('Error uploading file:', uploadError.message); + return; + } + + // Construct the URL of the uploaded file + const uploadedAvatarUrl = `https://your-supabase-instance.supabase.co/storage/v1/object/public/avatars/${file.name}`; + setAvatarUrl(uploadedAvatarUrl); + } + }; + + const handleDrop = async (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + if (e.dataTransfer.files && e.dataTransfer.files[0]) { + const file = e.dataTransfer.files[0]; + const filePath = `public/${file.name}`; + + // Upload file to Supabase Storage + const { data, error: uploadError } = await supabaseClient.storage + .from('Avatars') + .upload(filePath, file); + + if (uploadError) { + console.error('Error uploading file:', uploadError.message); + return; + } + + // Construct the URL of the uploaded file + const uploadedAvatarUrl = `https://your-supabase-instance.supabase.co/storage/v1/object/public/avatars/${file.name}`; + setAvatarUrl(uploadedAvatarUrl); + } + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(true); + }; + + const handleDragLeave = () => setIsDragging(false); + + const handleSaveChanges = async () => { + if (!user) return; + + // Update user metadata in Supabase + const { error } = await supabaseClient.auth.updateUser({ + data: { + username, // Update username directly in Supabase + avatar_url: avatarUrl, + status, + }, + }); + + + // Refetch user data to update UI + await refetchUser(); + + // Navigate to the home page + navigate("/"); + }; + + return ( +
+
+ User Avatar +
+ +
+ +
+ +
+ setUsername(e.target.value)} + /> + + setStatus(e.target.value)} + /> + + +
+
+ ); +}; +export default ProfileEditForm; diff --git a/nepalingo-web/src/components/UserAvatar.tsx b/nepalingo-web/src/components/UserAvatar.tsx index e7dda8d..8434ce2 100644 --- a/nepalingo-web/src/components/UserAvatar.tsx +++ b/nepalingo-web/src/components/UserAvatar.tsx @@ -1,17 +1,38 @@ -import React from "react"; +import React, { HTMLAttributes } from "react"; import { useAuth } from "@/hooks/Auth"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faPen } from "@fortawesome/free-solid-svg-icons"; -const UserAvatar: React.FC = () => { +interface UserAvatarProps extends HTMLAttributes { + showPenOnHover?: boolean; +} + +const UserAvatar: React.FC = ({ + onClick, + showPenOnHover = false, + ...props +}) => { const { user } = useAuth(); const username = user?.user_metadata?.username; const avatarUrl = `https://robohash.org/${username}.png?set=set4`; return ( - User Avatar +
+ User Avatar + {showPenOnHover && ( +
+ +
+ )} +
); }; diff --git a/nepalingo-web/src/components/header/UserProfile.tsx b/nepalingo-web/src/components/header/UserProfile.tsx index 546806a..97206e1 100644 --- a/nepalingo-web/src/components/header/UserProfile.tsx +++ b/nepalingo-web/src/components/header/UserProfile.tsx @@ -4,13 +4,17 @@ import { StreakContext } from "@/hooks/StreakContext"; import { getPhrase } from "@/components/header/StreakPhrase"; import { useAuth } from "@/hooks/Auth"; import fire from "@/assets/fire.svg"; +import { useNavigate } from "react-router-dom"; +import { supabaseClient } from "@/config/supabase-client"; // Import Supabase client const UserProfile: React.FC = () => { const [isOpen, setIsOpen] = useState(false); + const [status, setStatus] = useState(null); const { user, signOut } = useAuth(); const { currentStreak, longestStreak } = useContext(StreakContext); const phrase = getPhrase(currentStreak); const dropdownRef = useRef(null); + const navigate = useNavigate(); const toggleMenu = () => { setIsOpen(!isOpen); @@ -25,6 +29,10 @@ const UserProfile: React.FC = () => { } }; + const handleAvatarClick = () => { + navigate("/profile-edit"); + }; + useEffect(() => { document.addEventListener("mousedown", handleClickOutside); return () => { @@ -32,6 +40,27 @@ const UserProfile: React.FC = () => { }; }, []); + // Fetch the status from the UpdateUser table + useEffect(() => { + const fetchStatus = async () => { + if (user) { + const { data, error } = await supabaseClient + .from("updateUser") + .select("status") + .eq("username", user.user_metadata.username) + .single(); + + if (error) { + console.error("Error fetching status:", error.message); + } else { + setStatus(data?.status || ""); // Set status or empty string + } + } + }; + + fetchStatus(); + }, [user]); + return (