diff --git a/package.json b/package.json index 74245b0c..557cd56b 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "moment": "^2.30.1", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/src/App.css b/src/App.css new file mode 100644 index 00000000..db94c379 --- /dev/null +++ b/src/App.css @@ -0,0 +1,7 @@ +.app-container { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + } + \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 1091d431..f112984b 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,3 +1,60 @@ -export const App = () => { - return
Find me in src/app.jsx!
; +import React, { useState, useEffect } from "react"; +import { ThoughtForm } from "./components/ThoughtForm"; +import { ThoughtList } from "./components/ThoughtList"; +import { LoadingSpinner } from "./components/LoadingSpinner"; +import './App.css'; + +const App = () => { + const [thoughts, setThoughts] = useState([]); + const [likedThoughts, setLikedThoughts] = useState(() => { + const savedLikes = localStorage.getItem("likedThoughts"); + return savedLikes ? JSON.parse(savedLikes) : []; + }); + + useEffect(() => { + const fetchThoughts = async () => { + try { + const response = await fetch("https://happy-thoughts-ux7hkzgmwa-uc.a.run.app/thoughts"); + const data = await response.json(); + setThoughts(data); + } catch (error) { + console.error("Error fetching thoughts:", error); + } + }; + fetchThoughts(); + }, []); + + useEffect(() => { + localStorage.setItem("likedThoughts", JSON.stringify(likedThoughts)); + }, [likedThoughts]); + + const handleLike = (thoughtId) => { + if (!likedThoughts.includes(thoughtId)) { + setLikedThoughts((prevLikes) => [...prevLikes, thoughtId]); + setThoughts((prevThoughts) => + prevThoughts.map((thought) => + thought._id === thoughtId + ? { ...thought, hearts: thought.hearts + 1 } + : thought + ) + ); + } + }; + + return ( +
+ + {thoughts.length === 0 ? ( + + ) : ( + + )} +
+ ); }; + +export default App; diff --git a/src/assets/heart.png b/src/assets/heart.png new file mode 100644 index 00000000..d5833566 Binary files /dev/null and b/src/assets/heart.png differ diff --git a/src/components/LoadingSpinner.css b/src/components/LoadingSpinner.css new file mode 100644 index 00000000..a67918e2 --- /dev/null +++ b/src/components/LoadingSpinner.css @@ -0,0 +1,15 @@ +.loading-spinner { + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + font-family: 'Courier New', monospace; + color: #000000; + background-color: #ffffff; + padding: 20px; + border-radius: 12px; +} + +.loading-spinner p { + margin: 0; +} \ No newline at end of file diff --git a/src/components/LoadingSpinner.jsx b/src/components/LoadingSpinner.jsx new file mode 100644 index 00000000..82cc3c29 --- /dev/null +++ b/src/components/LoadingSpinner.jsx @@ -0,0 +1,7 @@ +import './LoadingSpinner.css'; + +export const LoadingSpinner = () => ( +
+

Loading thoughts...

+
+); diff --git a/src/components/ThoughtForm.css b/src/components/ThoughtForm.css new file mode 100644 index 00000000..d07c67d7 --- /dev/null +++ b/src/components/ThoughtForm.css @@ -0,0 +1,58 @@ +.thought-form { + background-color: #E0E0E0; + border: 1px solid #000000; + padding: 20px; + border-radius: 8px; + box-shadow: 8px 8px 0 #000000; + font-family: 'Courier New', monospace; + max-width: 400px; + width: 100%; + margin: 20px auto; +} + +.thought-form h2 { + font-size: 1.5rem; + margin: 0 0 10px 0; + font-weight: bold; + color: #000000; +} + +.thought-form textarea { + width: calc(100% - 22px); /* Ensures textarea is aligned with other elements */ + height: 100px; + padding: 10px; + border: 1px solid #000000; + border-radius: 4px; + font-family: 'Courier New', monospace; + font-size: 1rem; + color: #000000; + background-color: #FFFFFF; + resize: none; + margin-bottom: 10px; + box-sizing: border-box; +} + +.character-count { + font-size: 0.9rem; + color: #000000; + text-align: right; + margin-bottom: 15px; +} + +.thought-form button { + width: 100%; + padding: 10px; + background-color: #000000; + color: #FFFFFF; + border: none; + font-size: 1rem; + font-family: 'Courier New', monospace; + cursor: pointer; + text-transform: uppercase; + transition: background-color 0.3s; +} + +.thought-form button:hover { + background-color: #333333; +} + \ No newline at end of file diff --git a/src/components/ThoughtForm.jsx b/src/components/ThoughtForm.jsx new file mode 100644 index 00000000..cc0f6454 --- /dev/null +++ b/src/components/ThoughtForm.jsx @@ -0,0 +1,66 @@ +import React, { useState } from "react"; +import './ThoughtForm.css'; + +export const ThoughtForm = ({ setThoughts }) => { + const [thought, setThought] = useState(""); + const [error, setError] = useState(""); + const [isPosting, setIsPosting] = useState(false); + + const minLength = 5; + const maxLength = 140; + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (thought.length < minLength) { + setError(`Thought must be at least ${minLength} characters.`); + return; + } + + setIsPosting(true); + try { + const response = await fetch( + "https://happy-thoughts-ux7hkzgmwa-uc.a.run.app/thoughts", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ message: thought }), + } + ); + if (!response.ok) throw new Error("Failed to post thought."); + + const newThought = await response.json(); + setThoughts((prevThoughts) => [newThought, ...prevThoughts]); + setThought(""); // This clears the input field + setError(""); + } catch (err) { + setError("Something went wrong. Please try again."); + } finally { + setIsPosting(false); + } + }; + + return ( +
+

Share a happy thought

+
+