📛 Custom Usernames
Allows users to select custom usernames
Updated listenToUserProfileChanges()
function in use-session.ts:
sync function listenToUserProfileChanges(userId: string) {
const { data } = await supaClient
.from("user_profiles")
.select("*")
.filter("user_id", "eq", userId);
if (data?.[0]) {
setUserInfo({ ...userInfo, profile: data?.[0] });
} else { // this else clause is all you need to add!
setReturnPath();
navigate("/welcome");
}
return supaClient
.channel(`public:user_profiles`)
.on(
"postgres_changes",
{
event: "*",
schema: "public",
table: "user_profiles",
filter: `user_id=eq.${userId}`,
},
(payload) => {
setUserInfo({ ...userInfo, profile: payload.new as UserProfile });
}
)
.subscribe();
}
Welcome.tsx:
import { useContext, useMemo, useState } from "react";
import { redirect, useNavigate } from "react-router-dom";
import { UserContext } from "./App";
import Dialog from "./Dialog";
import { supaClient } from "./supa-client";
export async function welcomeLoader() {
const {
data: { user },
} = await supaClient.auth.getUser();
if (!user) {
return redirect("/");
}
const { data } = await supaClient
.from("user_profiles")
.select("*")
.eq("user_id", user?.id)
.single();
if (data?.username) {
return redirect("/");
}
}
export function Welcome() {
const user = useContext(UserContext);
const navigate = useNavigate();
const [userName, setUserName] = useState("");
const [serverError, setServerError] = useState("");
const [formIsDirty, setFormIsDirty] = useState(false);
const invalidString = useMemo(() => validateUsername(userName), [userName]);
return (
<Dialog
allowClose={false}
open={true}
contents={
<>
<h2 className="welcome-header">Welcome to Supaship!!</h2>
<p className="text-center">
Let's get started by creating a username:
</p>
<form
className="welcome-name-form"
onSubmit={(event) => {
event.preventDefault();
supaClient
.from("user_profiles")
.insert([
{
user_id: user.session?.user.id || "",
username: userName,
},
])
.then(({ error }) => {
if (error) {
setServerError(`Username "${userName}" is already taken`);
} else {
const target = localStorage.getItem("returnPath") || "/";
localStorage.removeItem("returnPath");
navigate(target);
}
});
}}
>
<input
name="username"
placeholder="Username"
onChange={({ target }) => {
setUserName(target.value);
if (!formIsDirty) {
setFormIsDirty(true);
}
if (serverError) {
setServerError("");
}
}}
className="welcome-name-input"
></input>
{formIsDirty && (invalidString || serverError) && (
<p className="welcome-form-error-message validation-feedback">
{serverError || invalidString}
</p>
)}
<p className="text-center">
This is the name people will see you as on the Message Board
</p>
<button
className="welcome-form-submit-button"
type="submit"
disabled={invalidString != null}
>
Submit
</button>
</form>
</>
}
/>
);
}
/**
* This only validates the form on the front end.
* Server side validation is done at the sql level.
*/
function validateUsername(username: string): string | undefined {
if (!username) {
return "Username is required";
}
const regex = /^[a-zA-Z0-9_]+$/;
if (username.length < 4) {
return "Username must be at least 4 characters long";
}
if (username.length > 14) {
return "Username must be less than 15 characters long";
}
if (!regex.test(username)) {
return "Username can only contain letters, numbers, and underscores";
}
return undefined;
}
fn to set a return path (I put this in Login.tsx, but it might go better in use-session.ts):
export const setReturnpath = () => {
localStorage.setItem("returnPath", window.location.pathname);
};
Adding the welcomeLoader
to our routing in the App.tsx file:
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
children: [
{
path: "",
element: <MessageBoard />,
children: [
{
path: ":pageNumber",
element: <AllPosts />,
},
{
path: "post/:postId",
element: <PostView />,
},
],
},
{
path: "welcome",
element: <Welcome />,
loader: welcomeLoader, // just this line right here; be sure to export this function from Welcome.tsx!
},
],
},
]);