Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions nepalingo-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
12 changes: 12 additions & 0 deletions nepalingo-web/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions nepalingo-web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,6 +39,7 @@ const App: React.FC = () => {
<Route path="/flashcard" element={<FlashcardPage />} />
<Route path="/dictionary" element={<DictionaryPage />} />
<Route path="/test-yourself" element={<TestYourself />} />
<Route path="/profile-edit" element={<ProfilePage />} />
</Route>
</Routes>
</Router>
Expand Down
153 changes: 153 additions & 0 deletions nepalingo-web/src/components/ProfileEditForm.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>) => {
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<HTMLDivElement>) => {
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<HTMLDivElement>) => {
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 (
<div className="flex flex-col items-center justify-center min-h-screen bg-black text-white">
<div
className={`relative w-40 h-40 rounded-full overflow-hidden group cursor-pointer ${isDragging ? "border-4 border-dashed border-white" : "border-2 border-gray-600"}`}
onDragOver={handleDragOver}
onDrop={handleDrop}
onDragLeave={handleDragLeave}
>
<img
src={avatarUrl || "/default-avatar.png"}
alt="User Avatar"
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50 rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<FontAwesomeIcon icon={faCamera} className="text-white text-xl" />
</div>
<input
type="file"
accept="image/*"
onChange={handleAvatarChange}
className="absolute inset-0 opacity-0 cursor-pointer"
/>
</div>

<div className="mt-8 w-3/4 max-w-xl">
<CustomTextInput
label="Username"
name="username"
required
autoComplete="name"
placeholder="eg: bird24"
iconProps={{
icon: faUser,
className: "text-white",
}}
value={username}
onChange={(e) => setUsername(e.target.value)}
/>

<CustomTextInput
label="Status"
name="status"
required
autoComplete="status"
placeholder="Enter your status"
iconProps={{
icon: faPen,
className: "text-white",
}}
value={status}
onChange={(e) => setStatus(e.target.value)}
/>

<button
onClick={handleSaveChanges}
className="w-full mt-6 bg-primary text-white p-3 rounded-lg hover:bg-secondary transition duration-200"
>
Save Changes
</button>
</div>
</div>
);
};
export default ProfileEditForm;
35 changes: 28 additions & 7 deletions nepalingo-web/src/components/UserAvatar.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement> {
showPenOnHover?: boolean;
}

const UserAvatar: React.FC<UserAvatarProps> = ({
onClick,
showPenOnHover = false,
...props
}) => {
const { user } = useAuth();
const username = user?.user_metadata?.username;
const avatarUrl = `https://robohash.org/${username}.png?set=set4`;

return (
<img
src={avatarUrl}
alt="User Avatar"
className="w-full h-full rounded-full"
/>
<div
className={`relative w-full h-full ${showPenOnHover ? "group cursor-pointer" : ""}`}
onClick={onClick}
{...props}
>
<img
src={avatarUrl}
alt="User Avatar"
className="w-full h-full rounded-full"
/>
{showPenOnHover && (
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50 rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<FontAwesomeIcon icon={faPen} className="text-white" />
</div>
)}
</div>
);
};

Expand Down
37 changes: 35 additions & 2 deletions nepalingo-web/src/components/header/UserProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(null);
const { user, signOut } = useAuth();
const { currentStreak, longestStreak } = useContext(StreakContext);
const phrase = getPhrase(currentStreak);
const dropdownRef = useRef<HTMLDivElement>(null);
const navigate = useNavigate();

const toggleMenu = () => {
setIsOpen(!isOpen);
Expand All @@ -25,13 +29,38 @@ const UserProfile: React.FC = () => {
}
};

const handleAvatarClick = () => {
navigate("/profile-edit");
};

useEffect(() => {
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);

// 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 (
<div className="relative inline-block text-left" ref={dropdownRef}>
<button
Expand All @@ -46,8 +75,12 @@ const UserProfile: React.FC = () => {
{isOpen && (
<div className="absolute right-0 z-10 mt-2 w-64 rounded-lg shadow-lg bg-[#2B2B2B] p-4">
<div className="flex flex-col items-center">
<div className="w-16 h-16">
<UserAvatar />
<p className="text-sm text-gray-400">
My moto: <span className="text-sm text-white">{status}</span>
</p>

<div className="w-16 h-16 mt-1">
<UserAvatar onClick={handleAvatarClick} showPenOnHover />
</div>
<span className="mt-1 text-white font-primary font-black">
{user?.user_metadata?.username}
Expand Down
16 changes: 16 additions & 0 deletions nepalingo-web/src/hooks/Auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type AuthContextProps = {
data: SignUpWithPasswordCredentials,
) => Promise<AuthTokenResponsePassword>;
resetPasswordEmail: (email: string) => Promise<{ error: Error | null }>;
refetchUser: () => Promise<void>; // Added refetchUser
};

const AuthContext = createContext<AuthContextProps>({
Expand All @@ -29,6 +30,7 @@ const AuthContext = createContext<AuthContextProps>({
supabaseClient.auth.resetPasswordForEmail(email, {
redirectTo: "https://www.nepalingo.com/reset-password",
}),
refetchUser: async () => {}, // Added refetchUser
});

export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
Expand Down Expand Up @@ -63,6 +65,19 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
};
}, []);

const refetchUser = async () => {
const {
data: { session },
error,
} = await supabaseClient.auth.getSession();
if (error) {
console.error("Error fetching user:", error.message);
return;
}
setSession(session);
setUser(session?.user || null);
};

const value: AuthContextProps = {
session,
user,
Expand All @@ -73,6 +88,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
supabaseClient.auth.resetPasswordForEmail(email, {
redirectTo: "https://www.nepalingo.com/reset-password",
}),
refetchUser, // Include refetchUser in the context
};

return (
Expand Down
2 changes: 1 addition & 1 deletion nepalingo-web/src/hooks/StreakContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React, {
useEffect,
ReactNode,
} from "react";
import { supabaseClient } from "@/config/supabase-client";
import { supabaseClient } from "@/config/supabase-client";
import { useAuth } from "@/hooks/Auth";

interface StreakContextProps {
Expand Down
16 changes: 16 additions & 0 deletions nepalingo-web/src/pages/ProfilePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// src/pages/ProfileEditPage.tsx
import React from "react";
import ProfileEditForm from "@/components/ProfileEditForm";

const ProfileEditPage: React.FC = () => {
return (
<div className="min-h-screen bg-black-100 flex items-center justify-center p-4">
<div className="w-full max-w-lg bg-white shadow-lg rounded-lg p-6">
<h1 className="text-3xl font-bold mb-6">Edit Profile</h1>
<ProfileEditForm />
</div>
</div>
);
};

export default ProfileEditPage;