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

+ Sharing 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 ( +
+
+

Login

+ {error &&

{error}

} + setEmail(e.target.value)} + required + /> + setPassword(e.target.value)} + required + /> + +
+
+ ); +}; + +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 ? ( +
+