Skip to content

Commit

Permalink
feat: implement profile settings feature (#808)
Browse files Browse the repository at this point in the history
* feat: add profile update api function

* refactor: minor updates

* feat: add settings na d profile route

* feat: add toast support and validation

* chore: minor api call fixes

* refactor: remove unwanted code

* refactor: update default values

* refactor: add timezone array to select options

* refactor: add cursor indication for disabled input

* feat: create useAuthSession hook for user data

* chore: guard settings page

* refactor: update input placeholders

* fix: build and language chart issues

* refactor: cleanups and finishing touches

* chore: remove unused code

* refactor: update user profile link icon

* Update components/organisms/UserSettingsPage/user-settings-page.tsx

Co-authored-by: Brian Douglas <[email protected]>

* Update components/organisms/UserSettingsPage/user-settings-page.tsx

Co-authored-by: Brian Douglas <[email protected]>

* Update components/organisms/UserSettingsPage/user-settings-page.tsx

Co-authored-by: Brian Douglas <[email protected]>

* chore: update Db user type

* feat: implement update user email preference

* refactor: cleanup anf fininshing touches

* chore: general refactor and cleanup

* chore: update api call response

* fix: build error

---------

Co-authored-by: Brian Douglas <[email protected]>
  • Loading branch information
OgDev-01 and bdougie authored Feb 1, 2023
1 parent 050a4b4 commit 4a8d706
Show file tree
Hide file tree
Showing 19 changed files with 1,554 additions and 33 deletions.
2 changes: 1 addition & 1 deletion components/atoms/Avatar/avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const Avatar = (props: AvatarProps): JSX.Element => {
case "string":
return <DefaultAvatar {...props} avatarURL={imageSource} />;
case "number":
return <CustomAvatar {...props} />;
return <CustomAvatar {...props} avatarURL={imageSource} />;

default:
return <span>invalid avatar size props!!!</span>;
Expand Down
4 changes: 3 additions & 1 deletion components/atoms/Checkbox/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ interface CheckboxProps extends React.ComponentProps<typeof SupbaseCheckboxCompo
const Checkbox: React.FC<CheckboxProps> = (props) => {
return (
<SupbaseCheckboxComponent
className={`checked:[&>*]:!bg-orange-500 !text-orange-500 ${props.className || ""}`}
className={`checked:[&>*]:!bg-orange-500 disabled:[&>*]:!cursor-not-allowed !text-orange-500 ${
props.className || ""
}`}
{...props}
/>
);
Expand Down
26 changes: 16 additions & 10 deletions components/atoms/TextInput/text-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const TextInput = ({
"flex-1 px-3 text-light-slate-12 bg-white shadow-input border transition rounded-lg py-1 flex items-center",
borderless && "!border-none",
state === "invalid" ? " focus-within:border-light-red-10 " : "focus-within:border-light-orange-9 ",
disabled && "bg-light-slate-3",
disabled && "bg-light-slate-3 text-light-slate-6",
classNames
)}
>
Expand All @@ -57,18 +57,24 @@ const TextInput = ({
placeholder={placeholder || ""}
onChange={onChange}
value={value}
className={`flex-1 focus:outline-none ${classNames}`}
className={`flex-1 focus:outline-none ${classNames} ${
disabled && "bg-light-slate-3 cursor-not-allowed text-light-slate-9"
}`}
autoFocus={autoFocus}
disabled={disabled}
/>
{state === "valid" ? (
<CheckCircleFillIcon className="text-light-orange-9" size={14} />
) : !!value ? (
<span className="flex items-center" onClick={() => handleResetInput()}>
<XCircleFillIcon className="text-light-red-11" size={14} />
</span>
) : (
""
{!disabled && (
<>
{state === "valid" ? (
<CheckCircleFillIcon className="text-light-orange-9" size={14} />
) : !!value ? (
<span className="flex items-center" onClick={() => handleResetInput()}>
<XCircleFillIcon className="text-light-red-11" size={14} />
</span>
) : (
""
)}
</>
)}
</div>
</label>
Expand Down
31 changes: 27 additions & 4 deletions components/molecules/AuthSection/auth-section.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import Image from "next/image";
import Link from "next/link";

import { FiLogOut, FiSettings } from "react-icons/fi";
import { Divider } from "@supabase/ui";

import useSession from "lib/hooks/useSession";
import useSupabaseAuth from "../../../lib/hooks/useSupabaseAuth";

import PersonIcon from "img/icons/person-icon.svg";
import notifications from "../../../img/notifications.svg";
import downArrow from "../../../img/chevron-down.svg";
import Avatar from "components/atoms/Avatar/avatar";
Expand All @@ -7,19 +16,33 @@ import userAvatar from "../../../img/ellipse-1.png";
import OnboardingButton from "../OnboardingButton/onboarding-button";
import DropdownList from "../DropdownList/dropdown-list";
import Text from "components/atoms/Typography/text";
import { Divider } from "@supabase/ui";
import useSupabaseAuth from "../../../lib/hooks/useSupabaseAuth";
import { FiLogOut } from "react-icons/fi";
import GitHubIcon from "img/icons/github-icon.svg";
import Icon from "components/atoms/Icon/icon";
import useSession from "lib/hooks/useSession";

const AuthSection: React.FC = ({}) => {
const { signIn, signOut, user } = useSupabaseAuth();
const { onboarded } = useSession();

const authMenu = {
authed: [
<Link
href={`/user/${user?.user_metadata.user_name}`}
key="settings"
className="group flex gap-x-3 text-lg hover:bg-light-orange-3 items-center px-4 py-2 rounded-md cursor-pointer transition"
>
<div className="w-5 h-5 flex justify-center items-center bg-blue-100 rounded-full">
<Image width={10} height={10} alt="Icon" src={PersonIcon} />
</div>
<Text className="group-hover:text-light-orange-10">{user?.user_metadata.user_name}</Text>
</Link>,
<Link
href="/user/settings"
key="settings"
className="group flex gap-x-3 text-lg hover:bg-light-orange-3 items-center px-4 py-2 rounded-md cursor-pointer transition"
>
<FiSettings className="group-hover:text-light-orange-10" />
<Text className="group-hover:text-light-orange-10">Settings</Text>
</Link>,
<span
onClick={async () => await signOut()}
key="authorized"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const ContributorProfilePage = ({
</div>
<div>
<p className="mb-4">Languages</p>
<CardHorizontalBarChart withDescription={false} languageList={languageList} />
<CardHorizontalBarChart withDescription={true} languageList={languageList} />
</div>
</div>
<div className="flex-1">
Expand Down
160 changes: 146 additions & 14 deletions components/organisms/UserSettingsPage/user-settings-page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";

import { User } from "@supabase/supabase-js";

import Button from "components/atoms/Button/button";
import Checkbox from "components/atoms/Checkbox/checkbox";
Expand All @@ -8,10 +10,56 @@ import Select from "components/atoms/Select/select";
import SelectOption from "components/atoms/Select/select-option";
import LanguagePill from "components/atoms/LanguagePill/LanguagePill";

const UserSettingsPage = () => {
import { updateUser } from "lib/hooks/update-user";
import { ToastTrigger } from "lib/utils/toast-trigger";
import { authSession } from "lib/hooks/authSession";
import { validateEmail } from "lib/utils/validate-email";
import { timezones } from "lib/utils/timezones";
import { updateEmailPreferences } from "lib/hooks/updateEmailPreference";
import { useFetchUser } from "lib/hooks/useFetchUser";

interface userSettingsPageProps {
user: User | null;
}

type EmailPreferenceType = {
display_email?: boolean;
receive_collaboration?: boolean;
};
const UserSettingsPage = ({ user }: userSettingsPageProps) => {
const { data: insightsUser } = useFetchUser(user?.user_metadata.user_name);

const [isValidEmail, setIsValidEmail] = useState<boolean>(true);
const [userInfo, setUserInfo] = useState<DbUser>();
const [email, setEmail] = useState<string | undefined>(userInfo?.email || user?.email);
const [emailPreference, setEmailPreference] = useState<EmailPreferenceType>({
// eslint-disable-next-line camelcase
display_email: false,
// eslint-disable-next-line camelcase
receive_collaboration: false
});
const [selectedInterest, setSelectedInterest] = useState<string[]>([]);
const interestArray = ["javascript", "python", "rust", "ML", "AI", "react"];

useEffect(() => {
async function fetchAuthSession() {
const response = await authSession();
if (response !== false) setUserInfo(response);
}

if (user) setEmail(user.email);
if (insightsUser) {
setEmailPreference({
// eslint-disable-next-line camelcase
display_email: insightsUser?.display_email,
// eslint-disable-next-line camelcase
receive_collaboration: insightsUser?.receive_collaboration
});
setSelectedInterest(insightsUser?.interests.split(","));
}
fetchAuthSession();
}, [user, insightsUser]);

const handleSelectInterest = (interest: string) => {
if (selectedInterest.length > 0 && selectedInterest.includes(interest)) {
setSelectedInterest((prev) => prev.filter((item) => item !== interest));
Expand All @@ -20,23 +68,65 @@ const UserSettingsPage = () => {
}
};

const handleUpdateEmailPreference = async () => {
const data = await updateEmailPreferences({ ...emailPreference });
if (data) {
ToastTrigger({ message: "Updated successfully", type: "success" });
} else {
ToastTrigger({ message: "An error occured!!!", type: "error" });
}
};

const handleUpdateInterest = async () => {
const data = await updateUser({
data: { interests: selectedInterest },
params: "interests"
});
if (data) {
ToastTrigger({ message: "Updated successfully", type: "success" });
} else {
ToastTrigger({ message: "An error occured!!!", type: "error" });
}
};
const handleUpdateProfile = async () => {
const data = await updateUser({
data: { email }
});
if (data) {
ToastTrigger({ message: "Updated successfully", type: "success" });
} else {
ToastTrigger({ message: "An error occured!!!", type: "error" });
}
};

return (
<div>
<div className="flex flex-col md:flex-row md:justify-between gap-4 text-sm text-light-slate-11">
<div className="flex flex-col md:flex-row md:gap-48 gap-4 text-sm text-light-slate-11">
<div>
<Title className="!text-2xl !text-light-slate-11" level={2}>
Public profile
</Title>
<form className="flex flex-col gap-6 mt-6">
<form onSubmit={(e) => e.preventDefault()} className="flex flex-col gap-6 mt-6">
<TextInput
classNames="bg-light-slate-4 text-light-slate-11 font-medium"
label="Name*"
placeholder="April O'Neil"
value={user?.user_metadata.full_name}
disabled
/>
<TextInput
classNames="bg-light-slate-4 text-light-slate-11 font-medium"
placeholder="[email protected]"
onChange={(e) => {
setEmail(e.target.value);
if (validateEmail(e.target.value)) {
setIsValidEmail(true);
} else {
setIsValidEmail(false);
}
}}
label="Email*"
value={email}
/>

{/* Bio section */}
Expand All @@ -45,42 +135,61 @@ const UserSettingsPage = () => {
<textarea
rows={4}
placeholder="Tell us about yourself."
className="bg-light-slate-4 rounded-lg px-3 py-2 "
className="bg-light-slate-4 rounded-lg px-3 py-2 disabled:cursor-not-allowed "
readOnly
value={
userInfo?.bio ||
"I am an open source developer with a passion for music and video games. I strive to improve the open source community and am always looking for new ways to contribute."
}
></textarea>
</div>
<TextInput
classNames="bg-light-slate-4 text-light-slate-11 font-medium"
placeholder="https://turtlepower.pizza"
label="URL"
disabled
/>
<TextInput
classNames="bg-light-slate-4 text-light-slate-11"
placeholder="@aprilcodes"
label="Twitter Username"
disabled
value={`@${(userInfo && userInfo.twitter_username) || "saucedopen"}`}
/>
<TextInput
classNames="bg-light-slate-4 text-light-slate-11 font-medium"
placeholder="StockGen"
label="Company"
disabled
value={userInfo?.company || "OpenSauced"}
/>
<TextInput
classNames="bg-light-slate-4 text-light-slate-11 font-medium"
placeholder="USA"
label="Location"
disabled
value={userInfo?.location || "Canada"}
/>
<div>
<Checkbox value={"true"} title="profile email" label="Display current local time" />
<Checkbox checked={false} title="profile email" label="Display current local time on profile" />
<span className="ml-7 text-light-slate-9 text-sm font-normal">
Other users will see the time difference from their local time.
</span>
</div>
<div className="flex flex-col gap-2">
<label>Time zone*</label>
<Select>
<SelectOption value="Wat+1">Select time zone</SelectOption>
<SelectOption value="select timezone">Select time zone</SelectOption>
{timezones.map((timezone, index) => (
<SelectOption key={index} value={timezone.value}>
{timezone.text}
</SelectOption>
))}
</Select>
</div>
<Button type="primary">Update profile</Button>
<Button disabled={!isValidEmail} onClick={handleUpdateProfile} type="primary">
Update profile
</Button>
</form>
</div>
<div className="flex flex-col-reverse md:flex-col gap-6">
Expand All @@ -98,19 +207,42 @@ const UserSettingsPage = () => {
/>
))}
</div>
<button className="px-4 w-max py-2 rounded-lg bg-light-slate-4 border border-light-slate-8">
<Button
type="default"
disabled={selectedInterest.length === 0}
onClick={handleUpdateInterest}
className="!px-4 !text-light-slate-11 !py-2 !bg-light-slate-4"
>
Update Interests
</button>
</Button>
</div>
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-3 ">
<label className="text-light-slate-11 text-2xl font-normal">Email Preferences</label>
<Checkbox value={"true"} title="profile email" label="Display Email On Profile" />
<Checkbox value={"true"} title="collaboration requests" label="Receive collaboration requests" />
<Checkbox
// eslint-disable-next-line camelcase
onChange={() => setEmailPreference((prev) => ({ ...prev, display_email: !prev.display_email }))}
checked={emailPreference.display_email}
title="profile email"
label="Display email on profile"
/>
<Checkbox
onChange={() =>
// eslint-disable-next-line camelcase
setEmailPreference((prev) => ({ ...prev, receive_collaboration: !prev.receive_collaboration }))
}
checked={emailPreference.receive_collaboration}
title="collaboration requests"
label="Receive collaboration requests"
/>
</div>
<button className="px-4 w-max py-2 rounded-lg bg-light-slate-4 border border-light-slate-8">
<Button
onClick={handleUpdateEmailPreference}
type="default"
className="!px-4 w-max !py-2 !bg-light-slate-4 "
>
Update Preferences
</button>
</Button>
</div>
</div>
</div>
Expand Down
24 changes: 24 additions & 0 deletions lib/hooks/authSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { supabase } from "lib/utils/supabase";

const baseUrl = process.env.NEXT_PUBLIC_API_URL;
export interface UserResponse extends DbUser {}
const authSession = async () => {
const sessionResponse = await supabase.auth.getSession();
const sessionToken = sessionResponse?.data.session?.access_token;
// const { data, error } = useSWR<UserResponse, Error>("auth/session", publicApiFetcher as Fetcher<UserResponse, Error>);
const response = await fetch(`${baseUrl}/auth/session`, {
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${sessionToken}`
}
});

if (response.status === 200) {
return response.json();
} else {
return false;
}
};

export { authSession };
Loading

0 comments on commit 4a8d706

Please sign in to comment.