Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test antoine #35

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NEXT_PUBLIC_API_URL=
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,61 @@

# Leboncoin Technical Test - Chat Application


https://github.com/Antoinelacombled/frontend-technical-test/assets/79117904/5eb84703-977a-437b-8b41-a2bd72aa3812


https://github.com/Antoinelacombled/frontend-technical-test/assets/79117904/6a27fcb1-2fb8-4d72-96b1-19ad5f44fcfc



# Introduction

This project showcases a simple yet effective chat application, designed and implemented as a technical test for Leboncoin. The focus was on creating user-friendly UI/UX, mirroring the simplicity of popular chat systems.

# Design

Initial designs focused on simplicity, avoiding user disorientation with complex changes.
Opted for separate pages for correspondents and conversations for clarity and focus on logic.

# Development

Created on a dedicated branch "test-antoine" to align with best practices in collaborative environments.
Utilized Styled-components for clearer, more organized code. Despite more experience with SASS and Tailwind, the scoping feature of Styled-components was advantageous.
Initial hard-coded implementation followed by dynamic data fetching.

Middleware in conversations.js: Faced an issue where the middleware intercepted the conversation/:id route, resulting in an empty array response. Resolved by adding a check in the conditional statement to exclude requests containing senderId, thus preventing unintended interceptions.

Persistent lastMessageTimestamp Update Issue: Encountered a bug where updates to lastMessageTimestamp in conversations were not reflected in real-time due to the static nature of the JSON database import. This requires server restarts to recognize updates in the JSON file, an issue that remains unresolved within the project's scope and time frame.

# Technical Challenges and Solutions

Implemented reusable API fetch functions for enhanced security, refactoring, and reusability.
Utilized useRef for auto-scrolling to the latest message in the chat.

# Version Control

Regular, feature-specific commits for clear historical tracking.

# UI Enhancements

Simple profile pictures using correspondents' first name initials.
Considered randomly generated backgrounds for each profile for personalization but prioritized time management.

# Future Improvements

Implement a login page redirection for non-logged users (current getLoggedUserId returns a hardcoded user).
Establish default color variables for consistent styling across components.
Explore WebSocket for real-time communication.
Enhance accessibility for inclusivity.
Having 100% code coverage.

# Conclusion

This project, while limited by time constraints, demonstrates key aspects of chat application development with a focus on user experience, technical problem-solving, and clean code practices.

---

# Context :

At leboncoin, our users can share messages about a transaction, or ask for informations about any products.
Expand All @@ -7,6 +65,8 @@ The interface needs to work on both desktop & mobile devices.

In addition to your code, a README explaining your thought process and your choices would be appreciated.

---

# Exercise :

- Display a list of all the conversations
Expand Down
27 changes: 27 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export const get = async (path: string) => {
const endpoint = process.env.NEXT_PUBLIC_API_URL;
const url = new URL(`${endpoint}${path}`);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(response.status.toString());
}
return response.json();
} catch (error) {
throw new Error(error);
}
};

export const getAll = async (paths: string[]) => {
const endpoint = process.env.NEXT_PUBLIC_API_URL;
const urls = paths.map((path) => new URL(`${endpoint}${path}`));
try {
const responses = await Promise.all(urls.map((url) => fetch(url)));
const jsons = await Promise.all(
responses.map((response) => response.json())
);
return jsons;
} catch (error) {
throw new Error(error);
}
};
46 changes: 46 additions & 0 deletions src/api/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
interface MessageInput {
conversationId: number;
userId: number;
body: string;
}

