My Loch Frontend Interview Experience | 80 LPA | Mumbai
Overview
Loch is a blockchain services company that describes itself as focused on "Truth Seeking Markets." The candidate applied for a frontend role through LinkedIn and was informed of a potential compensation package that included a 40-60% increase over their current salary, along with equity, totaling approximately 80 LPA (72 lakhs fixed).
Interview Rounds
The interview process at Loch consisted of three distinct, eliminatory rounds:
- Round 1: Screening Round
- Round 2: Home Take Assignment
- Round 3: Machine Coding + Technical Q&A
Round 2: Home Take Assignment
Following successful completion of the initial screening, the candidate received a take-home assignment. The task involved converting Figma designs into a fully functional and responsive frontend landing page for Loch. The landing page was intended to be displayed to users after clicking on a social media advertisement.
The assignment required the candidate to implement a landing page featuring a sign-up section and a carousel displaying details on the left side. The following steps outline the approach the candidate took:
Step 1: Basic Project Setup
The candidate initiated the project by creating a Vite.js project, although a ReactJS project would also have been acceptable. The candidate chose ViteJS because it was a specific requirement for this particular company.
npm install
Step 2: Writing Code
The candidate updated App.jsx, App.scss, and index.css with the following code:
Filename: src\App.jsx
// Styles
import './App.scss'
// Component
import Home from "./components/home";
function App() {
return (
<Home />
);
}
export default App;
Filename: src\App.scss
// Variables
@use "./styles/variables" as *;
#root {
width: 100%;
margin: 0 auto;
padding: 0;
text-align: center;
}
body {
margin: 0;
background: white;
color: $white;
font-family: "Inter", sans-serif;
overflow: hidden;
@media only screen and (max-width: 600px) {
overflow: unset;
}
}
p,
figure,
article {
margin: 0;
}
Filename: src\index.css
:root {
line-height: 1.2;
font-weight: 400;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
}
Next, the utilities folder was created. This folder contains utilities needed in the application.
Filename: src\utils\constants.js
export const ICON_LINKS = {
bellFilledIcon: "/assets/icons/bell-fill.svg",
cohortsImage: "/assets/cohorts.png",
eyeIcon: "/assets/icons/eye.svg",
starIcon: "/assets/icons/star.svg",
caretDownIcon: "/assets/icons/triangle-down.svg",
bellOutlineIcon: "/assets/icons/bell.svg",
barChartIcon: "/assets/icons/bar-chart.svg",
clockIcon: "/assets/icons/clock.svg"
};
export const INPUT_TYPE = {
EMAIL: 'email',
TEXT: 'text',
CHECKBOX: 'checkbox',
SUBMIT: 'submit'
}
export const NOTIFICATIONS = [
{
label: "We'll be sending notifications to you here",
placeholder: "hello@gmail.com",
type: "email",
icon: ICON_LINKS.bellOutlineIcon,
cta: true
},
{
label: "Notify me when any whale moves more than",
placeholder: "$1,000.00",
type: INPUT_TYPE.TEXT,
icon: ICON_LINKS.barChartIcon,
cta: false
},
{
label: "Notify me when any wallet dormant for",
placeholder: "> 30 days",
type: INPUT_TYPE.TEXT,
icon: ICON_LINKS.clockIcon,
footerText: "becomes active",
cta: false
},
];
export const TESTIMONIALS = [
{
quote:
"Love how Loch integrates portfolio analytics \n and whale watching into one unified app.",
author: "Jack F",
designation:"Ex Blackrock PM"
},
{
quote:
"I use Loch everyday now. I don't think I could \n analyze crypto whale trends or markets without it. \n I'm addicted!",
author: "Yash P",
designation:"Research, 3poch Crypto Hedge"
},
{
quote:
"Managing my own portfolio is helpful and \n well designed. What's really interesting is watching \n the whales though. No one else has made whale tracking \n so simple.",
author: "Shiv S",
designation:"Co-Founder Magik Labs"
},
];
export const LOCH_LANDING_PAGE_LINK = 'https://app.loch.one/welcome'
Filename: src\utils\helpers.js
// Function to check weather given email is correct or not
export const isValidEmail = (email = '') => {
return email.includes('@') && email.includes('.');
};
Then, the styles folder was created.
Filename: src\styles_variables.scss
// General
$white: #ffffff;
$black: #000000;
// Grays
$gray-100: #f2f2f2;
$gray-300: #d4d4d4;
$gray-500: #96979a;
$gray-800: #19191a;
// Backgrounds
$background-dark: #222;
$background-light: $white;
$background-radial-green: rgba(31, 169, 17, 0.810119);
$background-radial-blue: #2f15d0;
$background-input: #f9f9f9;
$background-form-button: $black;
$background-form-button-hover: #333;
// Border Colors
$border-light: $gray-300;
$border-dark: rgba(255, 255, 255, 0.68);
// Misc
$shadow-light: 0px 4px 10px 0px rgba(0, 0, 0, 0.04);
Finally, the components folder was created and the home, left-section, sign-up-form, and shared components were added.
Filename: src\components\home\index.jsx
import React from "react";
// Styles
import './styles.scss';
// Components
import SignUpForm from "../sign-up-form";
import LeftSection from "../left-section";
const Home = () => {
return (
<section className="main-wrapper">
<LeftSection />
<div className="right-side">
<SignUpForm />
</div>
</section>
);
};
export default Home;
Filename: src\components\home\styles.scss
// Variables
@use "../../styles/variables" as *;
.main-wrapper {
display: flex;
min-height: 100vh;
overflow: hidden;
.right-side {
background: white;
display: flex;
justify-content: center;
align-items: center;
padding: 2rem;
width: 45%;
@media only screen and (max-width: 600px) {
width: 100%;
padding: 0;
}
}
@media only screen and (max-width: 600px) {
flex-direction: column;
}
}
Filename: src\components\left-section\index.jsx
// React
import { useEffect, useState, useRef } from "react";
// Components
import InputCard from "../shared/input-card";
import TestimonialCard from "../shared/testimonial-card";
// Constants
import { NOTIFICATIONS, TESTIMONIALS } from "../../utils/constants";
import { LEFT_SECTION_CONTENT } from "./constants";
// Styles
import "./styles.scss";
const NotificationSection = () => {
const notificationCarouselRef = useRef(null);
const [isHovered, setIsHovered] = useState(false);
useEffect(() => {
const interval = setInterval(() => {
if (!isHovered && notificationCarouselRef.current) {
notificationCarouselRef.current.scrollLeft += 1;
if (
notificationCarouselRef.current.scrollLeft >=
notificationCarouselRef.current.scrollWidth -
notificationCarouselRef.current.clientWidth
) {
notificationCarouselRef.current.scrollLeft = 0;
}
}
}, 30);
return () => clearInterval(interval);
}, [isHovered]);
return (
<div className="flex-row first-section">
<article className="header-wrap">
<span>
<img
src={LEFT_SECTION_CONTENT.header?.icon}
alt="Notification bell icon"
/>
</span>
<h1 className="header-title">
{LEFT_SECTION_CONTENT.header?.title}
</h1>
<p className="header-description">
{LEFT_SECTION_CONTENT.header?.description}
</p>
</article>
<div
className="notification-inputs shadow-box"
ref={notificationCarouselRef}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{
[...NOTIFICATIONS, ...NOTIFICATIONS]
.map((item, index) => (
<InputCard
key={`notification-${item.type}-${index}`}
label={item.label}
placeholder={item.placeholder}
type={item.type}
icon={item.icon}
footerText={item.footerText}
cta={item.cta}
/>
))
}
</div>
</div>
);
};
const WatchSection = () => (
<div className="watch-section">
<figure className="dashboard-image">
<img
src={LEFT_SECTION_CONTENT.watchSection?.image}
alt="Whale analytics dashboard preview"
className="dashboard-img"
/>
</figure>
<article>
<span>
<img
src={LEFT_SECTION_CONTENT.watchSection?.icon}
alt="Eye watching icon"
/>
</span>
<h2 className="watch-title">
{LEFT_SECTION_CONTENT.watchSection?.title}
</h2>
<p className="watch-description">
{LEFT_SECTION_CONTENT.watchSection?.description}
</p>
</article>
</div>
);
const TestimonialSection = () => {
const testimonialCarouselRef = useRef(null);
const [isHovered, setIsHovered] = useState(false);
useEffect(() => {
const interval = setInterval(() => {
if (!isHovered && testimonialCarouselRef.current) {
testimonialCarouselRef.current.scrollLeft += 1;
if (
testimonialCarouselRef.current.scrollLeft >=
testimonialCarouselRef.current.scrollWidth -
testimonialCarouselRef.current.clientWidth
) {
testimonialCarouselRef.current.scrollLeft = 0;
}
}
}, 30);
return () => clearInterval(interval);
}, [isHovered]);
return (
<div className="testimonials">
<h3 className="testimonials-title">
{LEFT_SECTION_CONTENT.testimonials?.title}
</h3>
<div className="border-seprator" />
<div className="testimonial-wrap">
<figure>
<img
src={LEFT_SECTION_CONTENT.testimonials?.icon}
alt="Star icon for testimonials"
/>
</figure>
<article>
<div
className="testimonials-list"
ref={testimonialCarouselRef}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{
[...TESTIMONIALS, ...TESTIMONIALS]
.map((testimonial, index) => (
<TestimonialCard
key={`testimonial-${index}`}
quote={testimonial.quote}
author={testimonial.author}
designation={testimonial.designation}
/>
))
}
</div>
</article>
</div>
</div>
)
};
const LeftSection = () => (
<div className="left-side">
<NotificationSection />
<WatchSection />
<TestimonialSection />
</div>
);
export default LeftSection;
Filename: src\components\left-section\styles.scss
// Variables
@use "../../styles/variables" as *;
.left-side {
flex: 1;
padding: 2.813rem 0 1.875rem 0;
background-color: $black;
background: radial-gradient(
79.84% 79.84% at 32.27% 91.27%,
$background-radial-green 17.21%,
$background-radial-blue 64.58%,
$black 100%
);
width: 55%;
height: 100vh;
overflow-y: auto;
&::-webkit-scrollbar {
display: none;
}
.flex-row {
display: flex;
gap: 11px;
align-items: flex-start;
text-align: left;
@media only screen and (max-width: 600px) {
flex-direction: column;
flex-wrap: wrap;
}
}
.first-section {
margin-left: 60px;
position: relative;
.header-wrap {
width: 48%;
padding-right: 40px;
.header-title {
font-size: 1.938rem;
font-weight: 500;
margin: 16px 0;
text-align: left;
text-shadow: 2px 2px $background-dark;
line-height: 120%;
}
.header-description {
color: $gray-100;
font-size: 1rem;
font-weight: 500;
text-align: left;
margin: 0;
line-height: 120%;
@media only screen and (max-width: 600px) {
padding-right: 1.25rem;
}
}
@media only screen and (max-width: 600px) {
padding: 0;
width: 100%;
}
}
.shadow-box {
&::before {
content: "";
position: absolute;
width: 80px;
height: 171px;
z-index: 2;
pointer-events: none;
background: linear-gradient(to right,
rgba(0, 0, 0, 0.4), transparent);
}
&::after {
content: "";
position: absolute;
right: 0;
width: 80px;
height: 171px;
z-index: 2;
pointer-events: none;
background: linear-gradient(to left,
rgba(0, 0, 0, 0.4), transparent);
}
}
.notification-inputs {
display: flex;
flex-direction: row;
gap: 14px;
width: calc(100% - 322px);
overflow-x: scroll;
white-space: no-wrap;
&::-webkit-scrollbar {
display: none;
}
@media only screen and (max-width: 600px) {
width: 100%;
margin-top: 1.25rem;
}
}
@media only screen and (max-width: 600px) {
margin-left: 1.25rem;
}
}
.watch-section {
margin-top: 72px;
display: grid;
grid-template-columns: 1fr 1fr;
padding-right: 62px;
.dashboard-image {
border-radius: 12px;
margin: 0 0px 0px 60px;
.dashboard-img {
width: 100%;
border-radius: 0.5rem;
}
@media only screen and (max-width: 600px) {
border-radius: 12px;
margin: 0 0px 0px 1.25rem;
}
}
article {
text-align: right;
span {
img {
margin-top: 1.25rem;
}
}
.watch-title {
font-size: 31px;
font-weight: 500;
margin: 16px 0;
line-height: 120%;
padding-left: 6%;
}
.watch-description {
font-size: 16px;
color: rgba(242, 242, 242, 1);
font-weight: 500;
padding-left: 14%;
}
@media only screen and (max-width: 600px) {
margin-top: 1.875rem;
}
}
@media only screen and (max-width: 600px) {
grid-template-columns: auto;
padding-right: 1.25rem;
}
}
.testimonials {
margin-top: 22px;
margin-bottom: 100px;
.testimonials-title {
font-size: 25px;
font-weight: 500;
margin-bottom: 1.25rem;
text-align: right;
margin-right: 60px;
@media only screen and (max-width: 600px) {
margin-right: 1.25rem;
}
}
.border-seprator {
border-bottom: 1.5px solid $border-dark;
margin: 0 60px 40px;
@media only screen and (max-width: 600px) {
margin: 0 1.25rem 40px;
}
}
article {
&::-webkit-scrollbar {
display: none;
}
overflow-x: auto;
.testimonials-list {
display: flex;
flex-direction: row;
gap: 1rem;
padding-right: 1.25rem;
overflow-x: auto;
white-space: no-wrap;
&::-webkit-scrollbar {
display: none;
}
}
}
.testimonial-wrap {
display: flex;
gap: 40px;
align-items: flex-end;
figure {
margin-left: 3.75rem;
@media only screen and (max-width: 600px) {
margin-left: 1.25rem;
}
}
}
}
@media only screen and (max-width: 600px) {
width: 100%;
}
}
Filename: src\components\left-section\constants.js
// Constants
import { ICON_LINKS } from "../../utils/constants";
export const LEFT_SECTION_CONTENT = {
header: {
icon: ICON_LINKS.bellFilledIcon,
title: "Get notified when a highly correlated whale makes a move",
description:
"Find out when a certain whale moves more \n than any preset amount on-chain or when a \n dormant whale you care about becomes active.",
},
watchSection: {
image: ICON_LINKS.cohortsImage,
icon: ICON_LINKS.eyeIcon,
title: "Watch what the whales are doing",
description:
"All whales are not equal. Know exactly what \n the whales impacting YOUR portfolio are doing.",
},
testimonials: {
title: "Testimonials",
icon: ICON_LINKS.starIcon,
},
};
Filename: src\components\sign-up-form\index.jsx
import React, { useState } from 'react';
import PropTypes from 'prop-types';
// Styles
import './styles.scss'
// Constants
import { INPUT_TYPE, LOCH_LANDING_PAGE_LINK } from '../../utils/constants';
import { DEFAULT_FORM_DATA, ERROR_MESSAGE_EMAIL } from './constants';
// Utils
import { isValidEmail } from '../../utils/helpers';
const SignUpForm = ({
title = DEFAULT_FORM_DATA.title,
placeholder = DEFAULT_FORM_DATA.placeholder,
buttonText = DEFAULT_FORM_DATA.buttonText,
description = DEFAULT_FORM_DATA.description
}) => {
const [email, setEmail] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const handleSubmitForm = (e) => {
e.preventDefault();
if (!isValidEmail(email)) {
setErrorMessage(ERROR_MESSAGE_EMAIL);
return;
}
window.open(LOCH_LANDING_PAGE_LINK, '_blank');
};
const handleChangeEmail = (e) => {
setEmail(e.target.value);
setErrorMessage('');
};
return (
<form className='sign-up-form-wrap' onSubmit={handleSubmitForm}>
<h1 className='title'>{title}</h1>
<p className='description'>{description}</p>
<input
type={INPUT_TYPE.EMAIL}
className='email-input'
placeholder={placeholder}
onChange={handleChangeEmail}
/>
{errorMessage && <p className='error-message'>{errorMessage}</p>}
<button type={INPUT_TYPE.SUBMIT} className='form-button'>
{buttonText}
</button>
<p className='footer-text'>
By signing up, you agree to our{' '}
<a href='#' className='link'>
Terms of Service
</a>{' '}
and{' '}
<a href='#' className='link'>
Privacy Policy
</a>.
</p>
</form>
);
};
SignUpForm.propTypes = {
title: PropTypes.string,
placeholder: PropTypes.string,
buttonText: PropTypes.string,
description: PropTypes.string,
};
export default SignUpForm;
Key Takeaways
- The candidate was required to develop a responsive landing page from Figma designs.
- The assignment assessed the candidate's ability to translate designs into functional code, handle edge cases, and demonstrate logical thinking.
- Vite.js was used as the build tool, but ReactJS was also an acceptable alternative.
Original Source
This experience was originally published on medium. Support the author by visiting the original post.
Read on medium