Skip to content

Commit

Permalink
⛩️🍯 ↝ [SSG-103 SSG-104]: Navbar & profile edit form
Browse files Browse the repository at this point in the history
  • Loading branch information
Gizmotronn committed Jan 20, 2025
1 parent 1d05f05 commit 60eb555
Show file tree
Hide file tree
Showing 9 changed files with 437 additions and 297 deletions.
14 changes: 14 additions & 0 deletions app/account/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use client';

import { EarthViewLayout } from "@/components/(scenes)/planetScene/layout";
import ProfileSetupForm from "@/components/Account/ProfileSetup";
import Navbar from "@/components/Layout/Navbar";

export default function AccountPage() {
return (
<EarthViewLayout>
<Navbar />
<ProfileSetupForm />
</EarthViewLayout>
);
};
2 changes: 2 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { EarthViewLayout } from "@/components/(scenes)/planetScene/layout";
import Onboarding from "./scenes/onboarding/page";
import VerticalToolbar from "@/components/Layout/Toolbar";
import SimpleeMissionGuide from "./tests/singleMissionGuide";
import Navbar from "@/components/Layout/Navbar";

export default function Home() {
const session = useSession();
Expand All @@ -39,6 +40,7 @@ export default function Home() {
// />,
30: <EarthViewLayout>
<div className="w-full">
<Navbar />
<div className="flex flex-row space-y-4"></div>
<div className="py-3">
<div className="py-1">
Expand Down
4 changes: 2 additions & 2 deletions app/tests/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"use client";

import JournalPage from "@/components/Structures/Missions/Stardust/Journal";
import ProfileSetupForm from "@/components/Account/ProfileSetup";

export default function TestPage() {
return (
// <StarnetLayout>
<>
<JournalPage />
<ProfileSetupForm />
</>
// {/* </StarnetLayout> */}
);
Expand Down
77 changes: 76 additions & 1 deletion components/Account/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React, { useEffect, useState } from "react";
import Image from "next/image";
import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react";

interface Props {
author: string;
Expand All @@ -25,4 +27,77 @@ export const AvatarGenerator: React.FC<Props> = ({ author }) => {
)}
</div>
);
};
};

export function Avatar() {
const supabase = useSupabaseClient();
const session = useSession();

const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
let ignore = false;

async function fetchAvatar() {
if (!session?.user?.id) return;

const { data, error } = await supabase
.from("profiles")
.select("avatar_url")
.eq("id", session.user.id)
.single();

if (!ignore) {
if (error) {
console.warn("Error fetching avatar:", error.message);
} else if (data) {
setAvatarUrl(data.avatar_url || null);
}
setLoading(false);
}
}

fetchAvatar();
return () => {
ignore = true;
};
}, [session, supabase]);

if (loading) {
return (
<div className="w-16 h-16 rounded-full bg-gray-300 animate-pulse" />
);
}

return (
<div className="w-16 h-16 rounded-full overflow-hidden bg-gray-300 border-2 border-gray-400 shadow-md">
{avatarUrl ? (
<Image
src={`${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/avatars/${avatarUrl}`}
alt="User Avatar"
layout="fill"
objectFit="cover"
className="w-full h-full object-cover"
/>
) : (
<div className="flex items-center justify-center h-full text-gray-500">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-8 w-8"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
</div>
)}
</div>
);
}
212 changes: 212 additions & 0 deletions components/Account/ProfileSetup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import React, { useEffect, useState, type ChangeEvent } from "react";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react";

export default function ProfileSetupForm() {
const supabase = useSupabaseClient();
const session = useSession();
const router = useRouter();

const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const [username, setUsername] = useState("");
const [firstName, setFirstName] = useState("");
const [avatar, setAvatar] = useState<File | null>(null);
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);

useEffect(() => {
let ignore = false;
async function getProfile() {
setLoading(true);
const { data, error } = await supabase
.from("profiles")
.select(`username, full_name, avatar_url`)
.eq("id", session?.user?.id)
.single();

if (!ignore) {
if (error) {
console.warn(error);
} else if (data) {
setUsername(data.username);
setFirstName(data?.full_name);
setAvatarPreview(data?.avatar_url || "");
}
}

setLoading(false);
}

if (session?.user?.id) {
getProfile();
}

return () => {
ignore = true;
};
}, [session, supabase]);

