diff --git a/README.md b/README.md index 41ebece2..1f260774 100644 --- a/README.md +++ b/README.md @@ -1 +1,109 @@ # Happy Thoughts + +A Twitter-inspired positive thoughts sharing app built with React. Users can post happy thoughts (5-140 characters) and like others' thoughts. The app features real-time validation, character counting, and a responsive design. + +## Project Overview + +Happy Thoughts is a React application that allows users to share what makes them happy and spread positivity by liking others' thoughts. The app connects to a backend API to store and retrieve thoughts, providing a seamless user experience with instant feedback and validation. + +## Features + +- **Post Happy Thoughts**: Share what makes you happy with a simple form +- **Character Counter**: Real-time feedback showing remaining characters (turns red when exceeding limit) +- **Validation**: Friendly error messages for empty, too short, or too long messages +- **Like Thoughts**: Heart button to like others' thoughts (only once per session, spam prevention) +- **Loading States**: User-friendly loading indicators during API calls +- **Responsive Design**: Works seamlessly from mobile (320px) to desktop (1600px+) +- **Accessibility**: ARIA labels and live regions for screen reader support + +## Requirements Fulfilled + +### Core Requirements (7/7) + +- ✅ **Responsive Design**: App is fully responsive from 320px to 1600px+ width +- ✅ **Form Functionality**: Text area with submit button that empties form after submission +- ✅ **Clean Code**: Well-structured, commented code following best practices and DRY principles +- ✅ **API Integration**: Posts new thoughts to API on form submission +- ✅ **Thought Listing**: Displays thoughts with newest first, updates after submission +- ✅ **Display Message & Likes**: Shows thought content and like count for each thought +- ✅ **Like Functionality**: Heart button that sends likes and updates count in real-time + +### Stretch Goals (3/5 - Minimum 2 Required) + +- ✅ **Character Counter**: Live count of remaining characters, turns red when over 140 +- ✅ **Validation Error Messages**: User-friendly error messages for invalid input (empty, too short, too long) +- ✅ **Loading States**: Loading indicators during API calls with error handling +- ❌ Animation for new thoughts - not implemented +- ❌ Keep count av different liked posts + localStorage - not implemented + +## Technologies Used + +- **React** - Frontend framework with hooks (useState, useEffect) +- **react-timeago** - Automatic relative timestamps ("2 minutes ago") +- **Fetch API** - HTTP requests to backend +- **CSS** - Vanilla css for styling in this project +- **Vite** - Build tool and development server + +## Component Architecture + +``` +App.jsx (Main container) +├── State Management (thoughts, loading, error) +├── API Integration (fetch, post, like) +├── ThoughtForm.jsx (Create new thoughts) +│ ├── Controlled input with validation +│ ├── Character counter +│ └── Error messages +└── ThoughtList.jsx (Display thoughts) + └── ThoughtCard.jsx (Individual thought) + ├── Like button with spam prevention + └── Relative timestamps +``` + +## Project Structure + +``` +src/ +├── api.js +├── App.jsx +├── main.jsx +└── components/ + ├── ThoughtForm.jsx + ├── ThoughtList.jsx + └── ThoughtCard.jsx +└── styles/ +├── index.css +├── ThoughtForm.css +├── ThoughtList.css +├── ThoughtCard.css +``` + +## Key Implementation Details + +### Validation Logic + +- Minimum 5 characters, maximum 140 characters +- Real-time validation with visual feedback +- Submit button disabled only when typing invalid length (not when empty) +- Trim whitespace before submission ( hey) becomes (hey) + +### State Management + +- Global state in App.jsx for thoughts, loading, and error +- Local state in ThoughtCard for like spam prevention +- Functional setState to avoid race conditions + +### API Error Handling + +- Centralized error handling in `handleResponse` helper +- User-friendly error messages +- Console logging for debugging + +### Accessibility + +- ARIA labels on interactive elements +- aria-live regions for dynamic content updates +- Semantic HTML structure + +--- +Built with ❤️ as part of Technigo Bootcamp diff --git a/index.html b/index.html index d4492e94..fd9dac5d 100644 --- a/index.html +++ b/index.html @@ -2,15 +2,16 @@ - + + Happy Thoughts
- + diff --git a/package.json b/package.json index 2f66d295..5195dac2 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,10 @@ "preview": "vite preview" }, "dependencies": { + "moment": "^2.30.1", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-timeago": "^8.3.0" }, "devDependencies": { "@eslint/js": "^9.21.0", diff --git a/public/heart.png b/public/heart.png new file mode 100644 index 00000000..560599ff Binary files /dev/null and b/public/heart.png differ diff --git a/public/heartfavicon.png b/public/heartfavicon.png new file mode 100644 index 00000000..1b706d14 Binary files /dev/null and b/public/heartfavicon.png differ diff --git a/pull_request_template.md b/pull_request_template.md index 154c92e8..397b24b3 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -1 +1,2 @@ -Please include your Netlify link here. \ No newline at end of file +Please include your Netlify link here. +https://jennifer-happy-thoughts.netlify.app/ diff --git a/src/App.jsx b/src/App.jsx index 07f2cbdf..4ae03e13 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,67 @@ -export const App = () => { - return ( -

Happy Thoughts

+import React, { useState, useEffect } from 'react' +import { ThoughtForm } from './components/ThoughtForm' +import { ThoughtList } from './components/ThoughtList' +import { fetchThoughts, postThought, likeThought } from './api' + +export function App() { + const [thoughts, setThoughts] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + + useEffect(() => { + fetchThoughts() + .then((data) => { + setThoughts(data) + setError(null) + }) + .catch((error) => { + console.error("Failed to fetch thoughts:", error) + setError("Failed to load thoughts. Please try again later.") + }) + .finally(() => { + setLoading(false) + }) + }, []) + + + const handleFormSubmit = (message) => { + postThought(message) + .then((newThought) => { + setThoughts((previousThoughts) => [newThought, ...previousThoughts]) + }) + .catch((error) => { + console.error("Failed to submit thought:", error) + }) + } + + + const handleLike = (thoughtId) => { + likeThought(thoughtId) + .then((updatedThought) => { + setThoughts((previousThoughts) => + previousThoughts.map((thought) => + thought._id === updatedThought._id ? updatedThought : thought + ) + ) + }) + .catch((error) => { + console.error('Failed to like thought:', error) + }) + } + + if (loading) { + return
Loading happy thoughts...
+ } + + if (error) { + return
{error}
+ } + + return ( +
+ + +
) -} +} \ No newline at end of file diff --git a/src/api.js b/src/api.js new file mode 100644 index 00000000..27db6d0a --- /dev/null +++ b/src/api.js @@ -0,0 +1,27 @@ +const API_URL = 'https://happy-thoughts-api-4ful.onrender.com/thoughts' + +async function handleResponse(response) { + if (!response.ok) { + throw new Error(`API request failed with status ${response.status}`) + } + return response.json() +} + +export async function fetchThoughts() { + const res = await fetch(API_URL) + return handleResponse(res) +} + +export async function postThought(message) { + const res = await fetch(API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message }), + }) + return handleResponse(res) +} + +export async function likeThought(thoughtId) { + const res = await fetch(`${API_URL}/${thoughtId}/like`, { method: 'POST' }) + return handleResponse(res) +} \ No newline at end of file diff --git a/src/components/ThoughtCard.jsx b/src/components/ThoughtCard.jsx new file mode 100644 index 00000000..d433781a --- /dev/null +++ b/src/components/ThoughtCard.jsx @@ -0,0 +1,35 @@ +import { useState } from 'react' +import ReactTimeAgo from 'react-timeago' +import '../styles/thoughtCard.css' + +export const ThoughtCard = ({ thought, onLike }) => { + + const [hasLiked, setHasLiked] = useState(false) + const handleLike = () => { + if (hasLiked) return + setHasLiked(true) + if (onLike) onLike(thought._id) + } + + return ( +
+

{thought.message}

+
+ + + x{thought.hearts ?? 0} + + + {thought.createdAt ? : ''} + +
+
+ ) +} \ No newline at end of file diff --git a/src/components/ThoughtForm.jsx b/src/components/ThoughtForm.jsx new file mode 100644 index 00000000..cd97e985 --- /dev/null +++ b/src/components/ThoughtForm.jsx @@ -0,0 +1,99 @@ +import { useState } from 'react' +import '../styles/thoughtForm.css' + +const CHAR_LIMIT = 140 +const CHAR_MIN = 5 + +export const ThoughtForm = ({ onAdd }) => { + + const [text, setText] = useState('') + const [error, setError] = useState(null) + const isOverLimit = text.trim().length > CHAR_LIMIT + const isTooShort = text.trim().length > 0 && text.trim().length < CHAR_MIN + const remaining = CHAR_LIMIT - text.trim().length + + const isValid = () => { + const trimmedLength = text.trim().length + return trimmedLength >= CHAR_MIN && trimmedLength <= CHAR_LIMIT + } + const shouldDisableButton = () => { + if (text.length === 0) return false + return isTooShort || isOverLimit + } + + const handleInputChange = (e) => { + setText(e.target.value) + if (error) setError(null) + } + + const handleSubmit = (e) => { + e.preventDefault() + + if (!text.trim()) { + setError('Please write something!') + return + } + + if (!isValid()) { + setError('Message must be between 5 and 140 characters') + return + } + + onAdd(text.trim()) + setText('') + setError(null) + } + + + return ( +
+
+ + +