export const postMessage = async ({
conversationId,
userId,
body,
}: MessageInput) => {
try {
const responses = await Promise.all([
fetch(`${process.env.NEXT_PUBLIC_API_URL}messages/${conversationId}`, {
headers: {
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify({
conversationId,
timestamp: new Date().getTime(),
authorId: userId,
body,
}),
}),
fetch(
`${process.env.NEXT_PUBLIC_API_URL}conversation/${conversationId}`,
{
headers: {
"Content-Type": "application/json",
},
method: "PATCH",
body: JSON.stringify({
lastMessageTimestamp: new Date().getTime(),
}),
}
),
]);
const jsons = await Promise.all(
responses.map((response) => response.json())
);
return jsons;
} catch (error) {
throw new Error(error);
}
};
Binary file added src/assets/img-profilepic.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/lbc-logo-white.webp
Binary file not shown.
44 changes: 44 additions & 0 deletions src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from "react";
import {
CardInformations,
Container,
CardDate,
CardName,
ProfilePic,
MessagePreview,
} from "./cardStyledComponents";
import { Conversation } from "../../types/conversation";

import { getLoggedUserId } from "../../utils/getLoggedUserId";
import { getDateFormattedFromTimestamp } from "../../utils/getDateFormattedFromTimestamp";

interface Props {
conversation: Conversation;
}

function Card({ conversation }: Props) {
const userId = getLoggedUserId();

const displayName =
userId === conversation.recipientId
? conversation.senderNickname
: conversation.recipientNickname;

return (
<Container href={`/conversation/${conversation.id}`}>
<ProfilePic> {displayName.charAt(0).toUpperCase()} </ProfilePic>
<CardInformations>
<CardName>
{userId === conversation.recipientId
? conversation.senderNickname
: conversation.recipientNickname}
</CardName>
<CardDate>
🕓 {getDateFormattedFromTimestamp(conversation.lastMessageTimestamp)}
</CardDate>
</CardInformations>
</Container>
);
}

export default Card;
72 changes: 72 additions & 0 deletions src/components/Card/cardStyledComponents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import styled from "styled-components";

import Link from "next/link";

export const Container = styled(Link)`
cursor: pointer;
display: flex;
align-items: center;
margin: 0.5rem;
position: relative;
border-radius: 10px;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.9);
width: calc(100% + 8rem);
padding: 1.2rem;
color: grey;

&:hover {
background: rgba(255, 255, 255, 0.4);
box-shadow: 4.5px 4.5px 3.6px rgba(0, 0, 0, 0.01),
12.5px 12.5px 10px rgba(0, 0, 0, 0.016),
30.1px 30.1px 24.1px rgba(0, 0, 0, 0.024),
100px 100px 80px rgba(0, 0, 0, 0.04);
&::after {
content: "→";
position: absolute;
right: 10px;
font-size: 1.5rem;
}
}
`;

export const ProfilePic = styled.div`
border-radius: 50%;
width: 3rem;
height: 3rem;
margin-right: 1rem;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(300deg, #ff6633, #b71e07, #cfe50b);
background-size: 180% 180%;
animation: gradient-animation 18s ease infinite;
color: white;
font-size: 1.5rem;
font-weight: bold;
`;

export const CardInformations = styled.div`
display: flex;
flex-direction: column;
flex-grow: 1;
`;

export const CardName = styled.div`
font-size: 1rem;
font-weight: bold;
margin-bottom: 0.25rem;
color: rgba(8, 7, 7, 1);
`;

export const CardDate = styled.div`
color: grey;
font-size: 0.8rem;
`;

export const MessagePreview = styled.div`
font-size: 0.9rem;
color: grey;
margin-bottom: 0.5rem;
`;
80 changes: 80 additions & 0 deletions src/components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React, { useRef, useState, useEffect } from "react";
import {
ConversationContainer,
ConversationHeaderContainer,
Input,
InputContainer,
MessagesContainer,
Name,
} from "./chatStyledComponents";
import Messages from "../Messages/Messages";
import { Message } from "../../types/message";
import { Conversation } from "../../types/conversation";
import { getLoggedUserId } from "../../utils/getLoggedUserId";
import { ProfilePic } from "../Card/cardStyledComponents";
import { postMessage } from "../../api/message";
import { useRouter } from "next/router";

interface Props {
messages: Message[];
conversation: Conversation;
}

function Chat({ messages, conversation }: Props) {
const [message, setMessage] = useState<string>("");
const userId = getLoggedUserId();
const router = useRouter();
const lastMessageRef = useRef(null);

useEffect(() => {
if (lastMessageRef.current) {
lastMessageRef.current.scrollIntoView({ behavior: "smooth" });
}
}, [messages]);

const handleSendMessage = async () => {
if (message) {
const res = await postMessage({
conversationId: conversation.id,
userId,
body: message,
});
if (res.length) {
setMessage("");
router.replace(router.asPath);
}
}
};

const displayName =
userId === conversation.recipientId
? conversation.senderNickname
: conversation.recipientNickname;

return (
<ConversationContainer>
<ConversationHeaderContainer>
<ProfilePic>{displayName.charAt(0).toUpperCase()}</ProfilePic>
<Name>{displayName}</Name>
</ConversationHeaderContainer>
<MessagesContainer>
<Messages messages={messages} />
<div ref={lastMessageRef} />
</MessagesContainer>
<InputContainer>
<Input
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyPress={(e) => {
if (e.key === "Enter") {
handleSendMessage();
}
}}
placeholder="Type your message here ..."
/>
</InputContainer>
</ConversationContainer>
);
}

export default Chat;
44 changes: 44 additions & 0 deletions src/components/Chat/chatStyledComponents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from "react";
import styled from "styled-components";

export const ConversationContainer = styled.div`
display: flex;
flex-direction: column;
padding: 30px;
height: 90vh;
`;

export const MessagesContainer = styled.div`
overflow-y: scroll;
flex-grow: 1;
padding: 10px;
display: flex;
flex-direction: column;
gap: 10px;
`;

export const InputContainer = styled.div`
border-top: 1px solid #ccc;
padding: 10px;
`;

export const Input = styled.input`
width: 100%;
padding: 10px;
border: none;
border-radius: 20px;
box-shadow: 0px 1px 3px #00000029;
`;

export const ConversationHeaderContainer = styled.div`
display: flex;
align-items: center;
padding: 10px 20px;
border-bottom: 1px solid #ccc;
`;

export const Name = styled.span`
font-size: 18px;
font-weight: bold;
color: rgba(26, 26, 27, 1);
`;
Loading