const handleAvatarChange = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setAvatar(file);
const reader = new FileReader();
reader.onloadend = () => {
setAvatarPreview(reader.result as string);
};
reader.readAsDataURL(file);
}
};

async function updateProfile(event: React.FormEvent) {
event.preventDefault();
if (!username || !session?.user?.email) {
setError("Username and email are required.");
return;
}

setLoading(true);
setError(null);

let avatar_url = avatarPreview;

if (avatar) {
const fileName = `${Date.now()}-${session?.user?.id}-avatar.png`;
const { data, error } = await supabase.storage
.from("avatars")
.upload(fileName, avatar, {
contentType: avatar.type,
});

if (error) {
setError(error.message);
setLoading(false);
return;
}

avatar_url = `${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/avatars/${data?.path}`;
}

const updates = {
id: session?.user?.id,
username,
full_name: firstName,
avatar_url,
updated_at: new Date(),
};

const { error } = await supabase.from("profiles").upsert(updates);

if (error) {
alert(error.message);
} else {
router.push("/"); // Redirect to the home page
}

setLoading(false);
}

return (
<div className="min-h-screen flex items-center justify-center bg-[#1D2833] p-4 bg-[url('/game-background.jpg')] bg-cover bg-center">
<div className="bg-[#2C4F64]/90 p-8 rounded-3xl shadow-2xl max-w-md w-full backdrop-blur-sm border border-[#5FCBC3]/30 transform hover:scale-105 transition-all duration-300">
<h1 className="text-4xl font-bold text-[#FFE3BA] mb-8 text-center tracking-wide">
My Profile
</h1>
<form onSubmit={updateProfile} className="space-y-6">
<div className="group">
<label
htmlFor="username"
className="block text-[#5FCBC3] mb-2 font-semibold"
>
Username
</label>
<input
type="text"
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full px-4 py-3 bg-[#74859A]/50 text-[#FFE3BA] rounded-xl focus:outline-none focus:ring-2 focus:ring-[#FF695D] transition-all duration-300 placeholder-[#FFE3BA]/50"
required
placeholder="Enter your username"
/>
</div>
<div className="group">
<label
htmlFor="firstName"
className="block text-[#5FCBC3] mb-2 font-semibold"
>
First Name
</label>
<input
type="text"
id="firstName"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
className="w-full px-4 py-3 bg-[#74859A]/50 text-[#FFE3BA] rounded-xl focus:outline-none focus:ring-2 focus:ring-[#FF695D] transition-all duration-300 placeholder-[#FFE3BA]/50"
required
placeholder="Enter your first name"
/>
</div>
<div>
<label
htmlFor="avatar"
className="block text-[#5FCBC3] mb-2 font-semibold"
>
Avatar
</label>
<div className="flex items-center space-x-4">
<div className="relative w-24 h-24 rounded-full overflow-hidden bg-[#74859A]/50 border-4 border-[#5FCBC3] shadow-lg">
{avatarPreview ? (
<Image
src={`${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/avatars/${avatarPreview}` || "/placeholder.svg"}
alt="Avatar preview"
layout="fill"
objectFit="cover"
/>
) : (
<div className="flex items-center justify-center h-full text-[#FFE3BA]">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-12 w-12"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
</div>
)}
</div>
<input
type="file"
id="avatar"
accept="image/*"
onChange={handleAvatarChange}
className="hidden"
/>
<label
htmlFor="avatar"
className="px-4 py-2 bg-[#5FCBC3] text-[#1D2833] rounded-xl cursor-pointer hover:bg-[#FF695D] transition-colors duration-300 font-semibold shadow-md hover:shadow-lg"
>
Choose Avatar
</label>
</div>
</div>
<button
type="submit"
className="w-full py-4 bg-[#FF695D] text-[#FFE3BA] rounded-xl font-bold hover:bg-[#5FCBC3] transition-all duration-300 transform hover:scale-105 shadow-lg hover:shadow-xl"
>
Update your profile
</button>
</form>
</div>
</div>
);
};
Loading

0 comments on commit 60eb555

Please sign in to comment.