<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lakshya Satpal</title>
    <description>The latest articles on DEV Community by Lakshya Satpal (@lakshyasatpal).</description>
    <link>https://dev.to/lakshyasatpal</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1198454%2F58fd9279-29f0-4175-ba72-99a1e71f92de.jpeg</url>
      <title>DEV Community: Lakshya Satpal</title>
      <link>https://dev.to/lakshyasatpal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lakshyasatpal"/>
    <language>en</language>
    <item>
      <title>Building a Gamified Habit Tracker on MERN Stack and Hanko for passkey authentication</title>
      <dc:creator>Lakshya Satpal</dc:creator>
      <pubDate>Tue, 31 Oct 2023 18:06:19 +0000</pubDate>
      <link>https://dev.to/lakshyasatpal/building-a-gamified-habit-tracker-on-mern-stack-and-hanko-for-passkey-authentication-4dpf</link>
      <guid>https://dev.to/lakshyasatpal/building-a-gamified-habit-tracker-on-mern-stack-and-hanko-for-passkey-authentication-4dpf</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In this tutorial, we will create a gamified habit tracker using the MERN stack. We'll leverage MongoDB for the database, Express.js for the backend, React for the front end, and Node.js for the server. &lt;br&gt;
Additionally, we'll incorporate Hanko for passkey authentication. Hanko is a free and open-source solution to integrate passkey authentication, seamless and secure.&lt;/p&gt;

&lt;p&gt;The project is divided into two repositories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;frontend: &lt;a href="https://github.com/momentumXbyLakshya/react-client"&gt;https://github.com/momentumXbyLakshya/react-client&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;backend: &lt;a href="https://github.com/momentumXbyLakshya/express-server"&gt;https://github.com/momentumXbyLakshya/express-server&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Backend Setup
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Node.js: Ensure you have Node.js installed (preferably &amp;gt;= &lt;code&gt;v18&lt;/code&gt;). You can download it from &lt;a href="https://nodejs.org/"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clone the repository&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/momentumXbyLakshya/express-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Navigate to the project folder&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;express-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Setting up environment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;a. Create a &lt;code&gt;.env&lt;/code&gt; file in the root directory and paste the below snippet in that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MONGODB_URI=_
HANKO_API_URI=_
JWT_SECRET=_
PORT=8080
FRONTEND_URL=http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;b. Create a MongoDB Cluster on &lt;a href="https://www.mongodb.com/atlas/database"&gt;MongoDB Atlas&lt;/a&gt; and configure it such that it allows localhost to connect. Replace the &lt;code&gt;_&lt;/code&gt; in front of &lt;code&gt;MONGODB_URI&lt;/code&gt; with your database's URI. &lt;br&gt;&lt;br&gt;
c.You must create a &lt;a href="https://cloud.hanko.io/"&gt;Hanko Cloud Project&lt;/a&gt;. Sign to this platform and create a project specifying the front end of the project as &lt;code&gt;http://localhost:3000&lt;/code&gt;. Replace the &lt;code&gt;_&lt;/code&gt; in front of &lt;code&gt;HANKO_API_URI&lt;/code&gt; with your project's URL. &lt;br&gt;&lt;br&gt;
d. Replace the &lt;code&gt;_&lt;/code&gt; in front of &lt;code&gt;JWT_SECRET&lt;/code&gt; with any secret of your choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install the dependencies&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Start the server&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything is setup correctly, you will this log on your terminal:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vgS-NJZI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bfftsvosqwxmn0yulvcm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vgS-NJZI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bfftsvosqwxmn0yulvcm.png" alt="Image description" width="363" height="74"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Backend walkthrough
&lt;/h3&gt;

&lt;p&gt;You will see the following files in your project repo&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--oe1HyOZx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nk10avpr0vvr1i9wgotb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oe1HyOZx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nk10avpr0vvr1i9wgotb.png" alt="Image description" width="462" height="574"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Let's go through these files&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;index.js&lt;/strong&gt; The starting point of the application. We import middleware, router, database setup file, cronjob, and everything in this file.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import express from "express";
import "dotenv/config";
import cookieParser from "cookie-parser";
import cors from "cors";
import router from "./routes/index.js";
import { isAuthenticated } from "./middleware/auth.js";

