From ecf16e4a1613d9521865d6c083e9d821e5255065 Mon Sep 17 00:00:00 2001 From: violacathrine Date: Wed, 30 Apr 2025 09:15:08 +0200 Subject: [PATCH 01/36] started with the project --- index.html | 2 +- package.json | 3 ++- src/App.jsx | 11 +++++++++-- src/GlobalStyles.jsx | 32 ++++++++++++++++++++++++++++++++ src/components/Form.jsx | 41 +++++++++++++++++++++++++++++++++++++++++ src/index.css | 3 --- src/main.jsx | 11 ++++------- 7 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 src/GlobalStyles.jsx create mode 100644 src/components/Form.jsx delete mode 100644 src/index.css diff --git a/index.html b/index.html index d4492e94..170a8c1d 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - Happy Thoughts + Happy Thoughts by Cathi
diff --git a/package.json b/package.json index 2f66d295..144c0a4b 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ }, "dependencies": { "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "styled-components": "^6.1.17" }, "devDependencies": { "@eslint/js": "^9.21.0", diff --git a/src/App.jsx b/src/App.jsx index 07f2cbdf..53dd2f65 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,12 @@ +import { Form } from "./components/Form" +import { GlobalStyles } from "./GlobalStyles"; + export const App = () => { return ( + <> +

Happy Thoughts

- ) -} +
+ + ); +}; diff --git a/src/GlobalStyles.jsx b/src/GlobalStyles.jsx new file mode 100644 index 00000000..b7d8df1b --- /dev/null +++ b/src/GlobalStyles.jsx @@ -0,0 +1,32 @@ +import { createGlobalStyle } from "styled-components"; + +export const GlobalStyles = createGlobalStyle` + + html { + scroll-behavior: smooth; + } + + html, body { + margin: 0; + padding: 0; + color: #000; + background-color: rgb(255, 228, 232); + + } + + h1 { + display: flex; + justify-content: center; + font-family: verdana; + margin: 0 0 16px; + font-size: 48px; + } + + p { + font-family: Hind; + font-weight: 400; + line-height: 1.6; + margin: 0 0 16px; + } + +`; diff --git a/src/components/Form.jsx b/src/components/Form.jsx new file mode 100644 index 00000000..d3d83516 --- /dev/null +++ b/src/components/Form.jsx @@ -0,0 +1,41 @@ +import { useState } from "react"; +import styled from "styled-components"; + +const StyledForm = styled.form` + display: flex; + justify-content: center; + background-color: #fff8ee; + width: 50%; + margin: 0 auto; +`; + +const StyledInput = styled.input` +border-radius: 10px; +padding: 10px; +margin: 10px; +`; + +const Button = styled.button` +color: blue; +`; + +export const Form = () => { + const [message, setMessage] = useState(""); + + const handleSubmit = (event) => { + event.preventDefault(); + setMessage(""); + }; + + return ( + + + + + ); +}; diff --git a/src/index.css b/src/index.css deleted file mode 100644 index f7c0aef5..00000000 --- a/src/index.css +++ /dev/null @@ -1,3 +0,0 @@ -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; -} diff --git a/src/main.jsx b/src/main.jsx index 1b8ffe9b..7dad2312 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,12 +1,9 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' - -import { App } from './App.jsx' - -import './index.css' +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; ReactDOM.createRoot(document.getElementById('root')).render( -) +); \ No newline at end of file From 1b2044fc5cf53dc4f479c3eac1a411c84b404e68 Mon Sep 17 00:00:00 2001 From: violacathrine Date: Thu, 1 May 2025 14:08:47 +0200 Subject: [PATCH 02/36] styling --- src/App.jsx | 8 ++-- src/GlobalStyles.jsx | 26 +++++----- src/components/Form.jsx | 88 ++++++++++++++++++++++++++-------- src/components/MessageCard.jsx | 55 +++++++++++++++++++++ 4 files changed, 140 insertions(+), 37 deletions(-) create mode 100644 src/components/MessageCard.jsx diff --git a/src/App.jsx b/src/App.jsx index 53dd2f65..848301b1 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,12 +1,12 @@ -import { Form } from "./components/Form" +import { Form } from "./components/Form"; import { GlobalStyles } from "./GlobalStyles"; export const App = () => { return ( <> - -

Happy Thoughts

- + +

Happy Thoughts

+ ); }; diff --git a/src/GlobalStyles.jsx b/src/GlobalStyles.jsx index b7d8df1b..5f73992e 100644 --- a/src/GlobalStyles.jsx +++ b/src/GlobalStyles.jsx @@ -1,25 +1,23 @@ import { createGlobalStyle } from "styled-components"; export const GlobalStyles = createGlobalStyle` +*, +*::before, +*::after { + box-sizing: border-box; +} - html { - scroll-behavior: smooth; - } - - html, body { - margin: 0; - padding: 0; - color: #000; - background-color: rgb(255, 228, 232); - - } +body { + background-color:rgba(224, 203, 200, 0.23); +} h1 { - display: flex; - justify-content: center; + display: flex; + justify-content: center; font-family: verdana; - margin: 0 0 16px; + margin: 100px 0 16px; font-size: 48px; + color:rgb(27, 159, 169); } p { diff --git a/src/components/Form.jsx b/src/components/Form.jsx index d3d83516..f37647c1 100644 --- a/src/components/Form.jsx +++ b/src/components/Form.jsx @@ -1,41 +1,91 @@ import { useState } from "react"; import styled from "styled-components"; +import { MessageCard } from "./MessageCard"; + +const FormWrapper = styled.section` + max-width: 500px; + margin: 0 auto; +`; const StyledForm = styled.form` display: flex; - justify-content: center; - background-color: #fff8ee; - width: 50%; - margin: 0 auto; + justify-content: flex-start; + flex-direction: column; + background-color: rgba(237, 220, 217, 0.65); + height: auto; + width: auto; + border: 2px solid #264143; + box-shadow: 7px 7px 0px #e99f4c; + border-radius: 5px; `; -const StyledInput = styled.input` -border-radius: 10px; -padding: 10px; -margin: 10px; +const StyledLabel = styled.label` + color: #264143; + font-weight: 900; + font-size: 20px; + margin: 10px 10px 0px; `; -const Button = styled.button` -color: blue; +const StyledTextarea = styled.textarea` + outline: none; + width: 100%; + border: 2px solid #264143; + margin-top: 10px; + font-size: 15px; + resize: none; +`; + +const StyledButton = styled.button` + padding: 8px; + margin: 10px; + font-size: 16px; + background: rgb(244, 150, 197); + font-weight: 600; + border: none; + border-radius: 50px; `; export const Form = () => { const [message, setMessage] = useState(""); + const [messages, setMessages] = useState([]); const handleSubmit = (event) => { event.preventDefault(); + + const newMessage = { + id: Date.now(), + text: message, + createdAt: new Date(), + }; + + setMessages([newMessage, ...messages]); setMessage(""); }; return ( - - - - + <> + + + + What's making you happy today? + setMessage(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault(); + handleSubmit(event); + } + }} + /> + + Send Happy Thought + + + + {messages.map((msg) => ( + + ))} + ); }; diff --git a/src/components/MessageCard.jsx b/src/components/MessageCard.jsx new file mode 100644 index 00000000..7c9e9c46 --- /dev/null +++ b/src/components/MessageCard.jsx @@ -0,0 +1,55 @@ +import styled from "styled-components"; + +const CardWrapper = styled.section` + max-width: 500px; + margin: 0 auto; +`; + +const Card = styled.div` + position: relative; + display: flex; + flex-direction: column; + background-color: rgba(237, 220, 217, 0.03); + height: auto; + width: auto; + border: 2px solid #264143; + border-radius: 5px; + box-shadow: 7px 7px 0px #e99f4c; + margin: 30px 0px 30px; + overflow: hidden; + word-break: break-word; +`; + +const MessageText = styled.p` + font-size: 18px; + align-self: start; + margin: 10px; +`; + +const Timestamp = styled.small` + font-size: 16px; + font-family: arial; + color: gray; + align-self: flex-end; + margin: 10px; +`; + +const getMinutesAgo = (date) => { + const now = new Date(); + const then = new Date(date); + const diffInMinutes = Math.floor((now - then) / 1000 / 60); + return diffInMinutes === 0 + ? "Just now" + : `${diffInMinutes} minute${diffInMinutes > 1 ? "s" : ""} ago`; +}; + +export const MessageCard = ({ message }) => { + return ( + + + {message.text} + {getMinutesAgo(message.createdAt)} + + + ); +}; From e8be3a1a762908725ab217e95648d2f7a1e34ab1 Mon Sep 17 00:00:00 2001 From: violacathrine Date: Mon, 5 May 2025 11:36:30 +0200 Subject: [PATCH 03/36] clean, styling etc --- src/GlobalStyles.jsx | 7 +-- src/components/Form.jsx | 80 +++++++++++++++------------------- src/components/Form.styles.js | 61 ++++++++++++++++++++++++++ src/components/MessageCard.jsx | 6 +-- 4 files changed, 102 insertions(+), 52 deletions(-) create mode 100644 src/components/Form.styles.js diff --git a/src/GlobalStyles.jsx b/src/GlobalStyles.jsx index 5f73992e..4646ae6b 100644 --- a/src/GlobalStyles.jsx +++ b/src/GlobalStyles.jsx @@ -9,6 +9,7 @@ export const GlobalStyles = createGlobalStyle` body { background-color:rgba(224, 203, 200, 0.23); + font-family: arial; } h1 { @@ -17,14 +18,14 @@ body { font-family: verdana; margin: 100px 0 16px; font-size: 48px; - color:rgb(27, 159, 169); + color: } p { - font-family: Hind; - font-weight: 400; + font-family: arial; line-height: 1.6; margin: 0 0 16px; + font-size: 16px; } `; diff --git a/src/components/Form.jsx b/src/components/Form.jsx index f37647c1..71336205 100644 --- a/src/components/Form.jsx +++ b/src/components/Form.jsx @@ -1,60 +1,44 @@ import { useState } from "react"; -import styled from "styled-components"; import { MessageCard } from "./MessageCard"; - -const FormWrapper = styled.section` - max-width: 500px; - margin: 0 auto; -`; - -const StyledForm = styled.form` - display: flex; - justify-content: flex-start; - flex-direction: column; - background-color: rgba(237, 220, 217, 0.65); - height: auto; - width: auto; - border: 2px solid #264143; - box-shadow: 7px 7px 0px #e99f4c; - border-radius: 5px; -`; - -const StyledLabel = styled.label` - color: #264143; - font-weight: 900; - font-size: 20px; - margin: 10px 10px 0px; -`; - -const StyledTextarea = styled.textarea` - outline: none; - width: 100%; - border: 2px solid #264143; - margin-top: 10px; - font-size: 15px; - resize: none; -`; - -const StyledButton = styled.button` - padding: 8px; - margin: 10px; - font-size: 16px; - background: rgb(244, 150, 197); - font-weight: 600; - border: none; - border-radius: 50px; -`; +import { + FormWrapper, + StyledForm, + StyledLabel, + StyledTextarea, + StyledInfoCharacterText, + StyledButton, + StyledErrorMessage, +} from "./Form.styles"; export const Form = () => { const [message, setMessage] = useState(""); const [messages, setMessages] = useState([]); + const maxLength = 140; + const isTooLong = message.length > maxLength; + + const [error, setError] = useState(""); + const handleSubmit = (event) => { event.preventDefault(); + if (message.length < 5) { + setError("Your message is too short."); + return; + } + if (message.length > maxLength) { + setError("Your message is too long."); + return; + } + + setError(""); + + const capitalizedMessage = + message.charAt(0).toUpperCase() + message.slice(1); + const newMessage = { id: Date.now(), - text: message, + text: capitalizedMessage, createdAt: new Date(), }; @@ -79,7 +63,11 @@ export const Form = () => { }} /> - Send Happy Thought + + {maxLength - message.length} characters remaining + + {error && {error}} + Send some đŸ©· diff --git a/src/components/Form.styles.js b/src/components/Form.styles.js new file mode 100644 index 00000000..f70cc98d --- /dev/null +++ b/src/components/Form.styles.js @@ -0,0 +1,61 @@ +import styled from "styled-components"; + +export const FormWrapper = styled.section` + max-width: 450px; + margin: 0 auto; +`; + +export const StyledForm = styled.form` + display: flex; + justify-content: flex-start; + flex-direction: column; + background-color: rgba(237, 220, 217, 0.65); + height: auto; + width: auto; + border: 2px solid #264143; + box-shadow: 7px 7px 0px #e99f4c; + border-radius: 5px; +`; + +export const StyledLabel = styled.label` + color: rgb(0, 0, 0); + font-size: 18px; + margin: 10px 10px 0px; +`; + +export const StyledTextarea = styled.textarea` + outline: none; + width: 100%; + border: 2px solid #264143; + margin-top: 10px; + font-size: 15px; + resize: none; +`; + +export const StyledInfoCharacterText = styled.p` + font-size: 14px; + margin: 0px 10px 0px; + color: ${(props) => (props.exceedsLimit ? "red" : "gray")}; +`; + +export const StyledButton = styled.button` + padding: 8px; + margin: 10px; + font-size: 16px; + background: rgb(244, 150, 197); + font-weight: 500; + border-radius: 50px; + cursor: pointer; + border: none; + + &:hover { + background: rgb(230, 130, 180); + } +`; + +export const StyledErrorMessage = styled.p` + color: red; + margin: 10px; + font-size: 14px; + font-weight: 500; +`; \ No newline at end of file diff --git a/src/components/MessageCard.jsx b/src/components/MessageCard.jsx index 7c9e9c46..11a0e6ac 100644 --- a/src/components/MessageCard.jsx +++ b/src/components/MessageCard.jsx @@ -1,7 +1,7 @@ import styled from "styled-components"; const CardWrapper = styled.section` - max-width: 500px; + max-width: 450px; margin: 0 auto; `; @@ -21,13 +21,13 @@ const Card = styled.div` `; const MessageText = styled.p` - font-size: 18px; align-self: start; + font-weight: 500; margin: 10px; `; const Timestamp = styled.small` - font-size: 16px; + font-size: 14px; font-family: arial; color: gray; align-self: flex-end; From 24d1bb59d52a7b1e3ec15558f358c069d809661a Mon Sep 17 00:00:00 2001 From: violacathrine Date: Sat, 10 May 2025 10:13:07 +0200 Subject: [PATCH 04/36] loader --- src/App.jsx | 2 ++ src/GlobalStyles.jsx | 6 ++-- src/components/Form.jsx | 12 +++---- src/components/Form.styles.js | 32 +++++++++--------- src/components/LikeButton.jsx | 48 +++++++++++++++++++++++++++ src/components/Loader.jsx | 20 ++++++++++++ src/components/MessageCard.jsx | 59 +++++++++++++++++++++++++++------- src/components/MessageList.jsx | 43 +++++++++++++++++++++++++ 8 files changed, 186 insertions(+), 36 deletions(-) create mode 100644 src/components/LikeButton.jsx create mode 100644 src/components/Loader.jsx create mode 100644 src/components/MessageList.jsx diff --git a/src/App.jsx b/src/App.jsx index 848301b1..c396f2c8 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,5 @@ import { Form } from "./components/Form"; +import { MessageList } from "./components/MessageList"; import { GlobalStyles } from "./GlobalStyles"; export const App = () => { @@ -7,6 +8,7 @@ export const App = () => {

