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 = () => (
+
+);
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
+
+
+ );
+};
diff --git a/src/components/ThoughtItem.css b/src/components/ThoughtItem.css
new file mode 100644
index 00000000..e7378f26
--- /dev/null
+++ b/src/components/ThoughtItem.css
@@ -0,0 +1,53 @@
+.thought-item {
+ background-color: #FFFFFF;
+ border: 1px solid #000000;
+ border-radius: 8px;
+ padding: 10px 15px;
+ font-family: 'Courier New', monospace;
+ font-size: 1rem;
+ color: #000000;
+ box-shadow: 4px 4px 0 #000000;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.thought-item p {
+ margin: 0 0 10px 0;
+}
+
+.thought-item .bottom-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 0.9rem;
+ color: #555555;
+}
+
+.thought-item .heart-container {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+}
+
+.thought-item .heart-container button {
+ background: none;
+ border: none;
+ padding: 0;
+ cursor: pointer;
+}
+
+.thought-item .heart-container button.liked {
+ cursor: default;
+ opacity: 0.5;
+}
+
+.thought-item .heart-container img {
+ width: 20px;
+ height: 20px;
+ vertical-align: middle;
+}
+
+.thought-item .heart-container button.liked img {
+ filter: grayscale(100%);
+}
+
\ No newline at end of file
diff --git a/src/components/ThoughtItem.jsx b/src/components/ThoughtItem.jsx
new file mode 100644
index 00000000..0d431d46
--- /dev/null
+++ b/src/components/ThoughtItem.jsx
@@ -0,0 +1,37 @@
+import './ThoughtItem.css';
+import likeIcon from '../assets/heart.png';
+import moment from 'moment';
+
+export const ThoughtItem = ({ thought, isLiked, onLike }) => {
+ const handleLike = async () => {
+ if (isLiked) return;
+
+ try {
+ await fetch(`https://happy-thoughts-ux7hkzgmwa-uc.a.run.app/thoughts/${thought._id}/like`, {
+ method: 'POST',
+ });
+ onLike(thought._id);
+ } catch (error) {
+ console.error('Failed to like thought:', error);
+ }
+ };
+
+ return (
+
+
{thought.message}
+
+
+
+
x {thought.hearts}
+
+
{moment(thought.createdAt).fromNow()}
+
+
+ );
+};
diff --git a/src/components/ThoughtList.css b/src/components/ThoughtList.css
new file mode 100644
index 00000000..4c8201a1
--- /dev/null
+++ b/src/components/ThoughtList.css
@@ -0,0 +1,10 @@
+.thought-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ max-width: 350px;
+ width: 100%;
+ font-family: Arial, sans-serif;
+ margin: 20px auto 0;
+ padding: 0;
+}
\ No newline at end of file
diff --git a/src/components/ThoughtList.jsx b/src/components/ThoughtList.jsx
new file mode 100644
index 00000000..d15f2bf7
--- /dev/null
+++ b/src/components/ThoughtList.jsx
@@ -0,0 +1,15 @@
+import './ThoughtList.css';
+import { ThoughtItem } from "./ThoughtItem";
+
+export const ThoughtList = ({ thoughts, onLike, likedThoughts }) => (
+
+ {thoughts.map((thought) => (
+
+ ))}
+
+);
diff --git a/src/index.css b/src/index.css
index 4558f538..85014de4 100644
--- a/src/index.css
+++ b/src/index.css
@@ -11,3 +11,11 @@ code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
+
+.main-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 20px;
+}
+
diff --git a/src/main.jsx b/src/main.jsx
index 51294f39..b91620d3 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -1,6 +1,6 @@
import React from "react";
import ReactDOM from "react-dom/client";
-import { App } from "./App.jsx";
+import App from "./App.jsx";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")).render(