import "./db.setup.js";
import "./cronjobs/habit.js";

const app = express();
const PORT = process.env.PORT || 8080;

app.use(
  cors({
    origin: process.env.FRONTEND_URL,
    credentials: true,
  })
);
app.use(cookieParser());
app.use(express.json());

app.use(isAuthenticated);

app.use("/api", router);

app.listen(PORT, (err) =&amp;gt; {
  console.log(`Server running on port ${PORT}`);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;db.setup.js: Use Mongoose to connect to MongoDB database.&lt;/li&gt;
&lt;li&gt;routers: Define API endpoints for REST API&lt;/li&gt;
&lt;li&gt;controllers: Define handlers for every request to router&lt;/li&gt;
&lt;li&gt;models: Define a Mongoose schema and model to store data in the MongoDB database&lt;/li&gt;
&lt;li&gt;services: Define functions that will interact with the database and return to the controller&lt;/li&gt;
&lt;li&gt;cronjobs: Scheduled functions that run on specified time or period&lt;/li&gt;
&lt;li&gt;middleware: Functions that execute after a request is received and before it is passed to the controller&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Authorization middleware&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import * as jose from "jose";

const JWKS = jose.createRemoteJWKSet(
  new URL(`${process.env.HANKO_API_URI}/.well-known/jwks.json`)
);

export const isAuthenticated = async (req, res, next) =&amp;gt; {
  let token = null;
  if (
    req.headers.authorization &amp;amp;&amp;amp;
    req.headers.authorization.split(" ")[0] === "Bearer"
  ) {
    token = req.headers.authorization.split(" ")[1];
  } else if (req.cookies &amp;amp;&amp;amp; req.cookies.hanko) {
    console.log("hanko", req.cookies.hanko);
    token = req.cookies.hanko;
  }
  if (token === null || token.length === 0) {
    res.status(401).send("Unauthorized");
    return;
  }
  let authError = false;
  await jose.jwtVerify(token, JWKS).catch((err) =&amp;gt; {
    authError = true;
    console.log(err);
  });
  if (authError) {
    res.status(401).send("Authentication Token not valid");
    return;
  }
  next();
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;isAuthenticated&lt;/code&gt; middleware checks if the request is coming with a &lt;code&gt;hanko&lt;/code&gt; token either in cookies or in headers.&lt;/li&gt;
&lt;li&gt;It then verifies the token using a remote JSON Web key set.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frontend
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Node.js: Ensure you have Node.js installed (preferably &amp;gt;= &lt;code&gt;v18&lt;/code&gt;). You can download it from &lt;a href="https://nodejs.org/"&gt;here&lt;/a&gt;.&lt;br&gt;
Backend: Ensure the backend server is up and running&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clone the repository&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/momentumXbyLakshya/react-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Navigate to the project folder&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;react-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Setting up environment variables&lt;/strong&gt;&lt;br&gt;
Create a &lt;code&gt;.env.local&lt;/code&gt; file in the root directory and paste the following snippet in that file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REACT_APP_HANKO_API_URI=_
REACT_APP_BACKEND_URI=http://localhost:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the &lt;code&gt;_&lt;/code&gt; with the Hanko Cloud API URL that you should have created while setting up the backend server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install the dependencies&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Start the app&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Frontend walkthrough
&lt;/h3&gt;

&lt;p&gt;The project structure of react-app looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--zXJXmHAZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ijw5f6meni99fzl3faux.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zXJXmHAZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ijw5f6meni99fzl3faux.png" alt="Image description" width="462" height="608"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App.js: The first component that mounts into the DOM.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useContext, useEffect, useMemo } from "react";
import { Routes, Route, useNavigate } from "react-router-dom";
import { useCookies } from "react-cookie";
import { register, Hanko } from "@teamhanko/hanko-elements";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";

import Home from "./pages/Home";
import Dashboard from "./pages/Dashboard";
import Register from "./pages/Register";
import { axiosInstance } from "./lib/axios";

import AppContext from "./store/app-context";
import Loader from "./components/Loader";

const hankoApi = process.env.REACT_APP_HANKO_API_URI;

function App() {
  const navigate = useNavigate();
  const [cookies] = useCookies("hanko");
  const {
    isAuthenticated,
    authToken,
    appLoading,
    setAppLoading,
    handleCompleteUserAuth,
    handlePartialUserAuth,
    handleNewUserAuth,
    handleLogout,
  } = useContext(AppContext);
  const hanko = useMemo(() =&amp;gt; new Hanko(hankoApi), []);

  useEffect(() =&amp;gt; {
    hanko.onAuthFlowCompleted(async (detail) =&amp;gt; {
      if (isAuthenticated) {
        navigate("/dashboard");
      } else if (authToken) {
        navigate("/register");
      } else {
        setAppLoading(true);
        const session = hanko.session.get();
        const hankoUser = await hanko.user.getCurrent();
        const token = session.jwt;
        axiosInstance
          .get(`/user/${detail.userID}`)
          .then((res) =&amp;gt; {
            const user = res.data.data.user;
            if (user &amp;amp;&amp;amp; user.name &amp;amp;&amp;amp; user.avatar) {
              // already registered user
              handleCompleteUserAuth(token, user);
              navigate("/dashboard");
            } else if (user &amp;amp;&amp;amp; user.hankoId &amp;amp;&amp;amp; user.email) {
              // user was authentiated with hanko before, but profile needs to be completed
              handlePartialUserAuth(token, user);
              navigate("/register");
            } else {
              // new user
              handleNewUserAuth(token, hankoUser.id, hankoUser.email);
            }
            setAppLoading(false);
          })
          .catch(() =&amp;gt; {
            toast.error("Something went wrong!");
            setAppLoading(false);
          });
      }
    });
    hanko.onSessionExpired(() =&amp;gt; {
      toast("Session Expired. Please Login again.");
      handleLogout();
      navigate("/");
    });
  }, [
    authToken,
    isAuthenticated,
    hanko,
    navigate,
    handleCompleteUserAuth,
    handlePartialUserAuth,
    handleNewUserAuth,
    handleLogout,
  ]);

  useEffect(() =&amp;gt; {
    register(hankoApi).catch((error) =&amp;gt; {
      console.log("Something went wrong in Hanko Authentication");
    });

    const setStateBasedOnUser = async () =&amp;gt; {
      setAppLoading(true);
      const session = hanko.session.get();
      const hankoUser = await hanko.user.getCurrent();
      const token = session.jwt;
      axiosInstance
        .get(`/user/${hankoUser.id}`)
        .then((res) =&amp;gt; {
          const user = res.data.data.user;
          if (user &amp;amp;&amp;amp; user.name &amp;amp;&amp;amp; user.avatar) {
            // already registered user
            handleCompleteUserAuth(token, user);
            setAppLoading(false);
          } else if (user &amp;amp;&amp;amp; user.email &amp;amp;&amp;amp; user.hankoId) {
            handlePartialUserAuth(token, user);
            setAppLoading(false);
          }
        })
        .catch((err) =&amp;gt; {
          console.log(err);
          toast("Something went wrong!");
          setAppLoading(false);
        });
    };

    if (cookies &amp;amp;&amp;amp; cookies.hanko) {
      setStateBasedOnUser();
    }
  }, [cookies, hanko, handleCompleteUserAuth, handlePartialUserAuth]);

  if (appLoading) {
    return &amp;lt;Loader /&amp;gt;;
  }

  return (
    &amp;lt;&amp;gt;
      &amp;lt;Routes&amp;gt;
        &amp;lt;Route path="/" index element={&amp;lt;Home /&amp;gt;}&amp;gt;&amp;lt;/Route&amp;gt;
        {authToken &amp;amp;&amp;amp; !isAuthenticated &amp;amp;&amp;amp; (
          &amp;lt;Route path="/register" element={&amp;lt;Register /&amp;gt;}&amp;gt;&amp;lt;/Route&amp;gt;
        )}
        {isAuthenticated &amp;amp;&amp;amp; (
          &amp;lt;Route path="dashboard" element={&amp;lt;Dashboard hanko={hanko} /&amp;gt;} /&amp;gt;
        )}
      &amp;lt;/Routes&amp;gt;
      &amp;lt;ToastContainer /&amp;gt;
    &amp;lt;/&amp;gt;
  );
}

export default App;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;store: Store contains the context-api for the app. Below the &lt;code&gt;AppProvider.js&lt;/code&gt; component.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useCallback, useState } from "react";
import AppContext from "./app-context";
import { useNavigate } from "react-router-dom";
import { axiosInstance } from "../lib/axios";
import { toast } from "react-toastify";

const AppContextProvider = ({ children }) =&amp;gt; {
  const navigate = useNavigate();
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [user, setUser] = useState(null);
  const [authToken, setAuthToken] = useState(null);
  const [hankoDetails, setHankoDetails] = useState({
    userId: null,
    email: null,
  });
  const [appLoading, setAppLoading] = useState(false);

  const handleCompleteUserAuth = useCallback((token, user) =&amp;gt; {
    setIsAuthenticated(true);
    setAuthToken(token);
    setUser(user);
    setHankoDetails({
      userId: user.hankoId,
      email: user.email,
    });
  }, []);

  const handlePartialUserAuth = useCallback((token, user) =&amp;gt; {
    console.log("handlePartialUserAuth called ");
    setAuthToken(token);
    setHankoDetails({
      userId: user.hankoId,
      email: user.email,
    });
  }, []);

  const handleLogout = useCallback(() =&amp;gt; {
    setAuthToken(null);
    setIsAuthenticated(false);
    setUser(null);
    setHankoDetails({ userId: null, email: null });
  }, []);

  const handleAddHabit = (habit) =&amp;gt; {
    const newUser = { ...user };
    newUser.habits.push(habit);
    setUser(newUser);
  };

  const handleUpdateHabit = (index, habit) =&amp;gt; {
    const newUser = { ...user };
    newUser.habits[index] = habit;
    setUser(newUser);
  };

  const handleDeleteHabit = (index) =&amp;gt; {
    const newUser = { ...user };
    newUser.habits = newUser.habits.filter((hab, i) =&amp;gt; i !== index);
    setUser(newUser);
  };

  const handleNewUserAuth = useCallback(
    (token, hankoId, email) =&amp;gt; {
      setAuthToken(token);
      setHankoDetails({
        userId: hankoId,
        email,
      });
      axiosInstance
        .post(`/user/`, {
          hankoId,
          email,
        })
        .then(() =&amp;gt; {
          navigate("/register");
        })
        .catch(() =&amp;gt; {
          toast.error("Something went Wrong");
        });
    },
    [navigate]
  );

  const handleRegisterComplete = (name, avatar) =&amp;gt; {
    setAppLoading(true);
    axiosInstance
      .put(`/user/${hankoDetails.userId}`, {
        name,
        hankoId: hankoDetails.userId,
        email: hankoDetails.email,
        avatar,
      })
      .then((res) =&amp;gt; {
        const user = res.data.data.user;
        setIsAuthenticated(true);
        setUser(user);
        navigate("/dashboard");
        setAppLoading(false);
      })
      .catch(() =&amp;gt; {
        setAppLoading(false);
        toast.error("Something went wrong");
      });
  };

  const appContext = {
    isAuthenticated,
    authToken,
    hankoDetails,
    user,
    setUser,
    appLoading,
    setAppLoading,
    handleCompleteUserAuth,
    handleNewUserAuth,
    handlePartialUserAuth,
    handleRegisterComplete,
    handleLogout,
    handleAddHabit,
    handleUpdateHabit,
    handleDeleteHabit,
  };

  return (
    &amp;lt;AppContext.Provider value={appContext}&amp;gt;{children}&amp;lt;/AppContext.Provider&amp;gt;
  );
};

export default AppContextProvider;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;pages: There are three pages in the app, Home, Register, and Dashboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Future Goals
&lt;/h3&gt;

&lt;p&gt;Some Upcoming features on the projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accessories for the Avatar based on it's level&lt;/li&gt;
&lt;li&gt;More Avatars in User Registration&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Thank you for reading
&lt;/h3&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