Happy Thoughts

+ ); }; diff --git a/src/GlobalStyles.jsx b/src/GlobalStyles.jsx index 4646ae6b..e5583f67 100644 --- a/src/GlobalStyles.jsx +++ b/src/GlobalStyles.jsx @@ -9,7 +9,7 @@ export const GlobalStyles = createGlobalStyle` body { background-color:rgba(224, 203, 200, 0.23); - font-family: arial; + font-family: courier new; } h1 { @@ -22,10 +22,10 @@ body { } p { - font-family: arial; + font-family: courier new; line-height: 1.6; margin: 0 0 16px; - font-size: 16px; + font-size: 17px; } `; diff --git a/src/components/Form.jsx b/src/components/Form.jsx index 71336205..1f716538 100644 --- a/src/components/Form.jsx +++ b/src/components/Form.jsx @@ -26,6 +26,7 @@ export const Form = () => { setError("Your message is too short."); return; } + if (message.length > maxLength) { setError("Your message is too long."); return; @@ -63,17 +64,16 @@ export const Form = () => { }} /> - + + {maxLength - message.length} characters remaining + {error && {error}} - Send some đŸ©· + + ❀ Send Happy Thought ❀ - - {messages.map((msg) => ( - - ))} ); }; diff --git a/src/components/Form.styles.js b/src/components/Form.styles.js index f70cc98d..2e0dcfea 100644 --- a/src/components/Form.styles.js +++ b/src/components/Form.styles.js @@ -7,14 +7,12 @@ export const FormWrapper = styled.section` export const StyledForm = styled.form` display: flex; - justify-content: flex-start; flex-direction: column; - background-color: rgba(237, 220, 217, 0.65); - height: auto; - width: auto; - border: 2px solid #264143; - box-shadow: 7px 7px 0px #e99f4c; - border-radius: 5px; + background:rgb(237, 237, 237); + width: 100%; + border: 1px solid #ccc; + padding: 16px; + box-shadow: 7px 7px 0px rgb(0, 0, 0); `; export const StyledLabel = styled.label` @@ -26,30 +24,34 @@ export const StyledLabel = styled.label` export const StyledTextarea = styled.textarea` outline: none; width: 100%; - border: 2px solid #264143; + border: 1px solid #ccc; + padding: 8px; margin-top: 10px; font-size: 15px; + font-family: inherit; resize: none; `; export const StyledInfoCharacterText = styled.p` font-size: 14px; margin: 0px 10px 0px; - color: ${(props) => (props.exceedsLimit ? "red" : "gray")}; + color: ${(props) => (props.$exceedsLimit ? "red" : "gray")}; `; export const StyledButton = styled.button` - padding: 8px; - margin: 10px; + padding: 10px 16px; + margin-top: 16px; font-size: 16px; - background: rgb(244, 150, 197); + background:rgba(255, 94, 126, 0.72); font-weight: 500; - border-radius: 50px; + border-radius: 30px; cursor: pointer; border: none; + font-family: inherit; + color: black; &:hover { - background: rgb(230, 130, 180); + opacity: 0.9; } `; @@ -58,4 +60,4 @@ export const StyledErrorMessage = styled.p` margin: 10px; font-size: 14px; font-weight: 500; -`; \ No newline at end of file +`; diff --git a/src/components/LikeButton.jsx b/src/components/LikeButton.jsx new file mode 100644 index 00000000..e9636834 --- /dev/null +++ b/src/components/LikeButton.jsx @@ -0,0 +1,48 @@ +import styled from "styled-components"; + +const LikeContainer = styled.div` + display: flex; + align-items: center; + gap: 5px; +`; + +const HeartButton = styled.button` + background: transparent; + border: none; + cursor: pointer; + font-size: 20px; + padding: 0; + margin: 0; + + &:hover { + transform: scale(1.1); + } +`; + +const LikeCount = styled.span` + font-size: 14px; + color: #333; + white-space: nowrap; + font-family: arial; +`; + +export const LikeButton = ({ thoughtId, hearts, onLike }) => { + const handleLike = () => { + fetch( + `https://happy-thoughts-ux7hkzgmwa-uc.a.run.app/thoughts/${thoughtId}/like`, + { + method: "POST", + } + ) + .then((res) => res.json()) + .then(() => onLike()) + .catch((error) => console.error("Error liking thought:", error)); + }; + + return ( + + đŸ©· + x {hearts} + + ); +}; diff --git a/src/components/Loader.jsx b/src/components/Loader.jsx new file mode 100644 index 00000000..377ce0d7 --- /dev/null +++ b/src/components/Loader.jsx @@ -0,0 +1,20 @@ +import styled, { keyframes } from "styled-components"; + +const spin = keyframes` + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +`; + +const Spinner = styled.div` + border: 4px solid rgba(0, 0, 0, 0.1); + border-top: 4px solid rgba(255, 94, 126, 0.72); + border-radius: 50%; + width: 40px; + height: 40px; + animation: ${spin} 1s linear infinite; + margin: 60px auto; +`; + +export const Loader = () => { + return ; +}; diff --git a/src/components/MessageCard.jsx b/src/components/MessageCard.jsx index 11a0e6ac..8ebd3ece 100644 --- a/src/components/MessageCard.jsx +++ b/src/components/MessageCard.jsx @@ -1,4 +1,16 @@ -import styled from "styled-components"; +import styled, { keyframes } from "styled-components"; +import { LikeButton } from "./LikeButton"; + +const CardFadeIn = keyframes` + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +`; const CardWrapper = styled.section` max-width: 450px; @@ -6,24 +18,38 @@ const CardWrapper = styled.section` `; const Card = styled.div` - position: relative; display: flex; flex-direction: column; - background-color: rgba(237, 220, 217, 0.03); - height: auto; - width: auto; - border: 2px solid #264143; - border-radius: 5px; - box-shadow: 7px 7px 0px #e99f4c; + background-color: #f8f8f8; + width: 100%; + border: 1px solid #ccc; + padding: 16px; + box-shadow: 7px 7px 0px rgb(0, 0, 0); margin: 30px 0px 30px; overflow: hidden; word-break: break-word; + animation: ${CardFadeIn} 0.5s ease-out; + padding: 10px; +`; + +const TopRow = styled.div` + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 10px; +`; + +const BottomRow = styled.div` + display: flex; + justify-content: flex-end; + margin-top: 12px; `; const MessageText = styled.p` align-self: start; font-weight: 500; - margin: 10px; + margin: 0; + padding-right: 30px; // plats för hjĂ€rtat `; const Timestamp = styled.small` @@ -43,13 +69,22 @@ const getMinutesAgo = (date) => { : `${diffInMinutes} minute${diffInMinutes > 1 ? "s" : ""} ago`; }; -export const MessageCard = ({ message }) => { +export const MessageCard = ({ message, onLike }) => { return ( - {message.text} + + {message.message} + + + {getMinutesAgo(message.createdAt)} + ); -}; +}; \ No newline at end of file diff --git a/src/components/MessageList.jsx b/src/components/MessageList.jsx new file mode 100644 index 00000000..b628cd66 --- /dev/null +++ b/src/components/MessageList.jsx @@ -0,0 +1,43 @@ +import { useEffect, useState } from "react"; +import { MessageCard } from "./MessageCard"; +import { Loader } from "./Loader"; + +export const MessageList = () => { + const [messages, setMessages] = useState([]); + const [loading, setLoading] = useState(true); + + + const fetchMessages = () => { + setLoading(true); + fetch("https://happy-thoughts-ux7hkzgmwa-uc.a.run.app/thoughts") + .then((res) => res.json()) + .then((data) => { + setMessages(data); + setLoading(false); + }) + .catch((error) => { + console.error("Failed to fetch messages:", error); + setLoading(false); + }); + }; + + useEffect(() => { + fetchMessages(); + }, []); + + if (loading) { + return + } + + return ( + <> + {messages.map((msg) => ( + + ))} + + ); +}; From e24cb22329762ca6e68cd8704f4ce414cd62d494 Mon Sep 17 00:00:00 2001 From: violacathrine Date: Sat, 10 May 2025 10:43:37 +0200 Subject: [PATCH 05/36] hearticons, styling etc --- package.json | 1 + src/components/Form.jsx | 5 +++- src/components/Form.styles.js | 21 ++++++++------- src/components/HeartIcon.jsx | 13 ++++++++++ src/components/LikeButton.jsx | 30 +++++++++++++--------- src/components/MessageCard.jsx | 47 ++++++++++++++++++---------------- 6 files changed, 73 insertions(+), 44 deletions(-) create mode 100644 src/components/HeartIcon.jsx diff --git a/package.json b/package.json index 144c0a4b..fd5424ff 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0", + "react-icons": "^5.5.0", "styled-components": "^6.1.17" }, "devDependencies": { diff --git a/src/components/Form.jsx b/src/components/Form.jsx index 1f716538..b17533a3 100644 --- a/src/components/Form.jsx +++ b/src/components/Form.jsx @@ -1,5 +1,6 @@ import { useState } from "react"; import { MessageCard } from "./MessageCard"; +import { HeartIcon } from './HeartIcon'; import { FormWrapper, StyledForm, @@ -71,7 +72,9 @@ export const Form = () => { {error && {error}} - ❀ Send Happy Thought ❀ + + Send Happy Thought + diff --git a/src/components/Form.styles.js b/src/components/Form.styles.js index 2e0dcfea..7b52fc7e 100644 --- a/src/components/Form.styles.js +++ b/src/components/Form.styles.js @@ -8,7 +8,7 @@ export const FormWrapper = styled.section` export const StyledForm = styled.form` display: flex; flex-direction: column; - background:rgb(237, 237, 237); + background: rgb(237, 237, 237); width: 100%; border: 1px solid #ccc; padding: 16px; @@ -39,19 +39,22 @@ export const StyledInfoCharacterText = styled.p` `; export const StyledButton = styled.button` + display: flex; + font-family: courier new; + align-items: center; + justify-content: center; + gap: 0.5rem; padding: 10px 16px; - margin-top: 16px; - font-size: 16px; - background:rgba(255, 94, 126, 0.72); - font-weight: 500; - border-radius: 30px; - cursor: pointer; border: none; - font-family: inherit; + border-radius: 25px; + background-color: rgb(255, 166, 178); color: black; + font-size: 1rem; + cursor: pointer; + margin-top: 10px; &:hover { - opacity: 0.9; + background-color: pink; } `; diff --git a/src/components/HeartIcon.jsx b/src/components/HeartIcon.jsx new file mode 100644 index 00000000..18bd0e8c --- /dev/null +++ b/src/components/HeartIcon.jsx @@ -0,0 +1,13 @@ +import styled from 'styled-components'; +import { HiHeart } from 'react-icons/hi'; + +export const HeartIcon = styled(HiHeart)` + color: red; + font-size: 1rem; + transition: transform 0.3s ease; + + &:hover { + transform: scale(1.2); + } +`; + diff --git a/src/components/LikeButton.jsx b/src/components/LikeButton.jsx index e9636834..18e10567 100644 --- a/src/components/LikeButton.jsx +++ b/src/components/LikeButton.jsx @@ -1,29 +1,33 @@ import styled from "styled-components"; +import { HeartIcon } from "./HeartIcon"; const LikeContainer = styled.div` display: flex; align-items: center; - gap: 5px; + gap: 6px; `; -const HeartButton = styled.button` - background: transparent; +const HeartWrapper = styled.button` + display: flex; + align-items: center; + justify-content: center; + + width: 40px; + height: 40px; + border-radius: 50%; + background-color: #eaeaea; + border: none; cursor: pointer; - font-size: 20px; padding: 0; - margin: 0; + transition: transform 0.2s ease; - &:hover { - transform: scale(1.1); - } `; const LikeCount = styled.span` font-size: 14px; - color: #333; - white-space: nowrap; - font-family: arial; + color: #888; + font-family: Arial, sans-serif; `; export const LikeButton = ({ thoughtId, hearts, onLike }) => { @@ -41,7 +45,9 @@ export const LikeButton = ({ thoughtId, hearts, onLike }) => { return ( - đŸ©· + + + x {hearts} ); diff --git a/src/components/MessageCard.jsx b/src/components/MessageCard.jsx index 8ebd3ece..94d1c611 100644 --- a/src/components/MessageCard.jsx +++ b/src/components/MessageCard.jsx @@ -21,28 +21,31 @@ const Card = styled.div` display: flex; flex-direction: column; background-color: #f8f8f8; - width: 100%; border: 1px solid #ccc; padding: 16px; box-shadow: 7px 7px 0px rgb(0, 0, 0); - margin: 30px 0px 30px; - overflow: hidden; - word-break: break-word; + margin: 30px 0; animation: ${CardFadeIn} 0.5s ease-out; - padding: 10px; + word-break: break-word; `; -const TopRow = styled.div` +const BottomRow = styled.div` display: flex; justify-content: space-between; - align-items: flex-start; - gap: 10px; + align-items: center; + margin-top: 16px; `; -const BottomRow = styled.div` +const LeftPart = styled.div` display: flex; - justify-content: flex-end; - margin-top: 12px; + align-items: center; + gap: 8px; +`; + +const RightPart = styled.div` + font-size: 14px; + font-family: Arial, sans-serif; + color: gray; `; const MessageText = styled.p` @@ -73,18 +76,18 @@ export const MessageCard = ({ message, onLike }) => { return ( - - {message.message} - - - - {getMinutesAgo(message.createdAt)} + {message.message} + + + + + {getMinutesAgo(message.createdAt)} ); -}; \ No newline at end of file +}; From 487ac1e545fd184a13ea7ad48f55d024444466d4 Mon Sep 17 00:00:00 2001 From: violacathrine Date: Sat, 10 May 2025 20:38:00 +0200 Subject: [PATCH 06/36] env fil --- .gitignore | 1 + src/api/thoughts.js | 0 2 files changed, 1 insertion(+) create mode 100644 src/api/thoughts.js diff --git a/.gitignore b/.gitignore index b02a1ff7..b7f460f7 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ package-lock.json *.njsproj *.sln *.sw? +.env diff --git a/src/api/thoughts.js b/src/api/thoughts.js new file mode 100644 index 00000000..e69de29b From da8252e1d882611b480634cb35d20e5693a1e9f2 Mon Sep 17 00:00:00 2001 From: violacathrine Date: Sat, 10 May 2025 21:24:04 +0200 Subject: [PATCH 07/36] final touches --- .gitignore | 1 + index.html | 11 ++++-- src/App.jsx | 37 +++++++++++++++++- src/GlobalStyles.jsx | 7 ++-- src/api/thoughts.js | 33 ++++++++++++++++ src/components/Form.jsx | 71 ++++++++++++++-------------------- src/components/Form.styles.js | 9 +++-- src/components/LikeButton.jsx | 17 +------- src/components/MessageCard.jsx | 32 ++++++++++----- src/components/MessageList.jsx | 33 ++-------------- 10 files changed, 139 insertions(+), 112 deletions(-) diff --git a/.gitignore b/.gitignore index b7f460f7..97cff809 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ package-lock.json *.sln *.sw? .env +.env diff --git a/index.html b/index.html index 170a8c1d..7b35aaba 100644 --- a/index.html +++ b/index.html @@ -3,14 +3,17 @@ + + + Happy Thoughts by Cathi
- + diff --git a/src/App.jsx b/src/App.jsx index c396f2c8..24c58e39 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,14 +1,47 @@ +import { useEffect, useState } from "react"; +import { fetchThoughts, postThought, likeThought } from "./api/thoughts"; import { Form } from "./components/Form"; import { MessageList } from "./components/MessageList"; import { GlobalStyles } from "./GlobalStyles"; +import { Loader } from "./components/Loader"; export const App = () => { + const [messages, setMessages] = useState([]); + const [loading, setLoading] = useState(true); + + // Get all messages + const getMessages = () => { + setLoading(true); + fetchThoughts() + .then(setMessages) + .catch((error) => console.error("Fetch failed", error)) + .finally(() => setLoading(false)); + }; + + // Post new message + const handleNewMessage = (message) => { + postThought(message) + .then(getMessages) + .catch((error) => console.error("Post failed", error)); + }; + + // Like message + const handleLike = (id) => { + likeThought(id) + .then(getMessages) + .catch((error) => console.error("Like failed", error)); + }; + + useEffect(() => { + getMessages(); + }, []); + return ( <>

Happy Thoughts

- - + + ); }; diff --git a/src/GlobalStyles.jsx b/src/GlobalStyles.jsx index e5583f67..802cdbae 100644 --- a/src/GlobalStyles.jsx +++ b/src/GlobalStyles.jsx @@ -8,8 +8,10 @@ export const GlobalStyles = createGlobalStyle` } body { + display: flex; + justify-content: center; background-color:rgba(224, 203, 200, 0.23); - font-family: courier new; + font-family: roboto mono; } h1 { @@ -18,14 +20,11 @@ body { font-family: verdana; margin: 100px 0 16px; font-size: 48px; - color: } p { - font-family: courier new; line-height: 1.6; margin: 0 0 16px; font-size: 17px; } - `; diff --git a/src/api/thoughts.js b/src/api/thoughts.js index e69de29b..f5930f7d 100644 --- a/src/api/thoughts.js +++ b/src/api/thoughts.js @@ -0,0 +1,33 @@ +const BASE_URL = import.meta.env.VITE_API_URL; + +// GET Messages +export const fetchThoughts = () => { + return fetch(BASE_URL).then((res) => { + if (!res.ok) throw new Error("Failed to fetch thoughts"); + return res.json(); + }); +}; + +// POST messages +export const postThought = (message) => { + return fetch(BASE_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ message }), + }).then((res) => { + if (!res.ok) throw new Error("Failed to post message"); + return res.json(); + }); +}; + +// Like messages +export const likeThought = (id) => { + return fetch(`${BASE_URL}/${id}/like`, { + method: "POST", + }).then((res) => { + if (!res.ok) throw new Error("Failed to like message"); + return res.json(); + }); +}; diff --git a/src/components/Form.jsx b/src/components/Form.jsx index b17533a3..a624392b 100644 --- a/src/components/Form.jsx +++ b/src/components/Form.jsx @@ -1,6 +1,5 @@ import { useState } from "react"; -import { MessageCard } from "./MessageCard"; -import { HeartIcon } from './HeartIcon'; +import { HeartIcon } from "./HeartIcon"; import { FormWrapper, StyledForm, @@ -11,15 +10,12 @@ import { StyledErrorMessage, } from "./Form.styles"; -export const Form = () => { +export const Form = ({ onSubmitMessage }) => { const [message, setMessage] = useState(""); - const [messages, setMessages] = useState([]); - + const [error, setError] = useState(""); const maxLength = 140; const isTooLong = message.length > maxLength; - const [error, setError] = useState(""); - const handleSubmit = (event) => { event.preventDefault(); @@ -35,48 +31,37 @@ export const Form = () => { setError(""); - const capitalizedMessage = - message.charAt(0).toUpperCase() + message.slice(1); - - const newMessage = { - id: Date.now(), - text: capitalizedMessage, - createdAt: new Date(), - }; - - setMessages([newMessage, ...messages]); + onSubmitMessage(message); setMessage(""); }; return ( - <> - - - - What's making you happy today? - setMessage(event.target.value)} - onKeyDown={(event) => { - if (event.key === "Enter" && !event.shiftKey) { - event.preventDefault(); - handleSubmit(event); - } - }} - /> - + + + + What's making you happy today? + setMessage(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSubmit(e); + } + }} + /> + - - {maxLength - message.length} characters remaining - + + {maxLength - message.length} characters remaining + - {error && {error}} + {error && {error}} - - Send Happy Thought - - - - + + Send Happy Thought + + + ); }; diff --git a/src/components/Form.styles.js b/src/components/Form.styles.js index 7b52fc7e..60944fea 100644 --- a/src/components/Form.styles.js +++ b/src/components/Form.styles.js @@ -2,7 +2,7 @@ import styled from "styled-components"; export const FormWrapper = styled.section` max-width: 450px; - margin: 0 auto; + margin: 5px; `; export const StyledForm = styled.form` @@ -40,16 +40,17 @@ export const StyledInfoCharacterText = styled.p` export const StyledButton = styled.button` display: flex; - font-family: courier new; + width: 250px; + font-family: inherit; align-items: center; justify-content: center; - gap: 0.5rem; + gap: 6px; padding: 10px 16px; border: none; border-radius: 25px; background-color: rgb(255, 166, 178); color: black; - font-size: 1rem; + font-size: 16px; cursor: pointer; margin-top: 10px; diff --git a/src/components/LikeButton.jsx b/src/components/LikeButton.jsx index 18e10567..1d081789 100644 --- a/src/components/LikeButton.jsx +++ b/src/components/LikeButton.jsx @@ -21,7 +21,6 @@ const HeartWrapper = styled.button` cursor: pointer; padding: 0; transition: transform 0.2s ease; - `; const LikeCount = styled.span` @@ -30,22 +29,10 @@ const LikeCount = styled.span` font-family: Arial, sans-serif; `; -export const LikeButton = ({ thoughtId, hearts, onLike }) => { - const handleLike = () => { - fetch( - `https://happy-thoughts-ux7hkzgmwa-uc.a.run.app/thoughts/${thoughtId}/like`, - { - method: "POST", - } - ) - .then((res) => res.json()) - .then(() => onLike()) - .catch((error) => console.error("Error liking thought:", error)); - }; - +export const LikeButton = ({ hearts, onClick }) => { return ( - + x {hearts} diff --git a/src/components/MessageCard.jsx b/src/components/MessageCard.jsx index 94d1c611..db154b05 100644 --- a/src/components/MessageCard.jsx +++ b/src/components/MessageCard.jsx @@ -14,13 +14,14 @@ const CardFadeIn = keyframes` const CardWrapper = styled.section` max-width: 450px; - margin: 0 auto; + margin: 5px; `; const Card = styled.div` display: flex; flex-direction: column; - background-color: #f8f8f8; + background: rgb(255, 255, 255); + width: 100%; border: 1px solid #ccc; padding: 16px; box-shadow: 7px 7px 0px rgb(0, 0, 0); @@ -63,13 +64,25 @@ const Timestamp = styled.small` margin: 10px; `; -const getMinutesAgo = (date) => { +const getTimeAgo = (date) => { const now = new Date(); const then = new Date(date); - const diffInMinutes = Math.floor((now - then) / 1000 / 60); - return diffInMinutes === 0 - ? "Just now" - : `${diffInMinutes} minute${diffInMinutes > 1 ? "s" : ""} ago`; + const diffInSeconds = Math.floor((now - then) / 1000); + const diffInMinutes = Math.floor(diffInSeconds / 60); + const diffInHours = Math.floor(diffInMinutes / 60); + const diffInDays = Math.floor(diffInHours / 24); + + if (diffInSeconds < 60) { + return "Just now"; + } else if (diffInMinutes < 60) { + return `${diffInMinutes} minute${diffInMinutes > 1 ? "s" : ""} ago`; + } else if (diffInHours < 24) { + return `${diffInHours} hour${diffInHours > 1 ? "s" : ""} ago`; + } else if (diffInDays === 1) { + return "Yesterday"; + } else { + return `${diffInDays} days ago`; + } }; export const MessageCard = ({ message, onLike }) => { @@ -80,12 +93,11 @@ export const MessageCard = ({ message, onLike }) => { onLike(message._id)} /> - {getMinutesAgo(message.createdAt)} + {getTimeAgo(message.createdAt)} diff --git a/src/components/MessageList.jsx b/src/components/MessageList.jsx index b628cd66..e0998b83 100644 --- a/src/components/MessageList.jsx +++ b/src/components/MessageList.jsx @@ -1,42 +1,15 @@ -import { useEffect, useState } from "react"; import { MessageCard } from "./MessageCard"; import { Loader } from "./Loader"; -export const MessageList = () => { - const [messages, setMessages] = useState([]); - const [loading, setLoading] = useState(true); - - - const fetchMessages = () => { - setLoading(true); - fetch("https://happy-thoughts-ux7hkzgmwa-uc.a.run.app/thoughts") - .then((res) => res.json()) - .then((data) => { - setMessages(data); - setLoading(false); - }) - .catch((error) => { - console.error("Failed to fetch messages:", error); - setLoading(false); - }); - }; - - useEffect(() => { - fetchMessages(); - }, []); - +export const MessageList = ({ messages = [], loading, onLike }) => { if (loading) { - return + return ; } return ( <> {messages.map((msg) => ( - + ))} ); From ee8c95c4a414383273fde503b80faa9046aed624 Mon Sep 17 00:00:00 2001 From: violacathrine Date: Sun, 11 May 2025 20:43:20 +0200 Subject: [PATCH 08/36] favicon, logo, footer --- index.html | 2 +- public/happy-thoughts.png | Bin 0 -> 20784 bytes public/vite.svg | 1 - src/App.jsx | 64 +++++++++++++++++++++++++--------- src/GlobalStyles.jsx | 3 +- src/api/thoughts.js | 21 +++++------ src/components/Footer.jsx | 34 ++++++++++++++++++ src/components/Form.jsx | 2 +- src/components/Form.styles.js | 2 +- src/components/LikeButton.jsx | 2 +- src/components/Logo.jsx | 11 ++++++ 11 files changed, 108 insertions(+), 34 deletions(-) create mode 100644 public/happy-thoughts.png delete mode 100644 public/vite.svg create mode 100644 src/components/Footer.jsx create mode 100644 src/components/Logo.jsx diff --git a/index.html b/index.html index 7b35aaba..ccbb3216 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + w>G)jm_2@<2j zAuzi0d-;DpkD~9r_uO;OJ$=vdwVsY94V)DY007NhEwljuK*7I40SYqk$FBeIG5CYj zOZBc11^6qN!VU}mo6${QUfGI=VS~ z**@{Gzv=Gjkh-qG3IN=|U9^gkf7;qqzz1{VkII`HLGCx21neNH+;{!PU@$Xv^&CN4 z%u8=ox-7Cc>jF2ix$$!GJ6`!+gU<)_pL@ia$#G+A=$C29+F;)!jrY~V`4dBQzcv&# z6s;?6Y$RxNzx4iEbn-C$vGGj*&f?u63!fGI-2+AUbgy+;C~pz7oS&PkyVWj{_phtX zs`87R0h{$z@8_8n~W2wo+m8$wpT%!~ViaP8$7-Ka>9!3~uG+`B42J=~M6T3%7c|2?BAtjO;$<5tPxqp?U zU$7O)y+8fsE0E*a1;q${C;J;5T;y($>8GwlkNh|fAn+ff+ad%c$r4Tm9hvxt<7bb2Y{|AoOg1y`)QUZFkiV)gYYjm$ zK${-y^vARwy~pwqG%#}bfOc(2UYy$|I_z)5_Drrk(?(hPYn)Lzz+GYI=e0V9EwFm6 zs>I-97fV$5qyc%rQaM2(=M-Oj_G;2Y&1u;IF=z=Q5MAuxd(ZIx;!IBTZ+65vHCZe9 z{hkjkdAI7G_C{aRo#<0Dg8Ogr2t!>R3w75MS8Yqo5s)HRf{;lO3jS5jcGo#~-LzuN zmorD)7J@QD{rH=}K`C~YTi5Na`Mo;5;EkP8vQWn_pNtd7f0iGr(xU`jOaXc%YD4|Y zb(5by5h1kNf;Kjz47RthJ19fm4T2IG{icbU#?HwV?;i_T#I zV^PZ@tOn}wqm>gsS>;%&62U(z@(k2gb2Qj#y&(L2*t<1G5;QUr1`LWXSM6(g_8xB- z%T7EJ!tcEMT(z?QcB7xm$3e4}D2nd*4qmaY;)5Nrk}HG){iMN~N<9xU2C+PgbcwHC zTlZ=z>5(K~Hc z{OCanp96@`To(L6QGKD?(VNdM4m|M7okDgkhPKb^MEuZbCs&8;&ATiXt0%|t6D8N! zz$vUkIuLq@9_sjM^|a~Lq@eMm-`4Z-_oLO*j1|2eIgItG-Zo%|WJMQv5&qEbs&kci zrqzNUKdB(K1Q|!fth}p1>V4~gVuT;fblfQTZrewKP-!BH8U-F3TlLh*1bGwYIlx0N z#TWuc<$(@0mX;iToc09WZ^kz~?^lbJx@0P4=4u*wngV+2+PzLkEfcuY*ptKGTIw*L zT<6K_zE(rN+29^WvkZVSgR8sWapt1~RP}5+NZ0_0qKWIC#AQ5V$c!&zj(Hi76`lRN zpv!PXQwhP8W-U*ZavheqxfiwPCwl%81fuo?f4Pwi?lCttxE;;^ ztMQ`epr!(ZgPIB^KnIc9`(j zP+QJIv*wqNwG9Q!fvk#@|0-umO@1wuWcBzm7MY)sB^ZH3syGnh+&Y9!|uCDxJjz+KynVzcJO+mKigbwC=L zBYAn*wk1m6`ZC+|JD=_-_P3wq(DctcWqdZ)5G94qg@uFakyb z-j9O#jYGauHTGw=m*}M33wm08UJEW11#qI&u@*sA=4oW$Tu*&McgudC<4GNn-Dn)V zksqt6BY`0)-3=}T?w%A1z1-uojPV<{KsM^>XaI0|d$+62Ev|!X=O>xMZ8dl8R^MYy zAF<#z@aPJSE(Slq3zcu`X;X`Dtxk`8RyWkzXIgPgVHTivNgUBMXx5uyuMXsbZOL?h zU#7MNHFI+DN<+UdZIJ@Vb8m8qM4jV8pHhzw<&AM|TqRK0}rz_XxdFeKZAz8Nfv_+-_jYhP-h~)FW?l@_sL8YHwt<8GOLfwZ+Ne zrP);i4gSc$3m$%vKlPDYp>LW9cs}?W;i}Ptf`n$Tf2DN0N7MO71eJ6F6OVZ8Z8W;8 z4rK)QW;qDI-?>Z+d+3m*3wRQ6OEDXBI->3f7p(sdyXVt&LRBe_vPe&*LV$XnqIu(?J@30 zn#7jsj1l{2s7v=d1jT1dmpMvVDd*?x|xqGG=5#gHhBH>s=1}Vu6jYEQ8kJ( zT=jacsZ*66fu$8CA>L&8&w|%@ciGwL?BN%_%xZ9sXY#`-iL9sN z@#%)m!~Rw$7^1@NQ&B*0_ev!p^C8WFzRdPNpna_@LZvc!3?sixA17liPLq!OR;no6ev+ySnM zT#iywqK~nXT>ryWd}(n&>DB`o%JIwdbahZz^`9TYi6)c!em(jZf<$5lQ6U8b10v&Z zn(8gM-;y_nNT2c)iL*pZL%L4HN68sDbxHCD{>`k2JtgQGeaxXbw@V)VMRaF()}KWy;*xZGhYTeBtGLjwS{4lO~P@AMQ$3 zPr1q8lbxpkA~h~m-|+V(O1DEz^a-1u#ZuSmwAn%BhFeYgC-cp8#z+nRzmq+EnONF9 zZ96B06o32C;y*jC4njUu(lTwRaOVr^=Tf z*HpfhV+3|tVR^NMOBR@0icrPJ+}F{h3l=JAGlFOaM!0(W?4RSMn9{QJPj9!rTDEyl zHx^A#t=pH_yyKG*5s+*{=ujc_9hYw%7l6}dT;(+jZ6JD2YIHL%-kGKZ^TI!JdgZCq z(Z0lLo3^anOZB@S=)4d@BmsA3Wdx8}1%*pfCEYC=Jil|GZpA8Sd;8Yqujx@=yL8R# z#2w}D2|a=6<@-w4axVDS-Ms0kVx;FFPlwRw0htG1Wjj>PPT9>tg^|nw^}zOPA1)vE zv-{>AqV9!!Ioj>K+{#JWT&hoMz5X<+<;(|dLdCd>yJAyoyhT&@5j5%>OIO^&t8cja3aAL&~(lsQ8??XIX zLu+GdYUM+8n~uLY$&7x5j{0))<``rZO%&ui4sU<*o9MfuAquFLI6S=PTrD!H$=#Te zfECnb8Szd?{V3PG)45ku`I2S>N>OOH(-qI4?B$oz{R1?eh?F9nR`H(%rQ%lpX_ie5 zb6HN+rpiYXwxNHKAVZ@0Os+|mmlhJ=*Lc08DF9UMGH4wNLmQt$p%k9MAn zUJrYGWBca+K$xNF6a4GQTUkI9A)|n4RYU#Whu&9oFW@2$z$s!VbXTyT<*`akjU^;*wK2CNrVJzPPwM;WkYXtY@T+D?~ z7RLvu=VACAYR`)_79%dtZ7gX#GS#UJI_(^~V|_MMOc2H&@qb~t}n19y2U1ft0-g>L~Ql+siAC>l`!o)_l3 z$}sxAMT2zY61l-y-o(<0K5G&x40J$bg4Qp*fRFdi|3=!3WhLDX);=pgk=HCQ2PjPY zV_^$T4CjnU+LNn@l;{uth1I5KkC_6bTB)hFTeu55wD^QCBQI}{MG9ZkhMUtDoZ=&J zRB$OFP+UNA(3ch*vLZd?!m~ubhM`bz9^9CEo>YtPQ$eG@84|nkHFvB6@`&sm?NDx@ zsge*5ERn{a*%(xu!m}-OL2FCt|R>W45;{J=;_UDh_S> zP2hU;=`e*)!RR#>$ByP0-e}Gvs^Vp%!(puyXy>#j-dPQ0Ga@Z9v|6y2^>13N)#_IkW2AgXxuzK-X0NnJBQGxAWK7 zxIW1(++!MKjQi^9rPb-#1E#>D(D=eMBV^@&YuaDGZv5u^O4SyU7&SfqWuuVM==+Ku zZ;m!2vT*#l{_2k!FNP~r7XTc??seTQuG+Z3oLejLAKIWQ;>lXP{THWaS<6WZ^gIgcK?CE+wTD6gYuPo z+jUwUX*3T&u3Cg%AMeu>Y%Cyh4^`r&G+|NWmv`kWh+2;Frx!~SMzb*bC>s~f0$<+WY`&8 zsx)z6|8H&AmB2#F2#^m*SMEhr>zg^slOqt3spP+?na$=*u0N)$UwW5bv$GfhYlTqv zx&|ZKZ zLiFCb;aCA#9bIcEZ>#k9(N)Bt2bwfVA3^x9O8KkbXz`I%SvtXb>^D)Zpjz|vxpVSO zey2+jN@)(wW&7RSr;z9TBe$k(TX)YV7U}dhPO_d@wkNUZ;B7T)c&h-%$^8N zfVC>|z5s($6v%2Fh@?Qxrv;syO+F64Pj_4h<6sC8w7is=hWi)3*B*udua`a-mHA() zX1e5&wxheWRHX;0b%LGnk^LonBdvHKm!0tKO!rHW&&gU%Zt>IdqxmOSkd77U;aIG& zTJ>zVerplO``(kpFVv!7enz_Xjwjv<1e=fLmXo_uAK+m4^T4H1i3$KZNBjSWRZ17c z1G`}UTmJJ>X#(hJ3^#z>3wU5+TpYm5>oF$CxrhCf+8BB8z$?a=G^9f;=r@ci=V=3r zhbc7zkz_A4GHp?kCYkLuxT4zfu){C#^_OYn+9`M+)bqT1w!tBx_}Bmk#^Y+?<^?Yv zld(%mRGHl8R}x-1cig$QGh^00`UJx53Gy8pjXsHv20hdDGx`4>OufI+<<*UvF zh)!Ll}AbIWO6NIeZcR${tf@zOs()rA)E0vU1j_vJ5|xI=$n8!d9o{b zY~CLAxCQo5qx|#mtclNKP!z}nyuy2z%!*iYdb^rdz3}GU2KnH_4o2Y2)>6^%r996S z(5+xWK36Iac!<~3n9u=-plJ^BBnX6lvpHbPE;^-9V85a@Q3!%lhkIIG0|&CzVM&j; z4*pT}?Sh{H;jW2zkUF=`t4nS+&<2>^MrXr6xSdEfktr+?EoePk=q|6a4NH6z(f-KSuurv58h-blKu`i4W+6#a3 z-}V1JK8zLx%oh^MFPTkH3z}uQ*dpC)s+at8L8JoE>jHB^mjln9C;3lqmGm!_gJ5I; zcL+akPK6OcX2&Y+d`@3Yv4ocj$i4Evxj1?oTHXbl5oP9;=Sem%DS2jmfg*a|R+a+5 zp^4*J8Y#OZTx*x)aV<$$2{T=cp)Lds93T-MJky+yV-$q)Pp-^pUM4?kh61L zl^2Af8bbpZ&TDU*<`!dcpv^(MYlLDxxS@|Z*DM_ma*VPIpi+X~SK5x_QntJ8u}C^Q zoQx593i)aBG_2fK&?271JW#^p2MiV5LNiXwLUa2*v@cA$Y{l4mPB;AhL;R1sNIv2D zKj=m-=rhhW38X9xnDHRRz?~=L!}r5Ny$CHWE5fsC4_cW@AUR00g!j$<)Q3X_n6R|1 zzkOk0hSy0h_5x-TNaYZ9=zy1QHf~KY>VX)Ttv&d3b1y;!mJG>ovqA z^xZHQ5T-`VCht_{%GO6~gzHvAuWzFagp2F6s^kn0KD8-XCBaan6&=20!iMF7n_D6f zKW61ezo$Q1h@n0$q&olv>V>IbeyBOVU7t(1PWZW!#27)i0-%p`sY(y;877}5iREgO zy88YX^-U_|wEcVPi~6WI@@X#}4>K%nR%cRq>eF83S!pMAMxfmEX)=gp{c0*AU_&BH|UKZCITf#Lxd2!y!DY-3$+w3GQ%YLSi zF(rb*VRCq{9HcuRT=;{i0JCiTt`1p}R})~rbwsm#ab+)#!-`nv|ES~uMh$(&Ct<~) zBrI4L{7=$5=$RZ%rxe`Vo4GzuLQ&;^F0EaT4Hj!{)OmIaeguUwPRaIv(prwj$ex@x zd4h7p2~r3Y7tj48ky~88sl?wX9R`dA{$7&N;ovpMMRpy^ykP z^UrFh0<89A@hrI5-F@&}x+tRFlf7#i)32PwiS$c?!Y}0PlmH_SNMc=H^_PhxP=l-3 z^N-WlZ(=)F}9>!_V+t@eP6Vo!r)#&k?`W-jdi>{T=_SJ6vBA!En#U7vFT}MT`p}3%c?22CU~cb(t79N?AmYOhpqYZDUI2$8zh{Bi^9WWBlX`TBEmU< z*6r#N+;h;$_R@>gDhMa%BGFNxWe(~dNYMR+Sq+0K+g)zKZsD{F3W zaW1)8eL-^UUrRn-j+fs29+0(0g=t<(yGYCCy33Gt4w*e~vD<(@OADU8H50;h_?xfw z;+*0T|FffCtIezda`-d1^}}H6cS?*GW)Q>CfO-6mB*qPLd=itH^elm+8IqOoZtqc@ zo#AS&|7R{Uf=nx@erS?0Y8v+LOp_HiyXm2W!^aTJpja7pr;{7 z=`)^H$s*ItEyyz6&l*6h*U3ze*pmRWKe=`VG(w+7nOzR3vxbb{83GMYtIYM-k{)yX za4@C?txCY^7L6plRmEx?ypjY_nVgAP9ut0>OZP?p!`f!=^ly#HiV1QQ9?u~?r_6|8 zx9&iKLrdD9+r!REZF`CUEv4%9(dU}+WtT=*(B*ih&daCR1&%=*{v5gU;^2R7I)ldD zolba`DC3X4ez){15six!G8?*}ZRgSr_c=*dFk|EcXg!qW!$3hJ!Cy=%>yg1w6@^Qy z5=lb;Go+EmLyZAfi{4~F!Nl=#-6PFRzn-i_9#DKpb&;!*=c!7TLmUqGd*PR^4?1F1 zv9%!#_$2S1@nPx@D-*l(rCX?PDuhwG__cF^daVOl-u<_Ae=kWs_l47CgokYxE+l&_ zNiGVUzF}9=eaKOtn63<(%eKA1hz!(6!FmLy`J-!;iF$e4(&Xhp)=<%y2c{z|$VIeX zXV~Z0jY}^EOY3&P?R634B2*Y(6{hjnb~5PzlseGad&>_su447kit zDjbP!juwO)P57$jpopBo>s$bLQR&tr>`%U1ZGaCufqV$-;{!%MkwK5X`>TV1~33 z{MdJUeE5$Aan=S*jH+H^gei@XH7-<5a<=%$Z|<>SOh%ccOqlPdQwMK7vR>nfEYOH|_sjQImuII}XN|KaQQC0^mODyTed^P$+i%`b13^{yIUZCSy- z8xDl!ypZ2WTO06g?u;d44CX&nc(VQDsCD`HG*gJAdNK5`*britfRPXl!`vmbclJEP!X@b zIbSZ~zD^ke8NlDZ2tg#VU*EzCe}CQeH0rbF{FDPM`I<`~$K2?h=TPyK>#>H865Y?2 zxd00;s;e zI@I*)V+)S^@=pvbV0934NgMn94ORPl5SJa#jZ?^nG0Z8FA^Ry zy8QGq9OZK2fM>J%o1Pw>e(v;svWG9auAc>N(QVQ!kz$GuLolPA{~q#+$v88HGa zi>VR$?jEVJH3F#@mZ5e50J#TiXSUC%FpCevz-O)tG&zG2L_E}7ocOZi$wGu<6d#xl zZHD5>lvn)p=9im-F4!laC`|n&^&jRafAEc0;Qip+ZZkjER+X$p_M28e++`!tuJO02j-B`PACW`H;2EkX^d!(F+L0?m<^c z<6QO87d_Cu{^c5nNK%Qv!^i6~#(rOZ$+sm)&#M18uB}Rqq3osmzyjUIuy$mn%}h&F z1+W=xZZA)wk8_=(wUgv?cNqFUNBqd85fw-Elm^91xmJ?Jmt1I<7i)tTmk$FzKKp*L z)XYU;524p@YV{?{(Yr-9#@u=vL*el-UXwCvY8(>6p%Pa@4-?F7W$cf#8H2S>Y2Ijg ze)7LXvvRY>v&5Tb}hqAZU%jBLO7VUY$|z`=2Vizr%5EHXK}7 zFT*i*3R_AU-|xPSaDWN}XiPpG+3$BZylztGbJ$`W%m6)^HR9=SzLgUQ#4XdK|H11s zWzMfqREN-(2tq$;ZIY&VhT;C>4JEb@9;V{1$j9IKXL}7J9G!nXT8E&r%W8LY+GZ!@&Rko~-04c1Z2bG>F}3VW_a7IvodxZ~-VwdRfU zX5l&x#@DZSI6Me+vm+*ftncXde}FJp)W2;W$Z{8<4lb_M)o6^8Tnv-RWH;Py)aQo= zuiK(5Ts>ULjyAO37C>^x-fn4pow%Se+G;wO9$lx|KhisUTY47tryS_~bf9PMO91znRKGG)oz25CS2Fu)`__hjc+Ibr7Q#xHupF%H zm-N{B$+wvq_kqr(Rgm%P3D$I!#t>lUHWU?xQR7A<=lU8T?gL59b00%^1_IUnHK6R| zrkjY_oSg_)=@hc{;|FD~S7>;0qvKe(LP9tBrg^{WoNPv9b21e{Q5-CEc617}{D3}Z z&4y?%`~L0$B(s?n7fn9ntK?J^E*1=}n{AxMHy_X`oJK_#^$SXoHH>Z0#L8Wa`&)Ij z>LpNt$fT49UC6Zh0O+HC=8!xCHRYRD8**3c)-NE*3DnI*bM`49c;_b=xRK8I@={Kg zOCI{ZrfrR~i6Pjg6239GWu0`G1QvKNZnx@9d0l*5&`qbAb>U_!mRl1^dfm=Ho;lUC z@9Iw3Ow{*jJ?Z~-em)t4QO>cw>v=T+;&l^R@{v4Ncp$0cXK34(utLE%0{Ei@@5eLQ z%-xN@*XxxELfZKvoyJPG!*6Pl zLZrNi*`^KBn@!Bj(gKJ32aoo&eGfBn7zLOwi(z8@`Pu7bv4r&%)1=h}HDdrMJvhpM z3K@U@Rsp=mF&rvwCO^jPhuxffnNi?@7L?^F*L|Ze|~Wa!wc^tv;EiO4>wjQGid{jS45yvwc=1 zmGIz_KTj4gBB|jCy^xGhPXGDwHT|#7f1cKkaDZkmE;&3ofm}yX_9UnrexB(E5&Lqp z&so3YE^~*@4C&Xxem$eb83CV}I;Bj)%g?}=?htb<&A6O;&YR83CD8q}3q$_502HLX zJ%a%s@9)2Dk4p`oM*F&|3a6+4F8xBbvV!5e4rsE|qHmbL$OvYvJ2UABvk(qCD;oRv z@x_;QWvnLaLWpwj!CmkKyKzB0{s7unP`csmQvsX6`s&Ae#X4z{jin zGkM(;M>jqOv}_N~Ce=Mrcy{PKLJPZ9`lhe1k!kTIk+_$7z85H~sd-xg?+3j&;p4u# zNv85iI0agA!dWJQ*@L4`Hi5_K%IM9)R}F4~p)&n6A&|sP$Tz3-XZ{&yY@0+bdreZ| zj~fm#*-Ww)Dd_j$e+2TZDc0v{)%jAX!6xS#$>#05|K$po_$*-TWbq-$vo;wLr^7GLhe? zlN6O#=R9KIv8bl$cawcE_#qAsAg2Pe5(*2ACsMK&3iHtOU5>-&ah+#ZH+vyP{mLQ~o&%`{8@O zDmA8~itH5HOb4JLw_lAZ$hJ;-enS?;VZJBbAU_#;)5-mbc7Box`)u);yuxd-|D2<4 z+aT*}k8jT0J^t`(wNI%4UMh2;Nj9T!rDWCMK(&K*yF}tEP#MlOFV4?Wz4bj-5IOnbj0zKT2y9}uQC+%kKJXQKV8;Z~=*XZa{K zb?OdDbqGLv{BUs-8{u@_sFDf2H21a^_+kQFy*{QRS!FWn;Jy{KzEP7*{{)3hl?@3^!zdw!&&> z3U4kax7zAL95Ud&jaf1UaN3lAM@*Gn>=%3gdhi2SRk2x#e+3pJ|035;!$FV6pXWU} zaW`bR4&lL$!J&-8qqzFdSwZjkzzkG!C(KKyLs zB@pNFzQz1yDL}7~^z#8NSu1USSGGC5mXSZ+7x0@*4G5oLIN_gJWyp1yeIBACslW_72M=%Md!SdyTu zFrPC)IrUz}h$ROx`$5Bc==)lG8{fex>n&C+XP$Vs9fsI|0riFtTR2T>c=c-d`#*NI zo7aRTGN7v(zPtNAG*cWGtcxM%YLxc<^?`pITU4H#-%N-@iA(%_<*doa3pFRl6+xYx zE5i0exCb+*!Z0xhVBRMSSk_-2F>H8$#=Cr9jdF$o0GLk^ZyrNtRm#8ZNMsm7&~x*t zYh;SAQ3p49;MZ8D*$xQon*S9{j&GR{1b8Bbr2DqJf;p!g7$`Fswb1Bms+iPyY`W}H z-=~LiVcdCqfZO-ewu7D2^s)6LY?nE@UG_5=$=Toc+6eNc^mw4=*L5SZH#tTx9=P9Z zJN^^cmh1TmjJ=WQc&rG{tJ1vr`4Q>Y_8ne#8Z`plOK(yDKzr90rM>O<0z=2XJpjz} z*r_$1P39F{4At3;$944l4;X+sth1HrU%eI0nJE$rK1cbJ2?4Q!p-`slIy7b=q+O}j zric9>YT+o`=KiZdl%|CdtWx4Kx2v+p(jm36R|%l^r0EG(tY6=skJuDPz=;f~citq& zg~6GhfExdC+X!1xD43~gwPf>SR48|-yWmU(2{IuB0QA=59+}PzV`&nb{?8~72;>Ke zuV(6hrPBU*91pK=`kkY?3jo0SSL|m_{ce_TA)!X3S9x#xdvJqOReP3eik4!)JG40aapgQ8No_Lu*3ovyE+MRKEp?kDxXD zOHKL{8C5GFZ5S+r)1QVNMgl(kU>L68#cfsMi0nu35D>iHg?zVb?xzUQ zR3sC?q9HID4%tF#rJjQsgT>GMaLlCvunb>PsY=oB(CAl-RtFFW7=p(e;U@irf$bLOP#OY!6;ZI;Uy~Af z@%&xx;+H(IG?xA_lSG`}yMYBJWv{UNTpNSMdcGas>C{J7`z9}zE`o@!r#>V(bmN(j z0&0%P9xHI2CngAh4}k9_f~IeSPi8qnl06^*mF_a1U(%r!h>T%NV;r}XTP z_RD9MM|*qn-Dp0kN@~p?mpRkuD#)1G@~FO`?-ALmBHi6@>MxEqA#1xd2~WTzeewjs zA{+N^GJEHm1#o};QH2U=rpTYw0JzH*wUk{jf1T277Byz7x$QVM3j7m@(#7I>sn>Qw z2T-N}!(%m*t~WOKVQXM~`z0eGE+Zk-*TvoXHh!!0=i%5twzV(GGM=jFB?0?=4hTR6 zuo+Dr0a6SnK870QvJ19qoob~L18~meeWUVkw@g<9g5=Jn&mZ##(B49%B*j9{l}3Y8 zhE{=|9N@!5^|MM`Jp*c@bF~0Dw$4&~k8N$6<`m9~T$YZ$k&)oT!ln3hGxCj6t0TZG z%qA4FfXmOFa2M(+OHl{ej7G%mGzJ^`<0)BKzfD^;3s5)CVAT4pMlsz^s0Gr8v#FIB zTRt*<-$%O>6cDQNL-GbFj}?N`<^b@>6NdURRqHPfU5;TXy>sO?C*ACqnjK(#WHUr# z&>R?zUzxwg2UqU_&Wf~HcP2td`JNOSdn7BHOX9^FvQ`ZD3{jpaleh@Ua!m`V({)l*!XE%FUdR!>>K}Lef?{UZ2 zsZjCs3(F=2m)BVBN$%en4bkbgc|A^~)}^48lxmb*D59cSgaEcK1eRDj?kCh&O4F}F z&@gN?x4;Xq^mvvV%8ir$l_9JEWa!%^TC{$U%6}Hc^~osdA$O+=NGpBvOP9utrw9Ai z9wf5i*Y}DIft{xUs4yCCH+kKg6(`yb2P|5sN@RORxB!G65l+<_j$&`skG$1AeU%KJ zTS4$^o$>mq&HRpM_fV-*(;P2>#csbdP@GM-EHVITIQ{^#_PIRh-e@(4Ex9m2i+Krh4{h{@tYP)8x*vmV4>rZ; z61fIBIiBroPeEu;By@SH4}&&(K_&vA?e8V!#+r^Z0jL@Z56n-9or^^m`;2W}s|Mx~4X_IDu7HheB_% zOGrcF-*xh{@qPkKNE2|vLaAJaM~5s`V%Oyi#n7#NZf;)IlIe&{`)!+YPtrp0or}A{ zERpc308)qdeW(2tSlg9>qtJ6HgKN-M6`^y>#@lU|4~^Y!Y?A|1&v`In&|p5Ndzlhi zUJ?Kz(kPRqC@7IB`U<}_(gFa+NnWxx$d^RSp8-d%oM>e@ z_Y-;DPvdh(X;C~m1A2g>TCx6>Zm(oH`Frbv))-i8s#Z{g6A3!nH;PZFIYyXqa;? zNM4o-6Oq`b(~|8xUP}!I<)O4M|Du{IYNie(hW}&Z%~93<&~|V(^PbPYnBqD71q?+S zzuvqx(zzc0lP2weVpks@psiQC!;XGotlD$gErctQ+c^lIY?;uchTV?Sk}LO z)4vQlcc&V#Oq=#K;-9yodB%o$U9-c`eG}l&*ow$C@m}=4@@Iu*#|`E&?qTWjcEQ@^ zCCNjyk&JwBL`GWF#a;r<)X%;f9(gaK$7uE9<1Ol+abPY6{{Ag3X2mIYg-ekfCjbIt zeNw7-!_WH!<(!B9)+l&{sdW zi|UbET8qyGKSKKCzBVi*K{bSB7=d?BhqEjQ1)6R{o73wx4~heKFRwf@u+5mMMh7t( z#)6Tblro11pn0*|4ezlU-N0Dl*6fl`ZIYXiV+^jgT+V&)Em7B$k(3i z9dc73@tW|VFFny8^x=Z@%t-0=Iei1p%cd2Wmvtj_ZON&Cikz*gS9m}HiGJKHEES+c z<|)${fff7|J@u`@(#54%`&?>S0j!VX7RhUx!u5o~Ml$p}p7v|L03Ak-mn!?QNppJL zH&mt>UD-fj`OiCs@ZA2qxcz-j&f(;)r<`abvzh>b{>SJ0*`9`3Pej3nkv|k!PMaGl zwGH2HjjbH%dECZeEk|vs-+T*;*c=!}-pintOmGzix{C@$4+y*UMxxE)`S(zSX;$Lp zoCp6b_lvQj!f3owg&)P*Zm+huU<~;@)MgB80cr(zGHCtvRE!Z6Vmb;$G^vN zYx3mGim#Pu2>N`&Sz6g(-M*@=_CdupU|nd@SrC)n+}0VJK&?rF?fHWdu1Rni-@>6yltoar?fQ(%E%H7iLSJM@D zE*)XDP4^D}8^+e}-OHwT2D;>7*q#@39gO|lST4sqeI}aI+WuSI#&8bUVhW3ikI>@0 zO|x4HAM>glmNHBftDO`;^e98YkIC5@D87R|J}$tnOVE2*S_)?Zs~J3FJV>Px_Qw`?!RUBU@RiBtXJC z(G*CZrb7M3dk0z^)VE&6CMZqc*<>~LD0*2e2}qrU4&Xy(WUHOWxn8)47t7S#%|4jl zrAvrL=Y&Bv8S!Qqml5+-8Jrhv3!cW={meHaMagznM7+~Sd}1kxQuwC2?^qc381JxO zR>6IcCtMQaUKuZ0G~xKa)IVE)y;4U9Www|9*=)lLg1EzrefwNuRwz*~;-!8b-KLv! z)v7gh3?O_ZH~K~>5AE>~mz1yA3A#1z)x#spa@<<+M~RCN_!$yYM-(N$a#bac59gC7 z%1HavDCzcp#9c4{_r?SnX09g)ROFIMxvVX`Jed%G+op?arpe>+08NzfKdVo;oZ<}G z4NpNgjqcIXy6$xmy(Nkw4srxO+;i61_hcB7yKKVor|s^Y_}Kw0$=s(%xb+5Ym?h+0 z2F|8!7|Eeb*}SwluPc<2c8AHzCoVd;-#0!ObUkCZ1-2(Pq{t~5#$z0{kwUN`4U!;D z<7+Rz<+#;}+!mHS($neb0THG z4hF5dAJmbCC49eHptG{JCN>%Kk_qgs30M;6&|3#d8TAz!We9y65w5s%7W%}h6W3N% z=;x!LH};-V?A?;lHt47|Q?wO8R&%FsV?%ArlA=WQX#;~JenoDa@(j3}coOKqW&_DV z&?~S&*27VqzKn8Zu~0Tet|kSOaN5?j`VRLfciO7nYtQ2ZWjMw7+4UNG>P8#yvsR4zr4pta z0n0*bjAcgFN*H1W7&be6-iZRN>LFwm+SYI=W zF*{)Qh^!$blIu-_E9~p9gFOq)i zCo)TieYk*X{0mdrn#jyS6w1ds{a ze25Pa^c&&D;cqFn#!Pk}>g^syE7`V7wuEt$bUuH|9zu9CGe&EmeORe-n-pd@a_t5| zzB8Dt)q9hWZ-++fiM(JPRY|*WmNm4ieYU=Xyi0S&;q8b>pBkuMyNOrPrU~Q6ZkGji zHeNOIs1{U$&Whs$L<7x?<=7?qMeKyMXCL-pDV)%HtMK{8l+6bW(Gb`%7?Hko(4V0{ zhCS6$JBSl;B^OdZ72;-nr3sd{#^XOUQ=np03#h;f9YX{^X`Kb~9_vHzNwFYWvqwH# zDGsk?L?E%%X<`lth!Y1nsNan@Z-dXZe`WPfCk=vnwam_6lP7^j2FHu`Ra^|PIvZgRA)gxvlx$z~wO%V`ake$KGcCX*i zyGC2Yu2LLZ8V%>RXCV1XFzJ4`NqBM{Lj{^y&MJ<)w<%k-a{60M;+3D2^3T0dH|b~3 zfc&w(+tmHgIuq;lAO5*verr8!2K5U6mSPATs}v8RGGEKK;6IUeF%ZOe4sPL3@~X2U zi4(sE3yhNf7z8FLF5V9Wkq}MubkkOrMM5)QDW4W(78 zb0nKIvoY<`P`XmTL(721O0J%b`^GN&gXvU$cv^#-V7r)Q)zPgHEUnRx8_JVltB`C! zKxaxaM_pXbUqQY;1XS_Lz&8jBo%#(@A7BND9p5N~TQiX=0)g4fu(EqRZ0g=$Sj{%- z#;k`!>OY+1ID1t#$Qhr|=m*!Bma<6E0xG`8tU*n7-2+zuRhhYu z+%!fUvu(jQy6_2Di=fW-NEWAJ|&tTKtO!A>e+&AsGO03(4(-pDGsm~!yw)i7;vttCweoR zQ?}Xbu`BgH4Mv><9dVnd_&n#k?pZvAi;W7EOVmu~p2o6ZtE8~dW8kQ(bN$l+NSqQ= zz(@v0>h(yBZ1Em2jqun3A1JtOOOi{jIX7D6fce#wyWsx|zy@_o)$Y#d2v>$}Lhxod z{?j%jA^Jz78v_Fs2A==xi$hyC9gt=@`(9ghQ(4O>OwF_LQ*2Lv#&1StScfzO?4Hlo zGeEhGg`#)8(l*2tz=kfZRAob&-zN_MF3qx-l&HWZCZ0Zpqb1#Q^TyG6SCJMa(+O9N|i}g5;T`O>pm1nPPp( zBxeBcaH1#!ytV*wvb;4#RXa{=a2Gr&efrcfC)OLn>?%*iAZ^=t%Sxu#WbmPd!i9lm zFC9{H+Ly5+hdd|K{W1mOo2dOAnx(p&_J;*?3eBD@iJR zcc_FJ2m5*vh28igfj|c^*{&ULjMJ*@b4)dPt5+2L zwF5>)-ol<7ZSfo?*$!Q=*r6kj2i{7!o})SLA0vS4V||kRZN}R_)f)xr0qc?8cMIyj zY9Z#&9N zKUm~tfuMogAHiL#e9&8s1lK$Td!Y;`hWEp-teir!7;~sn)P;z)PNNkiuA1Lxpo0m} z*%FUWw(=OU9HvXm#4KHYChD|E#s9bX8v|Ag-kWB4|C1OK>LyPX*`HziN*Ojw<-b#i zWuR@M50-}!9$(*NPaFWd=3aU0;GuwWO}gy|%U6{r{+Aso-I|zOYmW_V=ASv+xaJvox_!NNB3@|M$-&m$Hgbreu|oyGYWC>E?ckB$i7?l3T=-_O_+B!sIf= zTZORrAhg`xSw>7*Vp(EyziXMxTP~T#_tpFR`3F9)AJ2K5`+1%7cs$Q}KCy}Einw^v zPUY%CRKhv*7Nw_&KHA#k^9NqM5-4ifS$=uKw^cUyk_CS+QB<>SRyRJ6$*k0~#>fRS zpD-1R{2~aYs+NvvnW^7LKQ_(gO!b&?UwU8S`ms$`W`7XO4ABLf*rEv)cC|m-#3491 z1UvOtY3#4ienBGlB!cEP8RdFm)@v<~kh5b!DNhk-j2T7hSU0Pe#EtK5tB5hdOPtFn z%fOGs~>A+o=Rr$x2g@<{}R;%WA-As4Cb&(a(fM3*Kju!nEXcTFd9$_U&&JWwD zsxFT{fS2F|tbLVMSAR3EWJ9>>^CaYTBG*p#?IFHoLeqWL7d2DXy_gz(;KU64zHI$O zhF);%<)FLL9-J#uAc2EaWrq^h#_y7e6?c)EC__US0iWX05bPZU1Ft zoS8w7xPh6&15XQm&8wxSiyMO7xaYj(r4i>SM^=tEcu&HwfPa5L`P158roxF3#H^vg9Nes zZL?o!TNSq4Sal|hzs>NDhj=~Z)|I}yhqD_@^>XKwghM;snjs0Xt}Mu zVjhUB=XwqYHwf?l!}?Ep$>}I5D@*&W2`MI{#uxsyU*g`4#qRgdUQ-MCHH*!tD_YWj zrVltxRc828TOyd-LE=R7o`$tTsxdAKG|2f#KDUhJ+&Y+iWIY>rUxJ*|Vm?$>b<*Qo zuQD12IEk>-o%nATpYfv|LS>A_Hj#E-0n`Nk3+|S)6L+Cm0jddDDmZ-6IldA|oG_#v z1jsT)5a9I6jwYA*A?Esyp-6qaIG`$sA?-@;DVVtOjUUi4SwtgfU`E zIUs36rar1Z!a(wjeWKt^+p{07(Ydl9RRK;qLLa{8mAN>^ML_2^odkNtl~92UXx&Oy zCUF*cODd@fIgbRNS~WyBb*j4y(KvsA$(V;@KDq@q+be{-w%+HLMYe}by;A`nQ(&pE zWlZx$1&Uc_l_9=O);6l-O`5An46_+dN_tiEm96>BiA6-v&r;e%7Xw4wqrWLj15!8+ z_{L8>tttNA_+fm#{+eugJ>{zVbV*-<5dgPjy)WKwA8n!NWgcM!J$O=)`LK?+pcVCK z#uhZ5KMV{$`423b#x)tM=IRya|7j7N_N%n}^><0s2{E8A1qZDg`7w#%aahz^EVZ+R z{`#cWYIxl8noh2s{9GL%<@tc;DLvh;S{hxJ_Cz~U3x%?tv}19EXvF9v&QWQpFExOb+#_&e~R&0z3+;{i$TvD{jXmm z$xT2?`{b`(eBtYQ<>N{X>xdFtoNA;E!)q&Do7JJ6!F1 zSTo^PDw#*VA0tvdIC2!TeBUlncTKnhUke6~9hGPVw}ON1=wE`9#^z1P{;eW=D`#@_mCA?TX?i@dZCG+NH1Lri!9MS(D66=b_sZ#N zuHMLDN#FI9>z+$&wcA_RFfc3yCl$=QN$XvZDx;`nPGBk@mh=*_WIU6)=!04hL2YZ}Z2o1L^?;D8n4o_t_YG$%Tb=rQ7!^Fz(o748v#$Ov0XyUevRGqiIX+ggV?JzSKIG$_XE=6 z2!h096pySxa-6%;=qUVVfXp@!!7+2C>$!;ooqAtFcwDOtuTBJ79Hiv-)qV!jRRaK=T!|5~%~QHR)} z(#0^9(+*$af36a97y2_4K`Mf`7re;{qsd7m?1n)Js`*sxxufhq$wc+>M_d>nMX)A8 zYh+3lyJspLz`)tZ?Xb@;YH{jtj^(sCfZv43=Txd~oT3CtcYnD4t288~QAw!|6-*vD z?0r@5gZ*#q)Fd$#8wrql38Bf^09a5x_4uE!9Mc80Ks;!@&JXR8A=Qzpzz9~ zH~lt38Fcq9)=_2WwMk_Up=*`N$;MTc>uG`(E<$8r zbgA#-$^Z8$ALiYbfue+v2Rc*mZtMl9PLU!$E`n6mz<{7M{sS61dGu&o1yA9(H2W+S zK%t%?PxS6VkLjmqRc&AeQc&m*gRZq?@WsnolNpj$b$USfcH&2K9u!9>ILWh{vzSVK zdeBwi{3@%45fIhC9bkG7gY7sSyKUh*kH=+`H_YOi9%qCS)S7sJltkmgn$y^BCmd$& zC?y{w#*fA|*|rTC5{4*JP(P5sm=U_0q!pS(l){Wfr1R70=v}>HfrFZS69D$!huwgC z(j%&V&0r_$Tv~`ny^CzUDklr)S-VI#bgrPJm3fW+VSW+*S6t`s!W z8Mu=cGxl=tivkz!N|<=I)((}l3pxQoa}^8?zAcdO>B&kGz~Qptn5PV=O=74uH3mP#Ku| zpD#Oc#LBche(DXCzSw8D-o4MO+nG7N36RW>eFyWrNUsb&RwRm+4RTaO_bg}~w~6=z zx`GUEh|&WUZiIAOtqZ-iQXP+?#{Uq$PTZLTHDzHK6F^0SsCN7I?CH6fYPGj`3%3qv zL&M_Rn(k0Vg_(;7OPVt1`u>NbT$Ify3 z|D#a;#b{)SeJ&VURX-g&SS)-g4Dpdm|H{;b09vu1iasPCW9mUi6aaH^vvOVO&<%ao zRJ+#XZL<6Wrc zk1Po2IRsu27|$KU?oqS-j-%81yxbT8*sKnyP1ZM!Z1}7`Mn6(DRIPWpUzzD1h{GZ} zAjRfn2iH_y1}{}wQ}~(#hA{E0AzvN44X6BR$X#PU>tV>HWKxKTmbfu;;*KxNrTgvo z*#lls?S(g;mn5n3goOjp+c_U+DvTCurFMFlS*_LAp@ zv;$~V*1f-fnQE*uC^sHd8-{{gt+wU__^ literal 0 HcmV?d00001 diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb1..00000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 24c58e39..1c2801a7 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,32 +4,61 @@ import { Form } from "./components/Form"; import { MessageList } from "./components/MessageList"; import { GlobalStyles } from "./GlobalStyles"; import { Loader } from "./components/Loader"; +import { Logo } from "./components/Logo"; +import { Footer } from "./components/Footer"; export const App = () => { const [messages, setMessages] = useState([]); const [loading, setLoading] = useState(true); + const [posting, setPosting] = useState(false); // Get all messages - const getMessages = () => { - setLoading(true); - fetchThoughts() - .then(setMessages) - .catch((error) => console.error("Fetch failed", error)) - .finally(() => setLoading(false)); + const getMessages = async () => { + try { + setLoading(true); + const data = await fetchThoughts(); + setMessages(data); + } catch (error) { + console.error("Fetch failed", error); + } finally { + setLoading(false); + } }; // Post new message - const handleNewMessage = (message) => { - postThought(message) - .then(getMessages) - .catch((error) => console.error("Post failed", error)); + const handleNewMessage = async (message) => { + const optimisticThought = { + _id: Date.now().toString(), + message, + hearts: 0, + createdAt: new Date().toISOString(), + }; + + setMessages((prev) => [optimisticThought, ...prev]); + setPosting(true); + + try { + await postThought(message); + await getMessages(); + } catch (error) { + console.error("Post failed", error); + } finally { + setPosting(false); + } }; - // Like message - const handleLike = (id) => { - likeThought(id) - .then(getMessages) - .catch((error) => console.error("Like failed", error)); + const handleLike = async (id) => { + setMessages((prevMessages) => + prevMessages.map((msg) => + msg._id === id ? { ...msg, hearts: msg.hearts + 1 } : msg + ) + ); + + try { + await likeThought(id); + } catch (error) { + console.error("Like failed", error); + } }; useEffect(() => { @@ -39,9 +68,12 @@ export const App = () => { return ( <> +

Happy Thoughts

- + + {!loading && posting && } +