From bdbf1956df511db6fedfbb09f0cd4cf8b9faa5ef Mon Sep 17 00:00:00 2001 From: "Eugene \"Pebbles\" Akiwumi" <62018288+akiwumi@users.noreply.github.com> Date: Thu, 4 Dec 2025 13:04:42 +0100 Subject: [PATCH 1/3] Add character counter, error handling, animations, and design system matching image --- src/App.css | 246 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/App.jsx | 189 +++++++++++++++++++++++++++++++++++++- src/index.css | 15 ++- 3 files changed, 448 insertions(+), 2 deletions(-) create mode 100644 src/App.css diff --git a/src/App.css b/src/App.css new file mode 100644 index 00000000..a518f7e9 --- /dev/null +++ b/src/App.css @@ -0,0 +1,246 @@ +.app { + max-width: 500px; + margin: 40px auto; + padding: 0 20px; + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; +} + +/* Form Styles */ +.thought-form { + background-color: #f2f0f0; + padding: 20px; + margin-bottom: 20px; + box-shadow: 2px 2px 0 0 rgba(0, 0, 0, 0.1); + border-radius: 0; +} + +.form-title { + font-size: 16px; + font-weight: 600; + margin: 0 0 12px 0; + color: #000; +} + +.thought-input { + width: 100%; + min-height: 80px; + padding: 12px; + border: 1px solid #d1d1d1; + background-color: #fff; + font-family: inherit; + font-size: 14px; + resize: vertical; + box-sizing: border-box; + margin-bottom: 12px; +} + +.thought-input:focus { + outline: 2px solid #ffadad; + outline-offset: 2px; + border-color: #ffadad; +} + +.thought-input:disabled { + background-color: #f5f5f5; + cursor: not-allowed; +} + +.form-footer { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 12px; +} + +.char-count-container { + flex: 1; + min-width: 150px; +} + +.char-count { + font-size: 12px; + color: #666; +} + +.char-count-over { + color: #ff0000; + font-weight: 600; +} + +.submit-button { + background-color: #ffadad; + color: #000; + border: none; + padding: 10px 20px; + border-radius: 25px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + box-shadow: 0 0 8px rgba(255, 0, 0, 0.3); + transition: all 0.2s ease; +} + +.submit-button:hover:not(:disabled) { + background-color: #ff9d9d; + box-shadow: 0 0 12px rgba(255, 0, 0, 0.4); + transform: translateY(-1px); +} + +.submit-button:active:not(:disabled) { + transform: translateY(0); +} + +.submit-button:disabled { + background-color: #d1d1d1; + cursor: not-allowed; + box-shadow: none; + opacity: 0.6; +} + +.heart-emoji { + font-size: 14px; +} + +.error-message { + margin-top: 12px; + padding: 10px; + background-color: #ffe0e0; + border: 1px solid #ffadad; + border-radius: 4px; + color: #d32f2f; + font-size: 13px; + animation: slideIn 0.3s ease; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Thoughts List */ +.thoughts-list { + display: flex; + flex-direction: column; + gap: 20px; +} + +.loading { + text-align: center; + padding: 40px; + color: #666; +} + +.thought-card { + background-color: #f2f0f0; + padding: 20px; + box-shadow: 2px 2px 0 0 rgba(0, 0, 0, 0.1); + border-radius: 0; + animation: fadeInUp 0.5s ease; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.thought-content { + display: flex; + flex-direction: column; + gap: 12px; +} + +.thought-message { + margin: 0; + font-size: 14px; + line-height: 1.5; + color: #000; + word-wrap: break-word; +} + +.thought-footer { + display: flex; + justify-content: space-between; + align-items: center; +} + +.like-button { + display: flex; + align-items: center; + gap: 6px; + background: none; + border: none; + cursor: pointer; + padding: 4px 8px; + border-radius: 4px; + transition: background-color 0.2s ease; +} + +.like-button:hover { + background-color: rgba(255, 173, 173, 0.2); +} + +.like-icon { + background-color: #fff; + width: 32px; + height: 32px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.heart-emoji-small { + font-size: 14px; + line-height: 1; +} + +.like-count { + font-size: 13px; + color: #000; + font-weight: 500; +} + +.thought-time { + font-size: 12px; + color: #666; +} + +/* Responsive */ +@media (max-width: 600px) { + .app { + margin: 20px auto; + padding: 0 15px; + } + + .form-footer { + flex-direction: column; + align-items: stretch; + } + + .char-count-container { + text-align: center; + } + + .submit-button { + width: 100%; + justify-content: center; + } +} + diff --git a/src/App.jsx b/src/App.jsx index 07f2cbdf..f7605951 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,192 @@ +import { useState, useEffect } from 'react' +import './App.css' + +const API_URL = 'https://happy-thoughts-technigo.herokuapp.com/thoughts' + export const App = () => { + const [thoughts, setThoughts] = useState([]) + const [newThought, setNewThought] = useState('') + const [error, setError] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + + const maxLength = 140 + const minLength = 5 + const remainingChars = maxLength - newThought.length + const isOverLimit = newThought.length > maxLength + + useEffect(() => { + fetchThoughts() + }, []) + + const fetchThoughts = async () => { + setIsLoading(true) + try { + const response = await fetch(API_URL) + const data = await response.json() + setThoughts(data) + } catch (err) { + setError('Failed to load thoughts. Please try again later.') + } finally { + setIsLoading(false) + } + } + + const handleSubmit = async (e) => { + e.preventDefault() + setError('') + + // Validation + if (!newThought.trim()) { + setError('Your thought cannot be empty. Please write something!') + return + } + + if (newThought.length < minLength) { + setError(`Your thought is too short. Please write at least ${minLength} characters.`) + return + } + + if (newThought.length > maxLength) { + setError(`Your thought is too long. Please keep it under ${maxLength} characters.`) + return + } + + setIsSubmitting(true) + try { + const response = await fetch(API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ message: newThought }), + }) + + const data = await response.json() + + if (!response.ok) { + // Handle API error messages + if (data.errors) { + if (data.errors.message) { + setError(data.errors.message) + } else { + setError('Something went wrong. Please try again.') + } + } else { + setError('Something went wrong. Please try again.') + } + return + } + + // Clear input and add new thought to the list with animation + setNewThought('') + setThoughts([data, ...thoughts]) + } catch (err) { + setError('Failed to post your thought. Please check your connection and try again.') + } finally { + setIsSubmitting(false) + } + } + + const handleLike = async (thoughtId) => { + try { + const response = await fetch(`${API_URL}/${thoughtId}/like`, { + method: 'POST', + }) + + if (response.ok) { + const updatedThought = await response.json() + setThoughts((prevThoughts) => + prevThoughts.map((thought) => + thought._id === thoughtId ? updatedThought : thought + ) + ) + } + } catch (err) { + // Silently fail for likes + console.error('Failed to like thought:', err) + } + } + + const formatTimeAgo = (createdAt) => { + const now = new Date() + const created = new Date(createdAt) + const seconds = Math.floor((now - created) / 1000) + + if (seconds < 60) { + return `${seconds} second${seconds !== 1 ? 's' : ''} ago` + } + const minutes = Math.floor(seconds / 60) + if (minutes < 60) { + return `${minutes} minute${minutes !== 1 ? 's' : ''} ago` + } + const hours = Math.floor(minutes / 60) + if (hours < 24) { + return `${hours} hour${hours !== 1 ? 's' : ''} ago` + } + const days = Math.floor(hours / 24) + return `${days} day${days !== 1 ? 's' : ''} ago` + } + return ( -

Happy Thoughts

+
+
+

What's making you happy right now?

+