diff --git a/README.md b/README.md
index 41ebece2..775bfdda 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,3 @@
# Happy Thoughts
+
+Netlify link: https://happy-thoughts-25.netlify.app/
\ No newline at end of file
diff --git a/index.html b/index.html
index d4492e94..a8465b8b 100644
--- a/index.html
+++ b/index.html
@@ -2,7 +2,10 @@
-
+
+
+
+
Happy Thoughts
diff --git a/package.json b/package.json
index 2f66d295..c82846a1 100644
--- a/package.json
+++ b/package.json
@@ -10,8 +10,10 @@
"preview": "vite preview"
},
"dependencies": {
+ "dayjs": "^1.11.19",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "styled-components": "^6.1.19"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 00000000..4ba09560
Binary files /dev/null and b/public/favicon.ico differ
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 07f2cbdf..f3a88b38 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,5 +1,219 @@
+import { useState, useEffect, useRef } from "react"
+import styled from "styled-components"
+
+import { ThemeProvider } from "styled-components"
+import { GlobalStyles } from "./styling/globalStyles.js"
+import { theme } from "./styling/theme.js"
+import { Header } from "./components/layout/Header.js"
+import { Hero } from "./components/layout/Hero.js"
+import { Footer } from "./components/layout/Footer.js"
+import { InputCard } from "./components/input/InputCard.jsx"
+import { MessageList } from "./components/messages/MessageList.jsx"
+import { HeartLoader } from "./styling/LoadingAnime.jsx"
+
export const App = () => {
+
+ const scrollRef = useRef(null)
+
+ const [messages, setMessages] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [scroll, setScroll] = useState(false)
+
+ // State to track liked posts in local storage
+ const [likedPosts, setLikedPosts] = useState(() => {
+ const saved = localStorage.getItem("likedPosts")
+ return saved ? JSON.parse(saved) : []
+ })
+
+ // fetch messages from API + interval polling
+ useEffect(() => {
+ const fetchMessages = () => {
+ fetch("https://happy-thoughts-api-4ful.onrender.com/thoughts")
+ .then(res => {
+ if (!res.ok) {
+ throw new Error(`Failed to fetch: ${res.status}`)
+ }
+ return res.json()
+ })
+ .then(data => {
+ setMessages(data)
+ setLoading(false)
+ setError(null)
+ })
+ .catch(error => {
+ console.error("Error fetching messages:", error)
+ setError("Something went wrong. Please try again ❤️")
+ setLoading(false)
+ })
+ }
+
+ fetchMessages() // Initial fetch
+
+ // Set interval to fetch messages every 30 seconds
+ const intervalID = setInterval(fetchMessages, 30000)
+
+ // Cleanup interval on component unmount
+ return () => clearInterval(intervalID)
+ },[])
+
+ // Update local storage when likedPosts changes
+ useEffect(() => {
+ localStorage.setItem("likedPosts", JSON.stringify(likedPosts))
+ }, [likedPosts])
+
+ // Post new message to API
+ const addMessage = async (newText) => {
+ try {
+ const response = await fetch("https://happy-thoughts-api-4ful.onrender.com/thoughts",
+ {
+ method: "POST",
+ headers: {"Content-Type": "application/json"},
+ body: JSON.stringify({ message: newText})
+ })
+
+ if (!response.ok) {
+ throw new Error("Failed to post your message 💔")
+ }
+
+ const data = await response.json()
+ setMessages(prev => [data, ...prev])
+ setScroll(true)
+
+ } catch (error) {
+ console.error("Error posting message:", error)
+ setError("Something went wrong. Please try again ❤️")
+ }
+ }
+
+ useEffect(() => {
+ if (scroll && scrollRef.current) {
+ scrollRef.current.scrollTo({
+ top: 0,
+ behavior: "smooth"
+ })
+ setScroll(false)
+ }
+ },[messages, scroll])
+
+ // Send like to API
+ const increaseHeart = async (id) => {
+ const message = messages.find(msg => msg._id === id)
+
+ try {
+ const response = await fetch(`https://happy-thoughts-api-4ful.onrender.com/thoughts/${message._id}/like`,
+ { method: "POST"}
+ )
+
+ if (!response.ok) {
+ throw new Error("Failed to send like 💔")
+ }
+
+ const updated = messages.map(msg =>
+ msg._id === id
+ ? { ...msg, hearts: msg.hearts + 1 }
+ : msg
+ )
+ setMessages(updated)
+
+ // Update likedPosts state
+ setLikedPosts(prev =>
+ prev.includes(id) ? prev : [...prev, id]
+ )
+
+ } catch (error) {
+ console.error("Error liking message:", error)
+ setError("Failed to send like ❤️🩹 Try again!")
+ }
+ }
+
return (
- Happy Thoughts
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ {error && {error}}
+
+ {loading ? (
+
+
+ Loading Happy Thoughts...
+
+ ) : (
+
+
+
+
+
+ )}
+
+
+
+
+
+ >
)
}
+
+const AppContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+`
+const CardWrapper = styled.div`
+ width: 600px;
+ max-width: 85%;
+ margin: 0 auto;
+`
+const ErrorBox = styled.div`
+ margin: 50px auto 10px;
+ border-radius: 6px;
+ width: fit-content;
+ text-align: center;
+`
+const LoadingWrapper = styled.div`
+ display: flex;
+ overflow-y: auto;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ font-size: 20px;
+ color: ${({ theme }) => theme.colors.text };
+`
+
+const ScrollArea = styled.div`
+ flex: 1;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 30px 0 50px;
+ padding-left: 12px;
+
+ /* Scrollbar Styling */
+ &::-webkit-scrollbar {
+ width: 12px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background-color: ${({ theme }) => theme.colors.primary };
+ border-radius: 8px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background-color: ${({ theme }) => theme.colors.formBackground };
+ }
+`
\ No newline at end of file
diff --git a/src/components/input/InputCard.jsx b/src/components/input/InputCard.jsx
new file mode 100644
index 00000000..ab954ff2
--- /dev/null
+++ b/src/components/input/InputCard.jsx
@@ -0,0 +1,30 @@
+import styled from "styled-components"
+import { InputForm } from "./InputForm.jsx"
+
+export const InputCard = ({onSubmit}) => {
+ return (
+
+
+ What's making you happy right now?
+
+
+
+
+
+ )
+}
+
+const InputCardSection = styled.section`
+ /* width: 100%; */
+`
+
+const StyledInputCard = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ border: 2px solid ${({ theme }) => theme.colors.border };
+ height: auto;
+ padding: 20px;
+ background-color: ${({ theme }) => theme.colors.formBackground };
+ box-shadow: 7px 7px ${({ theme }) => theme.colors.border };
+`
diff --git a/src/components/input/InputForm.jsx b/src/components/input/InputForm.jsx
new file mode 100644
index 00000000..a152e22d
--- /dev/null
+++ b/src/components/input/InputForm.jsx
@@ -0,0 +1,77 @@
+import { useState } from "react"
+import styled, { useTheme } from "styled-components"
+import { SubmitButton } from "./SubmitButton.jsx"
+
+
+export const InputForm = ({ onSubmit }) => {
+ const [textInput, setTextInput] = useState("")
+ const theme = useTheme()
+
+ const isValid = textInput.length >= 5 && textInput.length <= 140
+ const showError = textInput.length > 0 && !isValid
+
+ const handleSubmit = (event) => {
+ event.preventDefault()
+ if (!isValid) return
+
+ onSubmit(textInput)
+ setTextInput("") // Clear the input after submission
+ }
+
+ // Change color of character count based on length
+ const getColor = () => {
+ const length = textInput.length
+ if (length >= 130) return theme.colors.inputLimit
+ return theme.colors.text
+ }
+
+ return (
+
+
+ )
+}
+
+const StyledForm = styled.form`
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ padding-top: 10px;
+ width: 100%;
+ height: 150px;
+
+ textarea {
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ padding: 5px 10px;
+ border: 2px solid #ccc;
+ border-radius: 5px;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ }
+`
+
+const StyledP = styled.p`
+ font-size: 12px;
+ margin-bottom: 10px;
+ padding: 0;
+`
+const ErrorText = styled.p`
+ font-size: 12px;
+ color: ${({ theme }) => theme.colors.inputLimit };
+ margin-bottom: 10px;
+`
\ No newline at end of file
diff --git a/src/components/input/SubmitButton.jsx b/src/components/input/SubmitButton.jsx
new file mode 100644
index 00000000..b3296c9c
--- /dev/null
+++ b/src/components/input/SubmitButton.jsx
@@ -0,0 +1,31 @@
+import styled from "styled-components";
+
+export const SubmitButton = ( textInput ) => {
+ return (
+ 140}
+ >
+ ❤️ Send Happy Thought ❤️
+
+ )
+}
+
+const StyledButton = styled.button`
+ background-color: ${({ theme }) => theme.colors.primary};
+ border: none;
+ border-radius: 50px;
+ width: 50%;
+ padding: 10px 20px;
+ font-size: 16px;
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+
+ &:hover {
+ background-color:${({ theme }) => theme.colors.secondary};
+ }
+
+ @media ${({ theme }) => theme.breakpoints.mobile} {
+ width: 100%;
+ }
+`
\ No newline at end of file
diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx
new file mode 100644
index 00000000..c6a67d02
--- /dev/null
+++ b/src/components/layout/Footer.tsx
@@ -0,0 +1,26 @@
+import { styled } from "styled-components";
+
+type Props = {
+ text: string;
+}
+
+export const Footer = ({text}:Props) => {
+ return (
+
+ {text}
+
+ )
+}
+
+const FooterSection = styled.footer`
+ padding: 15px 0;
+ background-color: ${({ theme }) => theme.colors.primary};
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+`
+const FooterText = styled.p`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+`
\ No newline at end of file
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx
new file mode 100644
index 00000000..a2df5876
--- /dev/null
+++ b/src/components/layout/Header.tsx
@@ -0,0 +1,20 @@
+import { styled } from "styled-components"
+
+type HeaderProps = {
+ likedCount: number;
+}
+
+export const Header =({ likedCount }: HeaderProps ) => {
+ return (
+
+ You liked ❤️ {likedCount} posts
+
+ )
+}
+
+const NavBar = styled.div`
+ background-color: ${({ theme }) => theme.colors.primary};
+ display: flex;
+ justify-content: flex-end;
+ padding: 10px 30px;
+`
\ No newline at end of file
diff --git a/src/components/layout/Hero.tsx b/src/components/layout/Hero.tsx
new file mode 100644
index 00000000..9a804e24
--- /dev/null
+++ b/src/components/layout/Hero.tsx
@@ -0,0 +1,21 @@
+import { styled } from "styled-components";
+import { AnimatedTitle } from "../../styling/animatedTitle.jsx";
+
+type Props = {
+ text: string;
+}
+
+export const Hero = ({ text }: Props) => {
+ return (
+
+
+
+ )
+}
+
+const HeaderSection = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100px;
+`
\ No newline at end of file
diff --git a/src/components/messages/LikeButton.jsx b/src/components/messages/LikeButton.jsx
new file mode 100644
index 00000000..8bfb1b1a
--- /dev/null
+++ b/src/components/messages/LikeButton.jsx
@@ -0,0 +1,42 @@
+import styled from 'styled-components'
+
+export const LikeButton = ({ hearts, liked, onLike }) => {
+ return (
+
+
+ ❤️
+
+ x {hearts}
+
+ )
+}
+
+const ButtonWrapper = styled.div`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 10px;
+`
+
+const StyledLikeButton = styled.button`
+ background-color: ${({ theme, liked }) =>
+ liked ? theme.colors.primary : theme.colors.formBackground };
+ border: none;
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ font-size: 20px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background-color 0.3s ease;
+
+ &:hover {
+ background-color: ${({ theme }) => theme.colors.secondary};
+ }
+`
\ No newline at end of file
diff --git a/src/components/messages/MessageCard.jsx b/src/components/messages/MessageCard.jsx
new file mode 100644
index 00000000..7f813e31
--- /dev/null
+++ b/src/components/messages/MessageCard.jsx
@@ -0,0 +1,41 @@
+import styled from "styled-components";
+import { LikeButton } from "./LikeButton.jsx";
+import { HappyText } from "./MessageText.jsx";
+import { Time } from "./Time.jsx";
+
+export const MessageCard = ({ id, text, hearts, onLike, liked, createdAt }) => {
+ return (
+
+
+
+ onLike(id)}
+ liked={liked}
+ />
+
+
+
+ )
+}
+
+const MessageSection = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ margin-bottom: 30px;
+ gap: 20px;
+ border: 2px solid ${({ theme }) => theme.colors.border };
+ height: auto;
+ padding: 20px;
+ background-color: ${({ theme }) => theme.colors.cardBackground };
+ box-shadow: 7px 7px ${({ theme }) => theme.colors.border };
+ `
+
+const LikeButtonWrapper = styled.div`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+`
\ No newline at end of file
diff --git a/src/components/messages/MessageList.jsx b/src/components/messages/MessageList.jsx
new file mode 100644
index 00000000..d6627ca6
--- /dev/null
+++ b/src/components/messages/MessageList.jsx
@@ -0,0 +1,21 @@
+import { MessageCard } from "./MessageCard.jsx";
+
+export const MessageList = ({ messages, onLike }) => {
+ return (
+ <>
+ {messages.map((message) => {
+ return (
+ onLike(message._id)}
+ liked={message.hearts > 0}
+ />
+ )
+ })}
+ >
+ )
+}
diff --git a/src/components/messages/MessageText.jsx b/src/components/messages/MessageText.jsx
new file mode 100644
index 00000000..f6cd236c
--- /dev/null
+++ b/src/components/messages/MessageText.jsx
@@ -0,0 +1,14 @@
+import styled from "styled-components"
+
+export const HappyText = ({ text }) => {
+ return (
+ {text}
+ )
+}
+
+const StyledText = styled.p`
+ white-space: normal;
+ overflow-wrap: break-word;
+ word-break: break-word;
+ line-height: 1.5;
+`
\ No newline at end of file
diff --git a/src/components/messages/Time.jsx b/src/components/messages/Time.jsx
new file mode 100644
index 00000000..d3c7cecc
--- /dev/null
+++ b/src/components/messages/Time.jsx
@@ -0,0 +1,10 @@
+import dayjs from 'dayjs'
+import relativeTime from 'dayjs/plugin/relativeTime'
+
+dayjs.extend(relativeTime)
+
+export const Time = ({ createdAt }) => {
+ return (
+ {dayjs(createdAt).fromNow()}
+ )
+}
\ No newline at end of file
diff --git a/src/styling/LoadingAnime.jsx b/src/styling/LoadingAnime.jsx
new file mode 100644
index 00000000..41e5c8a0
--- /dev/null
+++ b/src/styling/LoadingAnime.jsx
@@ -0,0 +1,16 @@
+import styled, { keyframes } from 'styled-components'
+
+export const HeartLoader = () => {
+ return ❤️
+}
+
+const bounce = keyframes`
+ 0%, 100% { transform: translateY(0);}
+ 50% { transform: translateY(-12px);}
+`
+
+const Heart = styled.div`
+ font-size: 40px;
+ animation: ${bounce} 0.6s infinite ease-in-out;
+ text-align: center;
+`
\ No newline at end of file
diff --git a/src/styling/animatedTitle.jsx b/src/styling/animatedTitle.jsx
new file mode 100644
index 00000000..2ae5d388
--- /dev/null
+++ b/src/styling/animatedTitle.jsx
@@ -0,0 +1,43 @@
+import styled, { keyframes } from "styled-components"
+
+export const AnimatedTitle = ({ text }) => {
+ return (
+
+ {text.split("").map((char, index) => (
+
+ {char === " " ? "\u00A0" : char}
+
+ ))}
+
+ )
+}
+
+const bounce = keyframes`
+0% { transform: translateY(0); }
+20% { transform: translateY(-10px); }
+40% { transform: translateY(0); }
+80% { transform: translateY(0); }
+100% { transform: translateY(0); }
+`
+
+const Title = styled.h1`
+ font-size: 48px;
+ font-weight: 700;
+ display: flex;
+ gap: 2px;
+ justify-content: center;
+ margin: 0;
+
+ @media ${({ theme }) => theme.breakpoints.mobile} {
+ font-size: 32px;
+ }
+`
+
+const Letter = styled.span`
+ display: inline-block;
+ animation: ${bounce} 1.3s ease-in-out infinite;
+ animation-delay: ${({ delay }) => delay * 0.05}s;
+`
\ No newline at end of file
diff --git a/src/styling/globalStyles.js b/src/styling/globalStyles.js
new file mode 100644
index 00000000..7eaf20f8
--- /dev/null
+++ b/src/styling/globalStyles.js
@@ -0,0 +1,19 @@
+import { createGlobalStyle } from "styled-components";
+
+export const GlobalStyles = createGlobalStyle`
+ * {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ }
+
+ body {
+ background-color: ${({ theme }) => theme.colors.backgroundColor };
+ color: ${({ theme }) => theme.colors.text };
+ font-family: Google Sans Code, sans-serif;
+ }
+
+ p{
+ font-size: 1rem;
+ }
+`
\ No newline at end of file
diff --git a/src/styling/theme.d.ts b/src/styling/theme.d.ts
new file mode 100644
index 00000000..f5f4e800
--- /dev/null
+++ b/src/styling/theme.d.ts
@@ -0,0 +1,22 @@
+// styled.d.ts
+import "styled-components";
+
+declare module "styled-components" {
+ export interface DefaultTheme {
+ colors: {
+ primary: string;
+ secondary: string;
+ formBackground: string,
+ likeBtnBackground: string,
+ cardBackground: string;
+ text: string;
+ textSecondary: string;
+ border: string;
+ };
+ breakpoints: {
+ mobile: string;
+ tablet: string;
+ desktop: string;
+ };
+ }
+}
diff --git a/src/styling/theme.js b/src/styling/theme.js
new file mode 100644
index 00000000..fea765b9
--- /dev/null
+++ b/src/styling/theme.js
@@ -0,0 +1,19 @@
+export const theme = {
+ colors: {
+ primary: "#ffadad",
+ secondary: "#ff6f6f",
+ backgroundColor: "#fafafa",
+ formBackground: "#f2f0f0",
+ inputLimit: "#ff0000",
+ cardBackground: "#ffffff",
+ likeBtnBackground: "#ffe3e3",
+ text: "#1a1a1a",
+ textSecondary: "#4d4d4d",
+ border: "#000000"
+ },
+ breakpoints: {
+ mobile: "(max-width: 480px)",
+ tablet: "(min-width: 481px) and (max-width: 1023px)",
+ desktop: "(min-width: 1024px)"
+ }
+}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..5066fe90
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,48 @@
+{
+ // Visit https://aka.ms/tsconfig to read more about this file
+ "compilerOptions": {
+ // File Layout
+ // "rootDir": "./src",
+ // "outDir": "./dist",
+
+ // Environment Settings
+ // See also https://aka.ms/tsconfig/module
+ "module": "nodenext",
+ "target": "esnext",
+ "types": [],
+ // For nodejs:
+ // "lib": ["esnext"],
+ // "types": ["node"],
+ // and npm install -D @types/node
+
+ // Other Outputs
+ "sourceMap": true,
+ "declaration": true,
+ "declarationMap": true,
+
+ // Stricter Typechecking Options
+ "noUncheckedIndexedAccess": true,
+ "exactOptionalPropertyTypes": true,
+
+ // Style Options
+ // "noImplicitReturns": true,
+ // "noImplicitOverride": true,
+ // "noUnusedLocals": true,
+ // "noUnusedParameters": true,
+ // "noFallthroughCasesInSwitch": true,
+ // "noPropertyAccessFromIndexSignature": true,
+
+ // Recommended Options
+ "strict": true,
+ "jsx": "react-jsx",
+ "verbatimModuleSyntax": true,
+ "isolatedModules": true,
+ "noUncheckedSideEffectImports": true,
+ "moduleDetection": "force",
+ "skipLibCheck": true,
+
+ "allowJs": true,
+ "checkJs": false,
+ "noEmit": true
+ }
+}