From 26eabc12c378ad403be422e52febf7a41e970e7a Mon Sep 17 00:00:00 2001 From: Mika Date: Sun, 27 Oct 2024 22:21:08 +0100 Subject: [PATCH 1/2] requirements fulfilled --- src/App.css | 99 +++++++++++++++++++++++++++++++ src/App.jsx | 64 +++++++++++++++++++- src/components/HeartButton.jsx | 11 ++++ src/components/InputForm.jsx | 54 +++++++++++++++++ src/components/MessageContent.jsx | 3 + src/components/MessageFooter.jsx | 20 +++++++ src/components/Thought.jsx | 17 ++++++ src/utils/api.js | 30 ++++++++++ 8 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 src/App.css create mode 100644 src/components/HeartButton.jsx create mode 100644 src/components/InputForm.jsx create mode 100644 src/components/MessageContent.jsx create mode 100644 src/components/MessageFooter.jsx create mode 100644 src/components/Thought.jsx create mode 100644 src/utils/api.js diff --git a/src/App.css b/src/App.css new file mode 100644 index 00000000..74c82807 --- /dev/null +++ b/src/App.css @@ -0,0 +1,99 @@ +@import url("https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap"); + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: "Roboto Mono", monospace; + width: 100vw; + margin: 0 auto; +} + +.thought-container { + border: 1px solid black; + padding: 20px; + margin: 50px; + box-shadow: 7px 8px 0px rgba(0, 0, 0, 1); + max-width: 450px; +} + +.input-form { + background-color: #cdc3c082; + display: flex; + flex-direction: column; +} + +form { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +input[type="text"] { + width: 400px; + padding: 20px; + margin: 20px 0; +} + +ul, +li { + list-style: none; + color: #828282; + display: flex; + justify-content: space-between; +} + +button { + border: none; + background-color: #cdc3c082; + width: 40px; + height: 40px; + border-radius: 50%; + margin-right: 10px; + font-size: 18px; +} + +.submit { + border-radius: 20px; + width: fit-content; + padding: 0 20px; +} + +/* button:hover { + background-color: #cdc3c0; +} */ + +.liked { + background-color: #da2c3d71; + color: rgb(0, 0, 0, 0.8); + font-weight: 550; +} + +.heart-container { + display: flex; + align-items: center; +} + +.thought-footer { + display: flex; + justify-content: space-between; + align-items: center; +} + +.thought-message { + margin-bottom: 30px; + font-size: 20px; + font-family: "Roboto Mono", monospace; + word-wrap: break-word; +} + +.char-counter { + margin-bottom: 10px; +} + +.error { + color: red; +} diff --git a/src/App.jsx b/src/App.jsx index 1091d431..cae6c12c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,3 +1,65 @@ +import { useEffect, useState } from "react"; +import "./App.css"; +import { Thought } from "./components/Thought"; +import { InputForm } from "./components/InputForm"; +import { fetchData, URL } from "./utils/api"; + export const App = () => { - return
Find me in src/app.jsx!
; + const [thoughts, setThoughts] = useState([]); + const [likedThoughts, setLikedThoughts] = useState([]); + const [message, setMessage] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + fetchData().then((data) => setThoughts(data)); + }, []); + + const handleHeartClick = async (id) => { + try { + const res = await fetch(`${URL}/${id}/like`, { + method: "POST", + }); + + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } + + const updatedThought = await res.json(); + + // Update thoughts with new heart count from server + setThoughts((prevThoughts) => + prevThoughts.map((thought) => + thought._id === id + ? { ...thought, hearts: updatedThought.hearts } + : thought + ) + ); + + // Is not already liked, dd to likedThoughts array + setLikedThoughts((prev) => + prev.includes(id) + ? prev.filter((thoughtId) => thoughtId !== id) + : [...prev, id] + ); + } catch (error) { + console.error("Error liking thought:", error); + } + }; + + return ( + thoughts && ( + <> + + + + ) + ); }; diff --git a/src/components/HeartButton.jsx b/src/components/HeartButton.jsx new file mode 100644 index 00000000..2e872e90 --- /dev/null +++ b/src/components/HeartButton.jsx @@ -0,0 +1,11 @@ +export const HeartButton = ({ thoughtId, hearts, handleHeartClick, isLiked }) => ( +
+ +
  • x {hearts}
  • +
    +); diff --git a/src/components/InputForm.jsx b/src/components/InputForm.jsx new file mode 100644 index 00000000..6a69345f --- /dev/null +++ b/src/components/InputForm.jsx @@ -0,0 +1,54 @@ +import { URL } from "../utils/api"; + +export const InputForm = ({ setMessage, message, setThoughts }) => { + const handleSubmit = async (event) => { + event.preventDefault(); + + try { + const res = await fetch(URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ message: message }), + }); + + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } + + const data = await res.json(); + setThoughts((prevThoughts) => [data, ...prevThoughts]); + setMessage(""); + } catch (error) { + console.error("Error submitting thought:", error); + } + }; + + const MAX_CHARS = 140; + + return ( +
    + What's making you happy right now? +
    + setMessage(e.target.value)} + required + /> +
    MAX_CHARS ? "error" : ""}`} + > + {MAX_CHARS - message.length} characters remaining +
    + +
    +
    + ); +}; diff --git a/src/components/MessageContent.jsx b/src/components/MessageContent.jsx new file mode 100644 index 00000000..58733f37 --- /dev/null +++ b/src/components/MessageContent.jsx @@ -0,0 +1,3 @@ +export const MessageContent = ({ message }) => ( +
    {message}
    +); diff --git a/src/components/MessageFooter.jsx b/src/components/MessageFooter.jsx new file mode 100644 index 00000000..7b64da25 --- /dev/null +++ b/src/components/MessageFooter.jsx @@ -0,0 +1,20 @@ +import { HeartButton } from "./HeartButton"; +import { formatTimeAgo } from "../utils/api"; + +export const MessageFooter = ({ + thoughtId, + hearts, + createdAt, + handleHeartClick, + isLiked, +}) => ( +
    + +
  • {formatTimeAgo(createdAt)}
  • +
    +); diff --git a/src/components/Thought.jsx b/src/components/Thought.jsx new file mode 100644 index 00000000..4cf2af5c --- /dev/null +++ b/src/components/Thought.jsx @@ -0,0 +1,17 @@ +import { MessageContent } from "./MessageContent"; +import { MessageFooter } from "./MessageFooter"; + +export const Thought = ({ thoughts, handleHeartClick, likedThoughts }) => { + return thoughts.map((thought) => ( +
    + + +
    + )); +}; diff --git a/src/utils/api.js b/src/utils/api.js new file mode 100644 index 00000000..e8af2a07 --- /dev/null +++ b/src/utils/api.js @@ -0,0 +1,30 @@ +export const URL = "https://happy-thoughts-ux7hkzgmwa-uc.a.run.app/thoughts"; + +export const fetchData = async () => { + const response = await fetch(URL); + const data = await response.json(); + return data; +}; + +export const formatTimeAgo = (timestamp) => { + const now = new Date(); + const messageDate = new Date(timestamp); + const diffInSeconds = Math.floor((now - messageDate) / 1000); + + if (diffInSeconds < 60) { + return `${diffInSeconds} seconds ago`; + } + + const diffInMinutes = Math.floor(diffInSeconds / 60); + if (diffInMinutes < 60) { + return `${diffInMinutes} ${diffInMinutes === 1 ? "minute" : "minutes"} ago`; + } + + const diffInHours = Math.floor(diffInMinutes / 60); + if (diffInHours < 24) { + return `${diffInHours} ${diffInHours === 1 ? "hour" : "hours"} ago`; + } + + const diffInDays = Math.floor(diffInHours / 24); + return `${diffInDays} ${diffInDays === 1 ? "day" : "days"} ago`; +}; From 942650de9a82db77e936e02380d96d3ff69df927 Mon Sep 17 00:00:00 2001 From: Mika Date: Sun, 27 Oct 2024 22:28:13 +0100 Subject: [PATCH 2/2] responsivness --- src/App.css | 63 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/src/App.css b/src/App.css index 74c82807..ab59fd5e 100644 --- a/src/App.css +++ b/src/App.css @@ -10,14 +10,16 @@ body { font-family: "Roboto Mono", monospace; width: 100vw; margin: 0 auto; + overflow-x: hidden; } .thought-container { border: 1px solid black; padding: 20px; - margin: 50px; + margin: 20px auto; box-shadow: 7px 8px 0px rgba(0, 0, 0, 1); max-width: 450px; + width: 90%; } .input-form { @@ -30,12 +32,14 @@ form { display: flex; flex-direction: column; align-items: flex-start; + width: 100%; } input[type="text"] { - width: 400px; + width: 100%; padding: 20px; margin: 20px 0; + font-size: 16px; } ul, @@ -53,13 +57,16 @@ button { height: 40px; border-radius: 50%; margin-right: 10px; - font-size: 18px; + font-size: 16px; + cursor: pointer; } .submit { border-radius: 20px; - width: fit-content; - padding: 0 20px; + width: 100%; + max-width: 300px; + padding: 10px 20px; + margin: 10px 0; } /* button:hover { @@ -84,9 +91,9 @@ button { } .thought-message { - margin-bottom: 30px; - font-size: 20px; - font-family: "Roboto Mono", monospace; + margin-bottom: 20px; + font-size: 16px; + line-height: 1.4; word-wrap: break-word; } @@ -97,3 +104,43 @@ button { .error { color: red; } + + + +/* media queries for different screen sizes */ +@media (min-width: 768px) { + .thought-container { + margin: 30px auto; + } + + .submit { + width: auto; + } + + .thought-message { + font-size: 18px; + } +} + +@media (min-width: 1024px) { + .thought-container { + margin: 50px auto; + } + + .thought-message { + font-size: 20px; + } +} + +/* touch-friendly sizing for mobile */ +@media (max-width: 480px) { + button { + width: 44px; + height: 44px; + } + + .thought-footer { + flex-wrap: wrap; + gap: 10px; + } +}