diff --git a/README.md b/README.md
index 41ebece2..bb15a9dc 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,5 @@
# Happy Thoughts
+
+A Happy Thoughts app built with React and Hooks, where users can share and like positive messages fetched from an external API.
+
+https://tthiry-happythoughts.netlify.app
diff --git a/index.html b/index.html
index d4492e94..cf8549fb 100644
--- a/index.html
+++ b/index.html
@@ -4,6 +4,7 @@
+
Happy Thoughts
diff --git a/package.json b/package.json
index 2f66d295..2469ccb8 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,9 @@
},
"dependencies": {
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "react-router-dom": "^7.8.0",
+ "styled-components": "^6.1.19"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
diff --git a/src/App.css b/src/App.css
new file mode 100644
index 00000000..c2e7afff
--- /dev/null
+++ b/src/App.css
@@ -0,0 +1,356 @@
+/* Define CSS Variables */
+:root {
+ --primary-color: #FFA0C1;
+ --text-color: #1E1E1E;
+ --secondary-bg: #F0F0F0;
+ --disabled-bg: #D3D3D3;
+ --disabled-text: #A9A9A9;
+ --error-color: #c82020;
+ --placeholder-color: #767676;
+ --border-color: #CCCCCC;
+}
+
+/* Global Styles */
+body {
+ margin: 0;
+ padding: 0;
+ font-family: 'Segoe UI', sans-serif;
+ background: #F9F9F9;
+}
+
+h1 {
+ font-size: 1.5rem;
+ margin: 1rem 0;
+ text-align: center;
+}
+
+p {
+ color: var(--error-color);
+ max-width: 70%;
+}
+
+input {
+ color:rgb(171, 171, 171);
+ padding: 0.75rem;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ font-size: 1rem;
+}
+
+button {
+ background: var(--primary-color);
+ color: var(--text-color);
+ padding: 0.75rem;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ font-size: 1rem;
+ cursor: pointer;
+}
+
+.submit-button:disabled {
+ background-color: var(--disabled-bg);
+ color: var(--disabled-text);
+ cursor: not-allowed;
+ opacity: 0.7;
+}
+
+/* Layout */
+.outer-wrapper {
+ max-width: 500px;
+ margin: 0 auto;
+ padding: 0 1rem;
+}
+
+.app-container {
+ padding: 1rem;
+ background-color: #FFFFFF;
+ border: 1px solid var(--primary-color);
+ margin-bottom: 1rem;
+ box-shadow: 10px 10px 0 rgba(0, 0, 0, 0.7);
+}
+
+/* Header */
+.header {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 2rem 1rem 1rem 1rem;
+ flex-wrap: wrap;
+ text-align: center;
+}
+
+.header img {
+ width: 70px;
+ height: auto;
+}
+
+/* Message components */
+.message-container {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ margin-bottom: 4rem;
+ }
+
+ .message-card {
+ position: relative;
+ width: 100%;
+ padding: 1rem;
+ box-sizing: border-box;
+ background: #FFFFFF;
+ border: 1px solid var(--primary-color);
+ word-break: break-word;
+ box-shadow: 10px 10px 0 rgba(0, 0, 0, 0.7);
+ }
+
+ .message-card small {
+ font-size: 0.9rem;
+ font-weight: 300;
+ display: block;
+ margin-top: 1rem;
+ color: var(--placeholder-color);
+}
+
+.message-actions {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 1rem;
+}
+
+.message-list {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.like-button {
+ background-color: rgb(233, 233, 233);
+ border: 1px solid rgb(233, 233, 233);
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ padding: 0;
+}
+
+.like-button.liked {
+ background-color: var(--primary-color);
+ border-color: var(--primary-color);
+}
+
+.like-count {
+ margin-left: 8px;
+ font-size: 1rem;
+ color: var(--placeholder-color);
+}
+
+.time-ago {
+ margin-left: auto;
+ font-size: 0.8rem;
+ color: var(--placeholder-color);
+}
+
+/* Form styles */
+form {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ width: 100%;
+ max-width: 350px;
+}
+
+.message-form {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ margin: 2rem 0;
+}
+
+.message-form textarea {
+ resize: none;
+ padding: 1rem;
+ font-size: 1rem;
+ border-radius: 8px;
+ border: 1px solid var(--border-color);
+}
+
+.message-form textarea::placeholder {
+ color: var(--placeholder-color);
+}
+
+.message-form button {
+ width: 100%;
+ padding: 0.5rem;
+ font-size: 1rem;
+ background-color: var(--primary-color);
+ border: none;
+ border-radius: 8px;
+ color: var(--text-color);
+ cursor: pointer;
+ transition: background 0.3s ease;
+}
+
+/* Error Text */
+.error-text {
+ color: var(--error-color);
+ font-size: 0.9rem; /* Optional: Adjust font size */
+ margin-top: 0.5rem; /* Optional: Add spacing */
+}
+
+/* Edit/Delete Buttons */
+.button {
+ padding: 0.5rem 1rem;
+ border-radius: 4px;
+ border: none;
+ cursor: pointer;
+ font-size: 0.9rem;
+ transition: background 0.3s ease;
+}
+
+.save-button {
+ background-color: var(--primary-color);
+ color: var(--text-color);
+}
+
+.cancel-button {
+ background-color: var(--secondary-bg);
+ color: var(--text-color);
+}
+
+.submit-button {
+ background-color: var(--primary-color);
+ color: var(--text-color);
+}
+
+.submit-button:disabled {
+ background-color: var(--disabled-bg);
+ color: var(--disabled-text);
+ cursor: not-allowed;
+ opacity: 0.7;
+}
+
+.edit-button,
+.delete-button {
+ background: none;
+ border: none;
+ color: var(--text-color);
+ cursor: pointer;
+ font-size: 0.9rem;
+}
+
+.edit-button {
+ padding: 5px 10px;
+ border-radius: 5px;
+}
+
+.edit-button:hover,
+.delete-button:hover {
+ background-color: #E0E0E0;
+}
+
+.delete-message button {
+ background: none;
+ border: none;
+ color: var(--text-color);
+ cursor: pointer;
+}
+
+.delete-message button:hover {
+ background-color: var(--secondary-bg);
+ color: var(--error-color); /* Highlight the delete button in red */
+}
+
+.delete-message button:focus {
+ outline: 2px solid var(--primary-color); /* Add focus outline for accessibility */
+}
+
+div > div > p {
+ color: var(--text-color);
+}
+
+/* Signup/Login Containers */
+.signup-container,
+.login-container {
+ margin: 2rem;
+}
+
+/* Edit Actions */
+.top-right-actions {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ display: flex;
+}
+
+.edit-container {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.edit-textarea {
+ width: 80%;
+ padding: 0.5rem;
+ font-size: 1rem;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+}
+
+.edit-buttons {
+ display: flex;
+ gap: 0.5rem;
+ }
+
+
+/* Responsive design */
+@media (min-width: 400px) {
+ .header h1 {
+ font-size: 2rem;
+ margin: 2rem 0;
+ }
+
+ .header img {
+ width: 80px;
+ }
+
+ .top-right-actions {
+ gap: 0.5rem;
+ }
+}
+
+@media (max-width: 600px) {
+ .app-container {
+ padding: 1rem;
+ }
+
+ .message-form textarea {
+ font-size: 0.9rem;
+ }
+}
+
+@media (min-width: 768px) {
+ .header {
+ flex-direction: row;
+ justify-content: center;
+ text-align: left;
+ gap: 1rem;
+ }
+
+ .header h1 {
+ font-size: 2.25rem;
+ margin: 2rem 0;
+ }
+
+ .header img {
+ width: 100px;
+ }
+
+ .message-form button {
+ width: 50%;
+ }
+}
diff --git a/src/App.jsx b/src/App.jsx
index 07f2cbdf..eb745773 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,5 +1,144 @@
-export const App = () => {
+import { useState, useEffect } from 'react';
+import Home from './components/Home'
+import Signup from './components/Signup'
+import Login from './components/Login'
+import './App.css';
+
+const App = () => {
+ const [messages, setMessages] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [page, setPage] = useState('home');
+ const token = localStorage.getItem('accessToken');
+ const [isLoggedIn, setIsLoggedIn] = useState(!!token);
+
+
+ const fetchThoughts = () => {
+ setLoading(true);
+ fetch('https://happy-thoughts-api-svd7.onrender.com/thoughts')
+ // ')
+ .then(res => res.json())
+ .then(data => {
+ setMessages(data.response.slice(0, 5));
+ setLoading(false);
+ })
+ .catch(err => {
+ console.error(err);
+ setLoading(false);
+ });
+ };
+
+ useEffect(() => {
+ fetchThoughts();
+ }, []);
+
+ const addMessage = (newMessage) => {
+ setMessages(prev => [newMessage, ...prev]);
+ };
+
+ const handleLike = (id) => {
+ const token = localStorage.getItem('accessToken');
+
+
+ fetch(`https://happy-thoughts-api-svd7.onrender.com/thoughts/${id}/like`, {
+ method: 'PATCH',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ }
+ })
+ .then(res => res.json())
+ .then(() => {
+ setMessages(prev =>
+ prev.map(msg => msg._id === id ? { ...msg, hearts: msg.hearts + 1 } : msg)
+ );
+ })
+ .catch(err => console.error(err));
+ };
+
+ // Add handleEdit function here
+ const handleEdit = (id, updatedMessage) => {
+ const token = localStorage.getItem('accessToken');
+
+ fetch(`https://happy-thoughts-api-svd7.onrender.com/thoughts/${id}`, {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `${token}`,
+ },
+ body: JSON.stringify({ message: updatedMessage.trim() }),
+ })
+ .then((res) => res.json())
+ .then((data) => {
+ if (data.success) {
+ setMessages((prev) =>
+ prev.map((message) =>
+ message._id === id ? { ...message, message: updatedMessage } : message
+ )
+ );
+ } else {
+ console.error('Failed to edit thought:', data.message);
+ }
+ })
+ .catch(() => console.error('Network error while editing thought'));
+ }
+ // Add handleDelete function here
+ const handleDelete = (id) => {
+ const token = localStorage.getItem('accessToken');
+
+ fetch(`https://happy-thoughts-api-svd7.onrender.com/thoughts/${id}`, {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `${token}`,
+ },
+ })
+ .then((res) => res.json())
+ .then((data) => {
+ if (data.success) {
+ setMessages((prev) => prev.filter((message) => message._id !== id));
+ } else {
+ console.error('Failed to delete thought:', data.message);
+ }
+ })
+ .catch(() => console.error('Network error while deleting thought'));
+ };
+
+ const handleLogout = () => {
+ localStorage.removeItem('accessToken');
+ setIsLoggedIn(false);
+ setPage('login');
+ }
+
+ const handleLoginSuccess = () => {
+ setIsLoggedIn(true);
+ setPage('home');
+ fetchThoughts();
+ }
+
return (
- Happy Thoughts
- )
-}
+ <>
+
+
+ {page === 'signup' && }
+ {page === 'login' && }
+ {page === 'home' && (
+
+ )}
+ >
+ );
+};
+
+
+export default App;
diff --git a/src/assets/images/Sharing_thoughts.png b/src/assets/images/Sharing_thoughts.png
new file mode 100644
index 00000000..2c9a0f6e
Binary files /dev/null and b/src/assets/images/Sharing_thoughts.png differ
diff --git a/src/components/Home.jsx b/src/components/Home.jsx
new file mode 100644
index 00000000..c0864247
--- /dev/null
+++ b/src/components/Home.jsx
@@ -0,0 +1,42 @@
+import React from "react";
+import sharingImage from "../assets/images/Sharing_thoughts.png"
+import MessageForm from "./MessageForm";
+import MessageList from "./MessageList";
+
+
+const Home = ({ messages, loading, onLike, onAddMessage, onEdit, onDelete }) => {
+ const token = localStorage.getItem('accessToken');
+ const isDisabled = !token;
+
+ return (
+ <>
+
+
Happy Thoughts
+

+
+
+
+
What makes you happy right now?
+
+
+ {isDisabled && (
+
+ You need to be logged in to share your thoughts.
+
+ )}
+
+ {loading ?
+ (
Loading thoughts...
+ ) : (
+
+ )}
+
+
+ >
+ )
+}
+
+export default Home;
\ No newline at end of file
diff --git a/src/components/Login.jsx b/src/components/Login.jsx
new file mode 100644
index 00000000..68d41c42
--- /dev/null
+++ b/src/components/Login.jsx
@@ -0,0 +1,60 @@
+import { useState} from 'react';
+
+const Login = ({ onSuccess }) => {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+
+
+const handleSubmit = (e) => {
+ e.preventDefault();
+ setError('');
+
+
+fetch('https://happy-thoughts-api-svd7.onrender.com/users/login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email, password })
+})
+
+ .then(res => {
+ if (!res.ok) {
+ throw new Error('Login failed');
+ }
+ return res.json();
+ })
+ .then(data => {
+ localStorage.setItem('accessToken', data.accessToken);
+ onSuccess();
+ })
+ .catch(() => {
+ setError('Invalid email or password');
+ });
+};
+
+return (
+
+ );
+};
+
+export default Login
\ No newline at end of file
diff --git a/src/components/MessageCard.jsx b/src/components/MessageCard.jsx
new file mode 100644
index 00000000..14d1e6c4
--- /dev/null
+++ b/src/components/MessageCard.jsx
@@ -0,0 +1,95 @@
+import React, {useState} from 'react';
+
+function timeAgo(dateString) {
+ const now = new Date();
+ const then = new Date(dateString);
+ const diffMs = now - then;
+
+ const seconds = Math.floor(diffMs / 1000);
+ const minutes = Math.floor(seconds / 60);
+ const hours = Math.floor(minutes / 60);
+ const days = Math.floor(hours / 24);
+
+ if (days > 0) return `${days} day${days > 1 ? 's' : ''} ago`;
+ if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
+ if (minutes > 0) return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
+ return 'Just now';
+}
+
+const MessageCard = ({ message, onLike, onEdit, onDelete, disabled }) => {
+ const [isEditing, setIsEditing] = useState(false);
+ const [updatedMessage, setUpdatedMessage] = useState(message.message);
+
+ const handleEditClick = () => {
+ if (disabled) {
+ alert('You must be logged in to edit a message.');
+ return;
+ }
+ setIsEditing(true); // Enable editing mode
+ };
+
+ const handleSaveClick = () => {
+ onEdit(message._id, updatedMessage); // Call the onEdit function
+ setIsEditing(false); // Exit editing mode
+ };
+
+ const handleCancelClick = () => {
+ setUpdatedMessage(message.message); // Reset the message
+ setIsEditing(false); // Exit editing mode
+ };
+
+ return (
+
+ {isEditing ? (
+
+ ) : (
+
{message.message}
+ )}
+
+ {!isEditing && (
+
+ )}
+
+
+
+
+ x {message.hearts}
+ {timeAgo(message.createdAt)}
+
+
+ );
+};
+
+export default MessageCard;
\ No newline at end of file
diff --git a/src/components/MessageForm.jsx b/src/components/MessageForm.jsx
new file mode 100644
index 00000000..f7b04bc9
--- /dev/null
+++ b/src/components/MessageForm.jsx
@@ -0,0 +1,61 @@
+import { useState } from 'react';
+
+const MessageForm = ({ onAddMessage, disabled }) => {
+ const [message, setMessage] = useState('');
+ const [error, setError] = useState('');
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ if (disabled) {
+ setError('You must login to send a message.');
+ return;
+ }
+
+ if (message.trim().length < 5 || message.trim().length > 140) {
+ setError('Message must be between 5 and 140 characters');
+ return;
+ }
+
+ const token = localStorage.getItem('accessToken');
+
+ fetch('https://happy-thoughts-api-svd7.onrender.com/thoughts', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `${token}`
+ },
+ body: JSON.stringify({ message: message.trim() })
+ })
+ .then(res => res.json())
+ .then(data => {
+ if (data.message) {
+ onAddMessage(data.response);
+ setMessage('');
+ setError('');
+ } else {
+ setError('Something went wrong.');
+ }
+ })
+ .catch(() => setError('Network error'));
+ };
+
+ return (
+
+ );
+};
+
+export default MessageForm;
\ No newline at end of file
diff --git a/src/components/MessageList.jsx b/src/components/MessageList.jsx
new file mode 100644
index 00000000..cadc0643
--- /dev/null
+++ b/src/components/MessageList.jsx
@@ -0,0 +1,13 @@
+import MessageCard from './MessageCard';
+
+const MessageList = ({ messages, onLike, onEdit, onDelete, disabled }) => {
+ return (
+
+ {messages.map((message) => (
+
+ ))}
+
+ );
+};
+
+export default MessageList;
diff --git a/src/components/Signup.jsx b/src/components/Signup.jsx
new file mode 100644
index 00000000..dff67504
--- /dev/null
+++ b/src/components/Signup.jsx
@@ -0,0 +1,65 @@
+import { useState } from 'react';
+
+
+const Signup = ({onSuccess}) => {
+ const [formData, setFormData] = useState({
+ name: "",
+ email: "",
+ password: ""
+ });
+ const [error, setError] = useState(null);
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+
+
+ fetch('https://happy-thoughts-api-svd7.onrender.com/users/register', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json'},
+ body: JSON.stringify(formData)
+ })
+ .then(res => res.json())
+ .then(data => {
+ if (data.success) {
+ setError(null)
+ localStorage.setItem('accessToken', data.response.accessToken);
+ localStorage.setItem('userId', data.response.id);
+ onSuccess();
+ } else {
+ setError(data.message || 'Something went wrong.');
+ }
+ })
+ .catch(() => setError('Network error'));
+ };
+
+
+ return (
+
+ )
+
+}
+ export default Signup;
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
deleted file mode 100644
index f7c0aef5..00000000
--- a/src/index.css
+++ /dev/null
@@ -1,3 +0,0 @@
-:root {
- font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
-}
diff --git a/src/main.jsx b/src/main.jsx
index 1b8ffe9b..0b8d0334 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -1,9 +1,9 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
-import { App } from './App.jsx'
+import App from './App.jsx'
-import './index.css'
+import './App.css'
ReactDOM.createRoot(document.getElementById('root')).render(