Creating the edit profile, add links Page | LinkTree Project
This tutorial is a part of the Series ‘Complete FullStack LinkTree Project with MERN (opens in a new tab)‘. This Project covers beginner to Intermediate level implementation of MongoDB, Express, ReactJS, NodeJS, and NextJS.
Project Walkthrough
Users can signup on the LinkTree website and start creating their LinkTree immediately. They get a user profile editing page, an edit or customize links page, a Dashboard to edit and track the links, and many more.
I’ve created a YouTube series on this Project in 6 parts. This Code snippet is from the 6th and final part where we create an edit profile, and add Links page for a user’s LinkTree.
Frontend
import { createContext } from "react";
const UserContext = createContext(null);
export default UserContext;
// using the context in the whole app, this changes are added to previous code
import UserContext from "../context/userContext";
const [userData, setUserData] = useState({
name: "",
role: "",
bio: "",
avatar: "",
handle: ""
});
<UserContext.Provider value={{ userData, setUserData }}>
<Component {...pageProps} />
</UserContext.Provider>;
import React, { useContext, useEffect, useState } from "react";
import { useRouter } from "next/router";
import UserContext from "../../context/userContext";
import UserHeader from "../../components/UserHeader";
import { toast } from "react-toastify";
const profile = () => {
const router = useRouter();
const { userData, setUserData } = useContext(UserContext);
const [social, setSocial] = useState({
facebook: "",
twitter: "",
instagram: "",
youtube: "",
linkedin: "",
github: ""
});
const [name, setName] = useState("");
const [bio, setBio] = useState("");
const [avatar, setAvatar] = useState(
"https://cdn-icons-png.flaticon.com/64/4140/4140048.png"
);
const handleSocial = (e) => {
setSocial({
...social,
[e.target.id]: e.target.value
});
};
useEffect(() => {
if (userData) {
setName(userData.name);
setBio(userData.bio);
setAvatar(userData.avatar);
}
}, [userData]);
const saveProfile = (e) => {
e.preventDefault();
fetch(`http://localhost:8080/save/profile`, {
method: "POST",
headers: {
"Content-type": "application/json"
},
body: JSON.stringify({
tokenMail: localStorage.getItem("LinkTreeToken"),
name: name,
bio: bio,
avatar: avatar
})
})
.then((res) => res.json())
.then((data) => {
if (data.status === "error") return toast.error(data.error);
toast.success("Profile saved successfully");
});
};
const saveSocials = (e) => {
e.preventDefault();
fetch(`http://localhost:8080/save/socials`, {
method: "POST",
headers: {
"Content-type": "application/json"
},
body: JSON.stringify({
tokenMail: localStorage.getItem("LinkTreeToken"),
socials: social
})
})
.then((res) => res.json())
.then((data) => {
if (data.status === "error") return toast.error(data.error);
toast.success("Socials saved successfully");
});
};
useEffect(() => {
if (!localStorage.getItem("LinkTreeToken")) return router.push("/login");
fetch(`http://localhost:8080/load/socials`, {
method: "POST",
headers: {
"Content-type": "application/json"
},
body: JSON.stringify({
tokenMail: localStorage.getItem("LinkTreeToken")
})
})
.then((res) => res.json())
.then((data) => {
if (data.status === "error") return toast.error(data.error);
setSocial(data.socials);
});
}, []);
return (
<>
<div>
<UserHeader />
<main>
<section>
<div>
<h4 className="font-bold text-center mb-5">Edit profile</h4>
<div>
<form
onSubmit={saveProfile}
className="flex flex-col justify-center items-center"
>
<span className="flex flex-row mb-3 w-11/12 m-auto shadow-md border-2 px-3 py-2 rounded-md focus:outline-none">
<img className="w-6 mr-2" src="/svg/user.svg" alt="" />
<input
value={name}
onChange={(e) => setName(e.target.value)}
className="focus:outline-none w-full"
type="text"
placeholder="Set a Name"
required
/>
</span>
<span className="flex flex-row mb-3 w-11/12 m-auto shadow-md border-2 px-3 py-2 rounded-md focus:outline-none">
<img className="w-6 mr-2" src="/svg/bio.svg" alt="" />
<input
value={bio}
onChange={(e) => setBio(e.target.value)}
className="focus:outline-none w-full"
type="text"
placeholder="Enter a bio"
required
/>
</span>
<span className="flex flex-row mb-3 w-11/12 m-auto shadow-md border-2 px-3 py-2 rounded-md focus:outline-none">
<img className="w-6 mr-2" src="/svg/user.svg" alt="" />
<input
value={avatar}
onChange={(e) => setAvatar(e.target.value)}
className="focus:outline-none w-full"
type="text"
placeholder="Enter Image link"
required
/>
<img
className="w-10 rounded-full border-2 border-white shadow-md"
src={avatar}
/>
</span>
<input
className="bg-green-500 w-32 px-4 py-2 rounded-md border-2 border-green-800 shadow-md cursor-pointer text-white"
type="submit"
value="save profile"
/>
</form>
</div>
</div>
<div className="mt-14">
<h4 className="font-bold text-center mb-5">Edit Socials</h4>
<div>
<form
onSubmit={saveSocials}
className="flex flex-col justify-center items-center"
>
<span className="flex flex-row mb-3 w-11/12 m-auto shadow-md border-2 px-3 py-2 rounded-md focus:outline-none">
<img className="w-6 mr-2" src="/svg/facebook.svg" alt="" />
<input
id="facebook"
value={social.facebook}
onChange={handleSocial}
className="focus:outline-none w-full"
type="text"
placeholder="Enter Facebook ID"
required
/>
</span>
<span className="flex flex-row mb-3 w-11/12 m-auto shadow-md border-2 px-3 py-2 rounded-md focus:outline-none">
<img className="w-6 mr-2" src="/svg/instagram.svg" alt="" />
<input
id="instagram"
value={social.instagram}
onChange={handleSocial}
className="focus:outline-none w-full"
type="text"
placeholder="Enter Instagram ID"
required
/>
</span>
<span className="flex flex-row mb-3 w-11/12 m-auto shadow-md border-2 px-3 py-2 rounded-md focus:outline-none">
<img className="w-6 mr-2" src="/svg/twt.svg" alt="" />
<input
id="twitter"
value={social.twitter}
onChange={handleSocial}
className="focus:outline-none w-full"
type="text"
placeholder="Enter Twitter ID"
required
/>
</span>
<span className="flex flex-row mb-3 w-11/12 m-auto shadow-md border-2 px-3 py-2 rounded-md focus:outline-none">
<img className="w-6 mr-2" src="/svg/lnkdn.svg" alt="" />
<input
id="linkedin"
value={social.linkedin}
onChange={handleSocial}
className="focus:outline-none w-full"
type="text"
placeholder="Enter Linkedin ID"
required
/>
</span>
<span className="flex flex-row mb-3 w-11/12 m-auto shadow-md border-2 px-3 py-2 rounded-md focus:outline-none">
<img className="w-6 mr-2" src="/svg/github.svg" alt="" />
<input
id="github"
value={social.github}
onChange={handleSocial}
className="focus:outline-none w-full"
type="text"
placeholder="Enter Github ID"
required
/>
</span>
<span className="flex flex-row mb-3 w-11/12 m-auto shadow-md border-2 px-3 py-2 rounded-md focus:outline-none">
<img className="w-6 mr-2" src="/svg/yt.svg" alt="" />
<input
id="youtube"
value={social.youtube}
onChange={handleSocial}
className="focus:outline-none w-full"
type="text"
placeholder="Enter YouTube ID @"
required
/>
</span>
<input
className="bg-green-500 w-32 px-4 py-2 rounded-md border-2 border-green-800 shadow-md cursor-pointer mb-10 text-white"
type="submit"
value="save Socials"
/>
</form>
</div>
</div>
</section>
</main>
</div>
</>
);
};
export default profile;
import UserHeader from "../../components/UserHeader";
import React, { useState, useEffect, useContext } from "react";
import { toast } from "react-toastify";
const links = () => {
const [links, setLinks] = useState([{ url: "", title: "" }]);
const [title, setTitle] = useState("");
const handleLinkChange = (index, field, value) => {
const updatedLinks = [...links];
const linkToUpdate = { ...updatedLinks[index], [field]: value };
updatedLinks[index] = linkToUpdate;
setLinks(updatedLinks);
};
const handleAddLink = () => {
setLinks([...links, { url: "", title: "" }]);
};
const handleRemoveLink = (index) => {
const updatedLinks = [...links];
updatedLinks.splice(index, 1);
setLinks(updatedLinks);
};
const saveLinks = (e) => {
e.preventDefault();
const linksArray = Object.values(links);
const titlesArray = Object.values(title);
const linksData = linksArray.map((link, index) => ({
link,
title: titlesArray[index]
}));
fetch(`http://localhost:8080/save/links`, {
method: "POST",
headers: {
"Content-type": "application/json"
},
body: JSON.stringify({
tokenMail: localStorage.getItem("LinkTreeToken"),
links: linksData
})
})
.then((res) => res.json())
.then((data) => {
if (data.status === "error") return toast.error(data.error);
toast.success("Links saved successfully");
})
.catch((err) => {
toast.error(err.message);
});
};
useEffect(() => {
if (!localStorage.getItem("LinkTreeToken")) return router.push("/login");
fetch(`http://localhost:8080/load/links`, {
method: "POST",
headers: {
"Content-type": "application/json"
},
body: JSON.stringify({
tokenMail: localStorage.getItem("LinkTreeToken")
})
})
.then((res) => res.json())
.then((data) => {
if (data.status === "error") return toast.error(data.error);
setLinks(data.links);
});
}, []);
return (
<>
<div>
<UserHeader />
<main>
<section>
<h1 className="text-center font-bold text-xl text-gray-600">
Add or Customize your Links
</h1>
<div>
<form onSubmit={saveLinks}>
{links.map((link, index) => (
<div
className="flex flex-row justify-evenly my-2"
key={index}
>
<label>
URL:
<input
className="outline-none border-2 border-gray-200 shadow-md rounded-md px-2 p-1 ml-2"
type="text"
value={link.url}
onChange={(e) =>
handleLinkChange(index, "url", e.target.value)
}
/>
</label>
<label>
Title:
<input
className="outline-none border-2 border-gray-200 shadow-md rounded-md px-2 p-1 ml-2"
type="text"
value={link.title}
onChange={(e) =>
handleLinkChange(index, "title", e.target.value)
}
/>
</label>
<button
className="bg-indigo-500 text-white px-4 py-2 rounded-md shadow-sm ml-3"
type="button"
onClick={() => {
handleRemoveLink(index);
}}
>
Remove Link
</button>
</div>
))}
<div className="buttons flex flex-row gap-5 my-1">
<button
className="bg-indigo-500 text-white px-4 py-2 rounded-md shadow-sm w-full"
type="button"
onClick={handleAddLink}
>
Add link
</button>
<button
className="bg-green-500 text-white px-4 py-2 rounded-md shadow-sm w-full"
type="submit"
>
Save
</button>
</div>
</form>
</div>
</section>
</main>
</div>
</>
);
};
export default links;
Backend
// Added logic for loading saved data, and add new data
app.post("/save/socials", saveSocials);
app.post("/save/profile", saveProfile);
app.post("/save/links", saveLinks);
app.post("/load/socials", loadSocials);
app.post("/load/links", loadLinks);
const User = require("../models/user");
const jwt_decode = require("jwt-decode");
const saveSocials = async (req, res) => {
const { tokenMail, socials } = req.body;
console.log(req.body);
try {
const decodedTokenMail = jwt_decode(tokenMail, process.env.SECRET_JWT);
const email = decodedTokenMail.email;
console.log(email);
const user = await User.findOne({ email: email });
console.log(user);
user.socialMedia = socials;
user.save();
return res.json({ message: "saved", status: "success" });
} catch (err) {
return res.json({ status: "error", error: err.message });
}
};
const saveProfile = async (req, res) => {
const { tokenMail, name, bio, avatar } = req.body;
console.log(req.body);
try {
const decodedTokenMail = jwt_decode(tokenMail, process.env.SECRET_JWT);
const email = decodedTokenMail.email;
console.log(email);
const user = await User.findOne({ email: email });
console.log(user);
user.name = name;
user.bio = bio;
user.avatar = avatar;
user.save();
return res.json({ message: "saved", status: "success" });
} catch (err) {
return res.json({ status: "error", error: err.message });
}
};
const saveLinks = async (req, res) => {
const { tokenMail, links } = req.body;
try {
const decodedTokenMail = jwt_decode(tokenMail, process.env.SECRET_JWT);
const email = decodedTokenMail.email;
console.log(email);
const user = await User.findOne({ email: email });
console.log(user);
const newLinks = links.map((link) => ({
url: link.link.url,
title: link.link.title,
icon: ""
}));
user.links = newLinks;
await user.save();
return res.json({ message: "saved", status: "success" });
} catch (err) {
return res.json({ status: "error", error: err.message });
}
};
module.exports = { saveSocials, saveProfile, saveLinks };
const User = require("../models/user");
const jwt_decode = require("jwt-decode");
const loadSocials = async (req, res) => {
const { tokenMail } = req.body;
try {
const decodedTokenMail = jwt_decode(tokenMail, process.env.SECRET_JWT);
const email = decodedTokenMail.email;
console.log(email);
const user = await User.findOne({ email: email });
const socials = user.socialMedia;
return res.json({ message: "found", socials, status: "success" });
} catch (error) {
return res.json({ status: "error", error: error.message });
}
};
const loadLinks = async (req, res) => {
const { tokenMail } = req.body;
try {
const decodedTokenMail = jwt_decode(tokenMail, process.env.SECRET_JWT);
const email = decodedTokenMail.email;
console.log(email);
const user = await User.findOne({ email: email });
const links = user.links;
return res.json({ message: "found", links, status: "success" });
} catch (error) {
return res.json({ status: "error", error: error.message });
}
};
module.exports = { loadSocials, loadLinks };
Summing up
I hope this part of the tutorial on the Fullstack LinkTree Project was helpful to you. If you faced any problems while following the video or the source code snippets, comment below and I’ll reply as soon as possible. See you soon.