I recently completed a technical interview with a lending-as-a-service company. The interview required me to build four pages with TypeScript, React, and SCSS for styling.
Although, unfortunately I didn't obtain the position, but since it's a large project (which I personally think is gross for the first stage of a technical interview) I'm committed to sharing what I learned. In this article, I'll walk you through the features I implemented and how I implemented them. I hope this article will be helpful to other frontend developers who are preparing for technical interviews.
PS: I'm still open for a frontend developer role. If you're hiring for a frontend role, please reach out to me. I'd love to learn more about the opportunity.
Note that, this article will focus on the features implemented, with little to no discussion of the styling.
TL;DR
The following is a brief overview of the pages and features implemented in the project:
Login page: I used Firebase for authentication and implemented protected routes with React Router, even though it wasn't required.
List of users page: This page pulls data from a mock API with 500 records using Axios and Axios Interceptors.
User details page: This page was required to use local storage or indexedDB to store and retrieve user details. I chose to use localStorage.
The end product of the pages implemented should look like this:
This article shall be a complete overhaul from the ground up, with no stone unturned. I will start with the login page, and discuss how the user is navigated to the dashboard. From there, I will also explore the features implemented in the users page, and finally, we shall reach the user details page, where the user journey ends.
When you are ready, let's dive in.
How I Set My Project Up
I Set Up My React + TypeScript Project With Vite
To kickstart my project swiftly, I opted for Vite as my project initializer, and I highly recommend you do the same if you haven't been using Vite. Initializing a TypeScript + React project with Vite is effortless with just one command:
npm create vite@latest interview-project -- --template react-ts
How I Set Up Firebase For Authentication
I decided to utilize Firebase for authentication for this app although it wasn't a requirement.
The fact that I also wanted to implement a protected route functionality for all authenticated pages also played a role in my decision. This feature will be discussed in a later section.
In this section, I will briefly guide you through the process of how I set it up.
Generally, to use Firebase, you need to create and register your project on the platform, which I did.
Following this, I installed Firebase in my project and created a components
folder. Inside this folder, I created a file named firebaseConfig.ts
to store my configuration details and establish a connection between my app and Firebase like this:
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
const firebaseConfig = {
//firebase config
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);// connect firebase
export const auth = getAuth(app)
If you are new to Firebase and are interested in learning more about it, check out the official documentation here.
File Structure
I established a pages
directory to contain the various pages of the application.
Within the previously created components
directory, I created subdirectories with names corresponding to the folders within the pages
directory. This organizational structure allows for convenient grouping of relevant components within each page-specific folder.
Furthermore, I added a styles
folder in the src
directory to centralize all the styles for my pages. Additionally, an utils
folder was created within the src
directory to store files responsible for handling API requests.
I had adopted this file structure during my time at my previous job.
In the end, the folder structure looked like this:
- project
- src
- components
- dashboard
- common
- user-details
- users
- login
- firebaseConfig.ts
- pages
- Login.tsx
- Users.tsx
- User-details.tsx
- styles
- utils
- request-adapter.ts
- requests.ts
- main.tsx
- public
- package.json
I'll now walk you through each page and files if necessary in the next section.
Page Routing In The main.tsx
File
The powerful React Router was employed for seamless page routing, not like there are numerous alternatives available lol. If you are new to React Router or uncertain about its benefits, I recommend checking out its documentation here for further insights.
Once React Router is installed, I implemented the routes in the main.tsx
file as seen in the code snippet below.
I had not used React Router in a while due to the fact that I have been writing a lot of
Nextjs
these days. As a result, I was taken aback by the new syntax when revisiting it. If you're in a similar situation where you haven't kept up with the updates, note that an updated version,6.4
, has been released with fresh syntax and features. I recommend referring to the documentation to stay updated and informed.
import React from 'react'
import ReactDOM from 'react-dom/client'
import './App.scss';
import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
import Login from './pages/Login-Auth.tsx';
import Users from './pages/Users.tsx';
import UserDetails from './pages/User-details.tsx';
const router = createBrowserRouter([
{
path: "/",
element: <Login/>
},
{
path: "/dashboard/users",
element: <Users/>
},
{
path: "/dashboard/users/:id",
element: <UserDetails/>
},
]);
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<RouterProvider router={router}/>
</React.StrictMode>,
)
How I Implemented Features
Starting Off With Authentication In The Log In Page 🚀
For the sign-in method, Firebase enables you to use different sign-in providers, ranging from email
and password
provider to Google
provider and so on... For the authentication of this app, I employed the email and password provider since the email and password input fields were provided and required.
Here's how I implemented the feature:
import { createUserWithEmailAndPassword, signInWithEmailAndPassword } from "firebase/auth";
import { useNavigate } from 'react-router-dom'
import { auth } from '../components/login-auth/firebaseConfig';
export default function Login() {
const navigate = useNavigate()
const [ email, setEmail ] = useState<string>('');
const [ password, setPassword ] = useState<string>('')
const [ signUp, setSignUp ] = useState<boolean>(true)
const [ helperText, setHelperText ] = useState<string>('')
const [ hide, setHide ] = useState<boolean>(false)
const handleSignUp = (e: React.FormEvent<HTMLButtonElement> ) => {
e.preventDefault()
createUserWithEmailAndPassword(auth, email, password)
.then(() => {
setHelperText('Congrats!, you can now Sign In')
setTimeout(() => setHelperText(''), 2000)
setSignUp(!signUp)
setError(false)
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
setError(true)
console.log(errorCode, errorMessage)
});
}
const handleSignIn = (e: React.FormEvent<HTMLButtonElement>) => {
e.preventDefault()
signInWithEmailAndPassword(auth, email, password)
.then(() => {
setError(false)
navigate('/dashboard/users')
sessionStorage.setItem('auth', JSON.stringify(auth))
})
.catch((error) => {
const errorCode = error.code;
(errorCode === 'auth/user-not-found!') ?
setHelperText('Email is not registered!') :
setHelperText('Invalid password')
setError(true)
setTimeout(() => setHelperText(''), 2000)
});
}
}
return (
<form>
<div>
<h1>Welcome!</h1>
<p>Enter details to {signUp ? "login" : 'sign up'}.</p>
<div>
<p className={`${error ? 'error' : 'success'}`}>{helperText}</p>
<div>
<Input type='email' placeholder={'Email'} value={email} onChange={setEmail}/>
</div>
<div>
<Input type={hide ? "password" : 'text'} placeholder={'Password'} value={password} onChange={setPassword}/>
<span onClick={() => setHide(!hide)}>{hide ? "Show": "Hide"}</span>
</div>
</div>
<p>Forgot Password?</p>
<p>Don't have an account? <span onClick={() => setSignUp(!signUp)}>Sign Up</span></p>
<button type='submit' onClick={!signUp ? handleSignUp : handleSignIn}>
signUp ? ' Log In' : ' Sign Up'
</button>
</div>
</form>
)
I implemented two functions in the code block above: handleSignIn
and handleSignUp
.
As their names imply, the handleSignin
function handles the sign in process, while the latter handles that of the sign up. If the requirements of each function are met, the code in their .then
block runs, if not, the error is caught in the .catch
block.
Note that once the code in the
.then
block in thehandleSignIn
function runs, anauth
object is saved in thesessionStorage
of the browser. This will be used when implementing the protected routes feature. More details on this in the authenticated pages section.
I introduced the signUp
state to regulate which of the functions executes in the proper scenario. When a user hits the sign up
text in the form beneath the Forgot Password
text, the signUp
state is updated. The highlighted code below demonstrates how this is done:
<p>Don't have an account? <span onClick={() => setSignUp(!signUp)}>Sign Up</span></p>
As I mentioned in earlier, this state is used to toggle the functions that get invoked when the button
in the form
element is clicked. That is, if a user is new and just wants to sign up, the handleSignUp
function is triggered, otherwise, if it is an already existing user who wants to log in, the log in function is triggered.
This also controls the text that renders in the button
.
You can see how this is implemented in the highlighted code below:
<button type='submit' onClick={!signUp ? handleSignUp : handleSignIn}>
signUp ? ' Log In' : ' Sign Up'
</button>
The purpose of the signUp
state wasn't limited to just the function and text of the button as it also updates the text below the Welcome
header:
<p>Enter details to {signUp ? "login" : 'sign up'}.</p>
See video below for an illustration of this scenario.
On To The Authenticated Pages 🚶
Before I started off with the authenticated pages - the user and the user-details page; I thought the implementation of the protected routes feature seemed to make sense after the authentication procedure was finished.
As mentioned earlier, upon successful sign-in, an auth
object is stored in the user's browser sessionStorage
temporarily. I chose sessionStorage
for this purpose because it retains data only for the duration of the session, deleting it when the browser is closed.
With the auth
variable now appropriately stored in sessionStorage
, I proceeded to implement the protected route feature. To accomplish this, I created a Page.tsx file within the common folder of the dashboard, as indicated in the file structure section, and implemented the feature as follows:
import Navbar from "./Navbar";
import Sidebar from "./Sidebar";
import { useState } from 'react';
import { Navigate } from "react-router-dom";
type PageProps = {
children: React.ReactNode,
}
export default function Page ({children }: PageProps) {
const authFromSessionStorage = sessionStorage.getItem('auth')
return (
<>
{
(!authFromSessionStorage) ?
<Navigate to='/' replace={true}/> :
(
<>
<Navbar/>
<Sidebar/>
<div>
{children}
</div>
</>
)} // navigate to log in page if user isn't authenticated
</>
)
}
Since the Page
component will wrap around all authenticated pages, which share the navbar and sidebar components, I decided to include those components within it. This explains their inclusion in the code block. Here's an example of how I wrapped this component around an authenticated page component:
// users.tsx
export default function Users() {
return (
<Page>
<div>
// rest of the page content here
</div>
</Page>
)
}
Following this implementation, when an unauthenticated user attempts to access an authenticated page, they will be redirected to the sign-in page to authenticate themselves.
Navigating To The Users' Page 👥
This page requires candidates to pull data from a mock API and populate it in form of a table as seen in the image above.
After pulling the data, candidates are to also implement a feature to paginate the data gotten from the API just below the table:
Since the number of users gotten from this API is 100, I decided to paginate the data in multiples of 20, 50, and 100. At the end the feature looks like this:
Moving on, in the request-adapter.ts
file created in the utils
folder ( see folder structure section ), I created an instance of Axios
with Axios.create
and assigned it to a variable request
. This instance also includes a base URL
and timeout configuration. I also added a response interceptor to handle successful responses and errors, finally the instance is exported for use in other parts of the application:
import axios from "axios";
const request = axios.create({
baseURL: "https://6270020422c706a0ae70b72c.mockapi.io/lendsqr/api/v1/",
timeout: 30000,
});
request.interceptors.response.use(
(response) => {
return response;
},
({ response }) => {
return Promise.reject(response);
}
);
export default request;
Following this, the request
variable is imported from the request-adapter
module.
Then, a getUsers
function which it uses the request
instance to make a GET request to the /users
endpoint is defined.
import request from "./request-adapter"
export const getUsers = () => request.get('/users')
By calling the getUsers
, the Axios instance will send a GET request to the complete URL formed by combining the base URL from the instance's configuration and the /users
endpoint.
The getUsers
function is exported, making it available for other modules to import and use. This function can now be called in the users' page where it will initiate a GET request to retrieve the users' data from the specified endpoint.
Next, I implemented the markup for the table that displays the users. I created a Table.tsx
file in the users folder situated in the components folder for this purpose.
Here's how the markup looks like:
type TableProps<T extends object> = {
users: T
} // users is a generic of type object just as in he Users component
export default function Table<T extends object>({ users }: TableProps<T[]>) {
return (
<section>
<table>
<thead>
<tr>
<th>
<div>
<p>Organization</p>
</div>
</th>
<th>
<div>
<p>Username</p>
</div>
</th>
<th>
<div>
<p>Email</p>
</div>
</th>
<th>
<div>
<p>Phone Number</p>
</div>
</th>
<th>
<div>
<p>Date Joined</p>
</div>
</th>
<th>
<div>
<p>Status</p>
</div>
</th>
</tr>
</thead>
<tbody>
// list of users will be here
</tbody>
</table>
</section>
}
Notice the custom type TableProp
defined. This type is a generic type whose parameter extends an object
and assigned to the users property, this ensures that the users property accepts any type of object
. After the users endpoint is called, the body of the table will be implemented and populated.
After implementing the Table
component, I created another component in the users
folder for the pagination feature just below the table and named it PaginatedItems.tsx
. The pagination feature was not just implemented in this component though, I also made the call to the users
endpoint and rendered the Table
component inside this component. I will explain why.
This was done mostly because I used react-paginate
library to implement the pagination feature. This library needs to have access with the data you want to paginate directly - the users data gotten from the API, in order to interact with it.
I'll now walk you through the pagination and Table implementation step-by-step.
Firstly, I defined the component and fetched the data from the API:
import { useState, useEffect, useCallback } from "react";
import { getUsers } from "../../../utils/requests";
import { AxiosResponse } from "axios"
export default function PaginatedItems<T extends object>() {
const [ users, setUsers ] = useState<T[]>([]);
const fetchData = useCallback(() => {
getUsers()
.then(({data} : AxiosResponse<T[]>) => {
setUsers(data)
})
.catch(err => console.log(err))
}, []);
useEffect(() => {
fetchData()
}, [])
return (
// code markup here
)
}
I then went on to develop the pagination feature after implementing and obtaining the component. The npm website's instruction of how to use the react-paginate
module was sufficient for its integration. The following code snippet was included in the component:
import { useState, useEffect, useCallback } from "react";
import { getUsers } from "../../../utils/requests";
import { AxiosResponse } from "axios"
type DropdownProps = {
setItemsPerPage: React.Dispatch<React.SetStateAction<number>>
}
export function DropdownFilter({setItemsPerPage } : DropdownProps) {
return (
<div>
<p onClick={() => setItemsPerPage(20)}>20</p>
<p onClick={() => setItemsPerPage(50)}>50</p>
<p onClick={() => setItemsPerPage(100)}>100</p>
</div>
)
}
export default function PaginatedItems<T extends object>() {
const [ currentItems, setCurrentItems ] = useState<T[]>([]);
const [ itemsPerPage, setItemsPerPage ] = useState<number>(50);
const [ pageCount, setPageCount ] = useState<number>(0);
const [ itemOffset, setItemOffset ] = useState<number>(0);
useEffect(() => {
if(users){
const endOffset = itemOffset + itemsPerPage;
setCurrentItems(users.slice(itemOffset, endOffset));
setPageCount(Math.ceil(users.length / itemsPerPage));
}
}, [users, itemOffset, itemsPerPage]);
// Invoke when user clicks to request another page.
const handlePageClick = (event: { selected : number}) => { //since we just need the selected property
if(currentItems) {
const newOffset = event.selected * itemsPerPage % users.length;
setItemOffset(newOffset);
}
};
return (
<>
{
(currentItems.length > 0) && (
<>
<Table users={currentItems} />
<div>
<div>
<p>Showing</p>
<div
onClick={() => setShowDropdown(!showDropdown)}
>
<p>{itemsPerPage}</p>
<img src={dropdown} alt='dropdown icon'/>
<DropdownFilter
setItemsPerPage={setItemsPerPage}
className={showDropdown ? 'active' : ''}/>
</div>
<p>out of {users.length}</p>
</div>
<ReactPaginate
nextLabel={<Button src={right}/>}
onPageChange={handlePageClick}
pageRangeDisplayed={3}
marginPagesDisplayed={2}
pageCount={pageCount}
previousLabel={<Button src={left}/>}
pageClassName="page-item"
pageLinkClassName="page-link"
previousClassName="page-item"
previousLinkClassName="page-link"
nextClassName="page-item"
nextLinkClassName="page-link"
breakLabel="..."
breakClassName="page-item"
breakLinkClassName="page-link"
containerClassName="pagination"
activeClassName="active"
renderOnZeroPageCount={null}
/>
</div>
</>
)}
</>
)}
The code provided shows the implementation of a paginated table component. The PaginatedItems
component manages the state for displaying a specific number of items per page and calculating the necessary pagination parameters.
The DropdownFilter
component renders a dropdown menu with options for setting the number of items per page. When an option is clicked, the setItemsPerPage
function is called to update the itemsPerPage
state in the PaginatedItems
component.
Inside the PaginatedItems
component, the useEffect
hook is used to update the currentItems
state and calculate the pageCount
whenever the users
, itemOffset
, or itemsPerPage
dependencies change. The currentItems
state is updated by slicing the users array based on the current itemOffset
and itemsPerPage
values.
The handlePageClick
function is invoked when the user clicks on a page in the pagination component (ReactPaginate
). It calculates the new itemOffset
based on the selected page and updates the state accordingly.
The PaginatedItems
component also renders the Table
component with the currentItems
as the users
prop, showing the table with the paginated data.
Additionally, there are UI elements for displaying the current page's range and the total number of users. The ReactPaginate
component handles the pagination rendering and interaction based on the provided parameters.
After passing the data to the Table
component, I populated the body of the table with the data.
import { useState } from "react";
type TableProps<T extends object> = {
users: T;
}; // users is a generic of type object just as in he Users component
type User = {
[key: string]: any;
}; //use index signature for type checking individual objects in the users array.
export default function Table<T extends object>({ users }: TableProps<T[]>) {
const [activeId, setActiveId] = useState<string>("");
return (
<section>
<table>
<thead>
<tr>
<th>
<div>
<p>Organization</p>
</div>
</th>
<th>
<div>
<p>Username</p>
</div>
</th>
<th>
<div>
<p>Email</p>
</div>
</th>
<th>
<div>
<p>Phone Number</p>
</div>
</th>
<th>
<div>
<p>Date Joined</p>
</div>
</th>
<th>
<div>
<p>Status</p>
</div>
</th>
</tr>
</thead>
<tbody>
{users.map((user: User) => {
const { id, orgName, userName, email, phoneNumber, createdAt } =
user;
const date = new Date(createdAt);
const formattedDate = date.toLocaleString("en-NG", {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "numeric",
hour12: true,
});
return (
<tr key={id} className="tr-body">
<td>{orgName}</td>
<td>{userName}</td>
<td>{email}</td>
<td>{phoneNumber}</td>
<td>{formattedDate}</td>
<td>Inactive</td>
</tr>
);
})}
</tbody>
</table>
</section>
);
}
Next, the table needs a dropdown for each user, it will be triggered on clicking the ellipsis on the extreme right of the table. The dropdown should look like:
Clicking the view details text should navigate to the user details page. I created a Dropdown.tsx
file in the users folder of the component folder where I implemented the functionality like this:
import { Link } from "react-router-dom";
type DropdownProps = {
id: string;
toggleDropdown: {
activeId: string;
setActiveId: React.Dispatch<React.SetStateAction<string>>;
};
};
export default function Dropdown({ id, toggleDropdown }: DropdownProps) {
const { activeId, setActiveId } = toggleDropdown;
return (
<ul
onMouseEnter={() => setActiveId(id)}
onMouseLeave={() => setActiveId("")}
className={`${id === activeId ? "show" : ""} dropdown`}
>
<Link
style={{
textDecoration: "none",
}}
to={`/dashboard/users/${id}`}
>
<li>
<img src={viewDetails} alt="view icon" />
<p>View Details</p>
</li>
</Link>
<li>
<img src={blacklist} alt="view icon" />
<p>Blacklist User</p>
</li>
<li>
<img src={activate} alt="view icon" />
<p>Activate User</p>
</li>
</ul>
);
}
Inside the component, an unordered list (<ul>)
is rendered. The className of the <ul>
element is conditionally set based on whether the id
matches the activeId. If they match, the show class ( which makes the dropdown visible ) is added to the element, otherwise it remains empty.
The <ul>
element has two event handlers: onMouseEnter
and onMouseLeave
. When the mouse enters the element, the setActiveId
function is called with the id
as the argument, setting it as the activeId
. When the mouse leaves the element, the setActiveId
function is called with an empty string, clearing the activeId
.
Inside the <ul>
element, there are three list items (<li>
) representing the dropdown options. The first list item contains a Link component that wraps the content. The Link
component is used to create a clickable link that navigates to a specific URL. The to prop of the Link component is set to /dashboard/users/${id}
, which dynamically generates the URL based on the id
prop. This means that clicking on this option will navigate to the user details page for the corresponding id
.
This component is then imported to the table component and included it in the last element of the table like this:
<td>
<div>
<p className="status">Inactive</p>
<img
src={ellipsis}
alt="ellipsis icon"
onMouseEnter={() => setActiveId(id)}
/>
<Dropdown id={id} toggleDropdown={{ activeId, setActiveId }} />
</div>
</td>;
The end product of the dropdown looks like this:
Finally, I rendered the PaginatedItems
component in the users component created in the page folder. Ofcourse, wrapped around the page component initially discussed.
import PaginatedItems from "../components/dashboard/users/PaginatedItems"
export default function Users() {
return (
<Page>
<PaginatedItems/>
</Page>
)
}
If you have been with me up to this point, congratulations on surviving this wild ride through the User
page. Grab yourself a virtual cookie and take a well-deserved break! But hold on tight because in the next section, we'll be diving into the thrilling world of each user details, where we'll uncover secrets, unravel mysteries, and navigate through the intricacies of individual user profiles.
Journey's End: The User Details Page
Fortunately, unlike the users
page, the user page only requires implementing the data fetching and UI populating functions. However, there is an interesting twist. Once a user's information has been accessed, it needs to be saved in either the indexedDB
or localStorage
. The purpose of this storage is to avoid unnecessary network queries. If the user details data is already present in the localStorage
, we can simply retrieve it from there instead of making additional network requests.
To get started, I defined the route to the endpoint that calls a user's details in the requests.ts
file, located in the utils
folder, similar to how I did it for the users endpoint:
export const getUserById = (id: string) => request.get(`/users/${id}`);
This endpoint requires including the id
of each user in its query. Fortunately, with React Router, we can extract this id
from the URL once we have navigated to the user details page. Stay tuned for more exciting details!
Moving on, I created a file in the pages
folder and named it User-details.tsx
where I imported the previously defined endpoint for it to be called with the id
. This id
will be gotten from the URL with useParams
provided by react-router and passed to the API route as seen below:
import Page from "../components/dashboard/common/Page";
import { useParams } from "react-router-dom";
import { getUserById } from "../utils/requests";
import { useEffect, useState } from 'react';
export default function UserDetails():JSX.Element {
const [ user, setUser ] = useState<{[key: string]: any}>();
const { id } = useParams(); //get id from URL
return (
//markup here
)
}
Following this, I implemented the functionality to fetch and save to localStorage
in the fetchUserDetails
function included in the code snippet below:
import Page from "../components/dashboard/common/Page";
import { useParams } from "react-router-dom";
import { getUserById } from "../utils/requests";
import { useEffect, useState } from "react";
export default function UserDetails(): JSX.Element {
const [user, setUser] = useState<{ [key: string]: any }>();
const { id } = useParams();
const fetchUserDetails = (): void => {
const usersFromLocalStorage = localStorage.getItem("users");
if (usersFromLocalStorage) {
const users: { [key: string]: any }[] = JSON.parse(usersFromLocalStorage);
const user = users.find((u: { [key: string]: any }) => u.id === id);
if (user) {
setUser(user);
} else {
getUserById(id!)
.then(({ data }: AxiosResponse<{ [key: string]: any }>) => {
users.push(data);
localStorage.setItem("users", JSON.stringify(users));
setUser(data);
})
.catch((err) => console.log(err));
}
} else {
getUserById(id!)
.then(({ data }: AxiosResponse<{ [key: string]: any }>) => {
const users = [data];
localStorage.setItem("users", JSON.stringify(users));
setUser(data);
})
.catch((err) => console.log(err));
}
};
useEffect(() => {
fetchUserDetails();
}, [id]);
return (
<Page>
// UI population code here
</Page>
)
}
In the UserDetails
component, the fetchUserDetails
function is responsible for fetching and setting the user details. It first checks if the users property exists in the localStorage
. If it does, it searches for a user with a matching id and sets the user state accordingly.
If no matching user is found, a fresh API call is made using getUserById
and the retrieved data is added to the users array in the localStorage
. The user state is then set with the fetched data.
In the case where the users property does not exist in the localStorage
, a fresh API call is made to retrieve the user details. The retrieved data is stored in the users array in the localStorage
, and the user state is set with the fetched data.
After the fetchUserDetails
function is executed, the UI is populated with the relevant user details.
And thus, our adventurous quest through the intricate realm of user details comes to a triumphant end.
Conclusion
Well, despite my best efforts, one might think they would at least get a chance to proceed to the next stage, right? 😀 However, to my surprise, I received a rejection without the interviewer even signing into the app. Imagine finding out this tidbit from my Firebase Console! Life has a way of throwing unexpected twists in our coding journeys.
But fear not, my dear readers, for our shared adventure has not been in vain. I extend my deepest appreciation for your unwavering support and for joining me through the lines of code, the challenges, and the triumphs. 👊 Together, we have explored the intricacies of building an admin dashboard, overcoming obstacles along the way.
If you're curious to see the fruits of our labor, you can find the live site here, ready to be explored. For those interested in diving into the code, it awaits you on GitHub here.
Now, as I embark on new opportunities, I want to let you know that I'm still open to exploring frontend developer roles. If you or anyone you know is seeking a dedicated and passionate developer, please reach out to me via email at olasunkanmiibalogun@gmail.com. I'm eager to learn more about exciting opportunities that lie ahead.
Thank you once again for being part of this coding journey. Until we meet again in the realm of knowledge and exploration, may your code be bug-free and your programming endeavors be filled with joy and success. Happy coding!