Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
feea46a
adding initial files
JeffieJansson Dec 2, 2025
98a410e
create skeleton: add basic App, components code and base CSS
JeffieJansson Dec 2, 2025
7203f58
make ThoughtForm controlled with useState submit handler to call onAdd
JeffieJansson Dec 2, 2025
27e2bce
add thoughts state in App and pass add handler to ThoughtForm
JeffieJansson Dec 2, 2025
d476d26
render ThoughtCard for each thought in ThoughtList
JeffieJansson Dec 2, 2025
7e0a360
adding more css to match design for form and message cards
JeffieJansson Dec 2, 2025
7a910a7
change to input change handler to update text state and reset form fu…
JeffieJansson Dec 2, 2025
b4b1842
add time posted in hours and minutes
JeffieJansson Dec 2, 2025
f2c83f4
add char counter and disable submit when over 140 chars style and sma…
JeffieJansson Dec 3, 2025
fd5444e
add char counter and disable submit when over 140 chars
JeffieJansson Dec 3, 2025
16bc9b5
add meta tag
JeffieJansson Dec 3, 2025
fd8800a
add time ago on new thoughts
JeffieJansson Dec 3, 2025
5ae4814
remove resetForm, use setText, add char-count id and aria-describedby…
JeffieJansson Dec 3, 2025
62b788e
clean code and add comments
JeffieJansson Dec 3, 2025
e3e7315
changed so button is on the leftside of the form
JeffieJansson Dec 3, 2025
2c51903
change font family/size/weight
JeffieJansson Dec 3, 2025
8e0c7f0
use API to fetch existing/new thoughts and likes
JeffieJansson Dec 5, 2025
1eb0e5c
implement moment to use for timestamp
JeffieJansson Dec 5, 2025
cb8b576
add id to match APItick, onLike
JeffieJansson Dec 5, 2025
8e20a6c
add style and implement moment.js
JeffieJansson Dec 5, 2025
6b6a947
adding png for heart emoji and style it to match design
JeffieJansson Dec 5, 2025
3ff555e
add comment
JeffieJansson Dec 5, 2025
cdcd2a2
installed react time-ago for timestamp and removed {tick}
JeffieJansson Dec 9, 2025
89da30e
exploring moving fetching code to seperate js file for cleaner code
JeffieJansson Dec 9, 2025
a53c3a8
import api.js and extract error handling into a handleresponse helper…
JeffieJansson Dec 9, 2025
9b068db
extract error handling into a handleresponse helper function
JeffieJansson Dec 9, 2025
4a5c008
added a tooshort limit due to API most likely has a minimun of 5 char…
JeffieJansson Dec 9, 2025
f0ce0be
change parameter names to make code more readable
JeffieJansson Dec 9, 2025
4bb0999
add friendly error message and validation function
JeffieJansson Dec 10, 2025
d02f235
add error and loading state
JeffieJansson Dec 10, 2025
1b9e766
add error message, error/loading state and disable like button styles
JeffieJansson Dec 10, 2025
a99b039
add function to prevent multiple likes on single post
JeffieJansson Dec 10, 2025
d5166d3
added comments
JeffieJansson Dec 11, 2025
f028a89
clean code
JeffieJansson Dec 11, 2025
99d628d
enabled button when text area is empty to show please write something…
JeffieJansson Dec 11, 2025
baf6023
added text abot project in readme file and netlify link i Pull_reques…
JeffieJansson Dec 11, 2025
052a92b
added favicon
JeffieJansson Dec 11, 2025
188b732
clean extra spacing
JeffieJansson Dec 11, 2025
12c717d
final nitpicks
JeffieJansson Dec 12, 2025
f4757a4
created vanilla css files for each component instead of one index.css…
JeffieJansson Dec 14, 2025
882dbc2
Revise README project structure
JeffieJansson Dec 14, 2025
9a41547
Update README with minor content adjustments
JeffieJansson Dec 14, 2025
0989af3
adding the root variables to css files and fix src path to heart img
JeffieJansson Dec 14, 2025
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
108 changes: 108 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
11 changes: 6 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<link rel="icon" type="image/svg+xml" href="./heartfavicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="A simple React app where users can post and display happy thoughts."
/>
<title>Happy Thoughts</title>
</head>
<body>
<div id="root"></div>
<script
type="module"
src="./src/main.jsx">
</script>
<script type="module" src="./src/main.jsx"></script>
</body>
</html>
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Binary file added public/heart.png
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 public/heartfavicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
Please include your Netlify link here.
Please include your Netlify link here.
https://jennifer-happy-thoughts.netlify.app/
75 changes: 71 additions & 4 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,72 @@
export const App = () => {
return (
<h1>Happy Thoughts</h1>
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() {
// === STATE MANAGEMENT ===
const [thoughts, setThoughts] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)

// === INITIAL DATA FETCH ===
useEffect(() => { // Runs once when component mounts
fetchThoughts()
.then((data) => {
setThoughts(data) // Populate thoughts array with API data
setError(null) // Clear any previous errors
})
.catch((error) => {
console.error("Failed to fetch thoughts:", error)
setError("Failed to load thoughts. Please try again later.") // User-friendly message
})
.finally(() => { // ensure loading stops regardless of success or failure
setLoading(false) // Stop loading whether success or failure
})
}, []) // Empty array = run only once on mount, never re-run


// === EVENT HANDLERS ===

const handleFormSubmit = (message) => { //handleFormSubmit handles new thought submissions from ThoughtForm
postThought(message) // Send POST request to API
.then((newThought) => { // Receive newly created thought from API
setThoughts((previousThoughts) => [newThought, ...previousThoughts]) // Prepend new thought to existing array
})
.catch((error) => { // if POST fails catch the error and log it
console.error("Failed to submit thought:", error)
})
}

// handleLike processes like actions from ThoughtCard components
const handleLike = (thoughtId) => { // Receives the ID of the thought to like
likeThought(thoughtId) // Send POST request to like endpoint
.then((updatedThought) => {
setThoughts((previousThoughts) => // Update thoughts state with new like count
previousThoughts.map((thought) => // Map over existing thoughts
thought._id === updatedThought._id ? updatedThought : thought // Replace only the liked thought with updated data
)
)
})
.catch((error) => {
console.error('Failed to like thought:', error)
})
}

// === CONDITIONAL RENDERING ===
if (loading) {
return <div className="loading">Loading happy thoughts...</div>
}

if (error) {
return <div className="error">{error}</div>
}

// === MAIN RENDER ===
return (
<main className="app">
<ThoughtForm onAdd={handleFormSubmit} />
<ThoughtList thoughts={thoughts} onLike={handleLike} />
</main>
)
}
}
33 changes: 33 additions & 0 deletions src/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const API_URL = 'https://happy-thoughts-api-4ful.onrender.com/thoughts'

// handleResponse - Helper function to handle API responses: throws an error on failure, returns JSON on success.
async function handleResponse(response) {
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`)
}
return response.json()
}

// fetchThoughts retrieves an array of thought objects with _id, message, hearts, and createdAt
export async function fetchThoughts() {
const res = await fetch(API_URL)
return handleResponse(res)
}

// postThought makes a POST request to create a new happy thought.
// The backend will automatically add _id, hearts (0), and createdAt timestamp.
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)
}

// likeThought makes a POST request to the like endpoint.
// The backend increments the hearts counter and returns the updated thought.
export async function likeThought(thoughtId) {
const res = await fetch(`${API_URL}/${thoughtId}/like`, { method: 'POST' }) // POSTs to /:id/like.
return handleResponse(res)
}
40 changes: 40 additions & 0 deletions src/components/ThoughtCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useState } from 'react'
import ReactTimeAgo from 'react-timeago'
import '../styles/thoughtCard.css'

export const ThoughtCard = ({ thought, onLike }) => {

// === LOCAL STATE FOR LIKE SPAM PREVENTION ===
const [hasLiked, setHasLiked] = useState(false)

// === EVENT HANDLERS ===
const handleLike = () => {
if (hasLiked) return // disable multiple likes from same session
setHasLiked(true)
if (onLike) onLike(thought._id) // Trigger API call via parent
}

return (
<article className="thought">
<p className="message">{thought.message}</p>
<div className="meta">
<button
className={`like-button ${hasLiked ? 'liked' : ''}`}
onClick={handleLike}
aria-label={`Like this thought (${thought.hearts ?? 0} likes)`}
disabled={hasLiked} // Disable button if already liked in this session
>
<img src="./heart.png" alt="heart icon" className="hearts" />
</button>

{/* Like Count Display */}
<span className="likes">x{thought.hearts ?? 0}</span>

<span className="thought-time">
{/* ReactTimeAgo automatically updates the time string ("2 minutes ago" → "3 minutes ago") */}
{thought.createdAt ? <ReactTimeAgo date={thought.createdAt} /> : ''}
</span>
</div>
</article>
)
}
Loading