diff --git a/.gitignore b/.gitignore
index b02a1ff7..97cff809 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,5 @@ package-lock.json
*.njsproj
*.sln
*.sw?
+.env
+.env
diff --git a/README.md b/README.md
index 41ebece2..5441815f 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,49 @@
-# Happy Thoughts
+# Happy Thoughts Frontend
+
+React app for sharing happy thoughts. Users can post thoughts, like others' posts, and manage their own content with authentication.
+
+## Live Demo
+
+đ [View the live site here](https://happythoughtsbyc.netlify.app/)
+
+## Features
+
+- Post thoughts (anonymous or logged in)
+- Like and unlike thoughts
+- User authentication (register/login)
+- Edit and delete your own thoughts
+- Character counter (5-140 chars)
+- Responsive design with styled components
+
+## Tech Stack
+
+- React with hooks
+- Styled Components
+- Vite
+- localStorage for auth tokens
+- Custom Happy Thoughts API
+
+## Installation
+
+1. Clone and install:
+```bash
+npm install
+```
+
+2. Create `.env` file:
+```
+VITE_API_URL=http://localhost:8080
+```
+
+3. Start development server:
+```bash
+npm run dev
+```
+
+## Deployment
+
+Deployed on Netlify, connected to custom backend API.
+
+---
+
+Created with â€ïž by **Cathi**
diff --git a/index.html b/index.html
index d4492e94..78756125 100644
--- a/index.html
+++ b/index.html
@@ -2,15 +2,28 @@
-
-
+
+
+
+
Happy Thoughts
+
+
+
+
+
-
+
diff --git a/package.json b/package.json
index 2f66d295..fd5424ff 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,9 @@
},
"dependencies": {
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "react-icons": "^5.5.0",
+ "styled-components": "^6.1.17"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
diff --git a/public/happy-thoughts.png b/public/happy-thoughts.png
new file mode 100644
index 00000000..81bcfc8c
Binary files /dev/null and b/public/happy-thoughts.png differ
diff --git a/public/thumbnail-ht.png b/public/thumbnail-ht.png
new file mode 100644
index 00000000..5f3f4769
Binary files /dev/null and b/public/thumbnail-ht.png 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/pull_request_template.md b/pull_request_template.md
index 154c92e8..c6db882d 100644
--- a/pull_request_template.md
+++ b/pull_request_template.md
@@ -1 +1,2 @@
-Please include your Netlify link here.
\ No newline at end of file
+Please include your Netlify link here.
+https://happythoughtsbyc.netlify.app/
\ No newline at end of file
diff --git a/src/App.jsx b/src/App.jsx
index 07f2cbdf..b0f8ea0d 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,5 +1,150 @@
+// src/App.jsx
+import React, { useContext, useState } from "react";
+import { AuthContext } from "./context/AuthContext";
+import { useThoughts } from "./hooks/useThoughts";
+import { RegisterForm } from "./components/RegisterForm";
+import { LoginForm } from "./components/LoginForm";
+import { EditThought } from "./components/EditThought";
+import { Form } from "./components/Form";
+import { MessageList } from "./components/MessageList";
+import { GlobalStyles } from "./GlobalStyles";
+import { Logo } from "./components/Logo";
+import { Footer } from "./components/Footer";
+import { Loader } from "./components/Loader";
+import {
+ AppContainer,
+ PageTitle,
+ WelcomeSection,
+ WelcomeTitle,
+ WelcomeText,
+ LoginButton,
+ UserSection,
+ UserInfo,
+ UserEmail,
+ LogoutButton,
+ AuthFormSection,
+ AuthFormBox,
+ AuthFormTitle,
+ ToggleText,
+ ToggleLink
+} from "./App.styles";
+
export const App = () => {
+ const { user, logout } = useContext(AuthContext);
+ const {
+ messages,
+ loading,
+ posting,
+ likeCount,
+ addMessage,
+ toggleLike,
+ removeMessage,
+ saveMessage,
+ } = useThoughts();
+ const [editingId, setEditingId] = useState(null);
+ const [showAuthForms, setShowAuthForms] = useState(false);
+ const [authMode, setAuthMode] = useState("login"); // "login" or "register"
+
return (
- Happy Thoughts
- )
-}
+ <>
+
+
+ Happy Thoughts
+
+
+ {/* Welcome text - always visible */}
+
+
+ Welcome to Happy Thoughts!
+
+ {!user && (
+ <>
+
+ Share your happy thoughts for the day and enjoy reading what makes others happy.
+ If you want to edit or delete your thoughts, you can login below.
+
+ setShowAuthForms(!showAuthForms)}>
+ {showAuthForms ? "Close" : "Login"}
+
+ >
+ )}
+
+
+ {/* Logout button for logged in users */}
+ {user && (
+
+
+ {user.email}
+
+ Logout
+
+
+
+ )}
+
+ {/* Auth forms - only show when toggled */}
+ {!user && showAuthForms && (
+
+
+ {authMode === "login" ? (
+ <>
+ Login
+ {
+ setShowAuthForms(false);
+ setAuthMode("login");
+ }} />
+
+ Don't have an account?{" "}
+ setAuthMode("register")}>
+ Sign up here
+
+
+ >
+ ) : (
+ <>
+ Register
+ {
+ setShowAuthForms(false);
+ setAuthMode("login");
+ }} />
+
+ Already have an account?{" "}
+ setAuthMode("login")}>
+ Login here
+
+
+ >
+ )}
+
+
+ )}
+
+ {/* Form is always visible for everyone */}
+
+ {!loading && posting && }
+
+ {editingId && user ? (
+ m._id === editingId)}
+ onSave={(id, fields) => {
+ saveMessage(id, fields);
+ setEditingId(null);
+ }}
+ onCancel={() => setEditingId(null)}
+ />
+ ) : (
+ setEditingId(id) : null} // Only logged in can edit
+ currentUserId={user?.id}
+ />
+ )}
+
+
+
+ >
+ );
+};
diff --git a/src/App.styles.js b/src/App.styles.js
new file mode 100644
index 00000000..48695160
--- /dev/null
+++ b/src/App.styles.js
@@ -0,0 +1,125 @@
+import styled from "styled-components";
+
+export const AppContainer = styled.div`
+ width: 100%;
+ max-width: 500px;
+ margin: 0 auto;
+`;
+
+export const PageTitle = styled.h1`
+ text-align: center;
+ margin: 20px 0;
+ color: #333333;
+`;
+
+export const WelcomeSection = styled.div`
+ text-align: center;
+ margin-bottom: 30px;
+ padding: 0 20px;
+`;
+
+export const WelcomeTitle = styled.h2`
+ font-size: 18px;
+ font-weight: normal;
+ color: #333;
+ line-height: 1.5;
+ font-family: inherit;
+`;
+
+export const WelcomeText = styled.p`
+ font-size: 14px;
+ color: #666;
+ line-height: 1.6;
+ margin-top: 10px;
+ font-family: inherit;
+`;
+
+export const LoginButton = styled.button`
+ margin-top: 15px;
+ background: transparent;
+ border: 1px solid #333;
+ border-radius: 20px;
+ padding: 6px 16px;
+ cursor: pointer;
+ font-size: 14px;
+ font-family: inherit;
+ color: #333;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: #f5f5f5;
+ }
+`;
+
+export const UserSection = styled.div`
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 16px;
+`;
+
+export const UserInfo = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 10px;
+`;
+
+export const UserEmail = styled.span`
+ font-size: 14px;
+ color: #666;
+`;
+
+export const LogoutButton = styled.button`
+ background: transparent;
+ border: 1px solid #333;
+ border-radius: 20px;
+ padding: 6px 16px;
+ cursor: pointer;
+ font-size: 14px;
+ font-family: inherit;
+ color: #333;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: #f5f5f5;
+ }
+`;
+
+export const AuthFormSection = styled.section`
+ max-width: 450px;
+ margin: 5px auto 30px auto;
+`;
+
+export const AuthFormBox = styled.div`
+ padding: 20px;
+ border: 1px solid #333;
+ box-shadow: 7px 7px 0px rgba(0, 0, 0, 1);
+ background: white;
+ box-sizing: border-box;
+ width: 100%;
+`;
+
+export const AuthFormTitle = styled.h2`
+ margin-bottom: 15px;
+`;
+
+export const ToggleText = styled.p`
+ text-align: center;
+ margin-top: 20px;
+ font-size: 14px;
+ color: #666;
+`;
+
+export const ToggleLink = styled.button`
+ background: none;
+ border: none;
+ color: #333;
+ text-decoration: underline;
+ cursor: pointer;
+ font-size: 14px;
+ font-family: inherit;
+ padding: 0;
+
+ &:hover {
+ color: #000;
+ }
+`;
\ No newline at end of file
diff --git a/src/GlobalStyles.jsx b/src/GlobalStyles.jsx
new file mode 100644
index 00000000..e062f6db
--- /dev/null
+++ b/src/GlobalStyles.jsx
@@ -0,0 +1,31 @@
+import { createGlobalStyle } from "styled-components";
+
+export const GlobalStyles = createGlobalStyle`
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ display: flex;
+ justify-content: center;
+ background-color:rgba(224, 203, 200, 0.23);
+ font-family: roboto mono;
+}
+
+ h1 {
+ display: flex;
+ justify-content: center;
+ font-family: verdana;
+ margin: 20px 0 16px;
+ font-size: 48px;
+ color:#FF5364
+ }
+
+ p {
+ line-height: 1.6;
+ margin: 0 0 16px;
+ font-size: 17px;
+ }
+`;
diff --git a/src/api/auth.js b/src/api/auth.js
new file mode 100644
index 00000000..1c6bf110
--- /dev/null
+++ b/src/api/auth.js
@@ -0,0 +1,28 @@
+// src/api/auth.js
+const API_BASE = import.meta.env.VITE_API_URL;
+
+export const registerUser = async ({ email, password }) => {
+ const res = await fetch(`${API_BASE}/auth/register`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ email, password }),
+ });
+ if (!res.ok) {
+ const err = await res.json();
+ throw new Error(err.message || "Register failed");
+ }
+ return res.json();
+};
+
+export const loginUser = async ({ email, password }) => {
+ const res = await fetch(`${API_BASE}/auth/login`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ email, password }),
+ });
+ if (!res.ok) {
+ const err = await res.json();
+ throw new Error(err.message || "Login failed");
+ }
+ return res.json();
+};
diff --git a/src/api/thoughts.js b/src/api/thoughts.js
new file mode 100644
index 00000000..fe24bd97
--- /dev/null
+++ b/src/api/thoughts.js
@@ -0,0 +1,85 @@
+// src/api/thoughts.js
+const BASE_URL = import.meta.env.VITE_API_URL;
+
+// HÀmtar inloggnings-token frÄn localStorage
+function getAuthHeader() {
+ const token = localStorage.getItem("token");
+ return token ? { Authorization: `Bearer ${token}` } : {};
+}
+
+// GET alla tankar (öppet endpoint)
+export const fetchThoughts = async () => {
+ const res = await fetch(`${BASE_URL}/thoughts`);
+ if (!res.ok) throw new Error("Failed to fetch thoughts");
+ return res.json();
+};
+
+// POST ny tanke (fungerar för alla, med eller utan token)
+export const postThought = async (message) => {
+ const res = await fetch(`${BASE_URL}/thoughts`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...getAuthHeader(), // Includes token if user is logged in
+ },
+ body: JSON.stringify({ message }),
+ });
+ if (!res.ok) {
+ const err = await res.json();
+ throw new Error(err.error || "Failed to post message");
+ }
+ return res.json();
+};
+
+// PATCH uppdatera tanke (krÀver token)
+export const updateThought = async (id, updatedFields) => {
+ const res = await fetch(`${BASE_URL}/thoughts/${id}`, {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ ...getAuthHeader(),
+ },
+ body: JSON.stringify(updatedFields),
+ });
+ if (!res.ok) {
+ const err = await res.json();
+ throw new Error(err.error || "Failed to update message");
+ }
+ return res.json();
+};
+
+// PATCH gilla tanke (fungerar för alla)
+export const likeThought = async (id) => {
+ const res = await fetch(`${BASE_URL}/thoughts/${id}/like`, {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ ...getAuthHeader(), // Includes token if user is logged in
+ },
+ });
+ if (!res.ok) throw new Error("Failed to like message");
+ return res.json();
+};
+
+// PATCH ogilla tanke (fungerar för alla)
+export const unlikeThought = async (id) => {
+ const res = await fetch(`${BASE_URL}/thoughts/${id}/unlike`, {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ ...getAuthHeader(), // Includes token if user is logged in
+ },
+ });
+ if (!res.ok) throw new Error("Failed to unlike message");
+ return res.json();
+};
+
+// DELETE tanke (krÀver token)
+export const deleteThought = async (id) => {
+ const res = await fetch(`${BASE_URL}/thoughts/${id}`, {
+ method: "DELETE",
+ headers: getAuthHeader(),
+ });
+ if (!res.ok) throw new Error("Failed to delete message");
+ return res.json();
+};
diff --git a/src/components/AuthForm.styles.js b/src/components/AuthForm.styles.js
new file mode 100644
index 00000000..fb8b3e97
--- /dev/null
+++ b/src/components/AuthForm.styles.js
@@ -0,0 +1,47 @@
+import styled from "styled-components";
+
+export const AuthForm = styled.form`
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+`;
+
+export const ErrorMessage = styled.p`
+ color: red;
+ margin: 0;
+ font-size: 14px;
+`;
+
+export const Input = styled.input`
+ padding: 8px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ font-size: 14px;
+ font-family: inherit;
+
+ &:focus {
+ outline: none;
+ border-color: #333;
+ }
+`;
+
+export const SubmitButton = styled.button`
+ padding: 10px 20px;
+ background: rgb(255, 166, 178);
+ color: black;
+ border: none;
+ border-radius: 25px;
+ cursor: pointer;
+ font-size: 16px;
+ font-family: inherit;
+ font-weight: 500;
+ transition: background 0.2s ease;
+
+ &:hover {
+ background: pink;
+ }
+
+ &:active {
+ transform: translateY(1px);
+ }
+`;
\ No newline at end of file
diff --git a/src/components/EditThought.jsx b/src/components/EditThought.jsx
new file mode 100644
index 00000000..3248edcc
--- /dev/null
+++ b/src/components/EditThought.jsx
@@ -0,0 +1,33 @@
+import React, { useState } from "react";
+
+export function EditThought({ thought, onSave, onCancel }) {
+ const [message, setMessage] = useState(thought.message);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ // Invoke the onSave callback provided by parent
+ await onSave(thought._id, { message });
+ };
+
+ return (
+
+ );
+}
diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx
new file mode 100644
index 00000000..c0750c78
--- /dev/null
+++ b/src/components/Footer.jsx
@@ -0,0 +1,44 @@
+import styled from "styled-components";
+import { HeartIcon } from "./HeartIcon";
+
+const FooterWrapper = styled.footer`
+ text-align: center;
+ font-size: 14px;
+ color: #666;
+ margin: 50px auto 20px;
+ padding: 10px;
+`;
+
+const FooterLink = styled.a`
+ color: inherit;
+ text-decoration: none;
+
+ &:hover {
+ color: #000;
+ }
+`;
+
+const IconInline = styled(HeartIcon)`
+ margin: 0 4px;
+ vertical-align: middle;
+`;
+
+export const Footer = ({ likeCount }) => {
+ return (
+
+
+ Youâve {likeCount} thought{likeCount !== 1 ? "s" : ""}
+
+
+ © 2025{" "}
+
+ Cathi
+
+
+
+ );
+};
diff --git a/src/components/Form.jsx b/src/components/Form.jsx
new file mode 100644
index 00000000..9e83f2b0
--- /dev/null
+++ b/src/components/Form.jsx
@@ -0,0 +1,94 @@
+import { useState } from "react";
+import { HeartIcon } from "./HeartIcon";
+import {
+ FormWrapper,
+ StyledForm,
+ StyledLabel,
+ StyledTextarea,
+ StyledInfoCharacterText,
+ StyledButton,
+ StyledErrorMessage,
+} from "./Form.styles";
+
+export const Form = ({ onSubmitMessage }) => {
+ const [message, setMessage] = useState("");
+ const [error, setError] = useState("");
+ const maxLength = 140;
+ const isTooLong = message.length > maxLength;
+
+ 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("");
+
+ onSubmitMessage(message);
+ setMessage("");
+ };
+
+ const handleInputChange = (e) => {
+ const newMessage = e.target.value;
+ setMessage(newMessage);
+
+ if (newMessage.length === 0) {
+ setError("");
+ } else if (newMessage.length < 5) {
+ setError("Your message is too short.");
+ } else if (newMessage.length > maxLength) {
+ setError("Your message is too long.");
+ } else {
+ setError("");
+ }
+ };
+
+ const handleKeyDown = (e) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ handleSubmit(e);
+ }
+ };
+
+ const errorMessageElement = error ? (
+ {error}
+ ) : null;
+
+
+ return (
+
+
+
+ What's making you happy right now?
+
+
+
+
+
+ {maxLength - message.length} characters remaining
+
+
+ {errorMessageElement}
+
+
+ Send Happy Thought
+
+
+
+ );
+};
diff --git a/src/components/Form.styles.js b/src/components/Form.styles.js
new file mode 100644
index 00000000..bd51a285
--- /dev/null
+++ b/src/components/Form.styles.js
@@ -0,0 +1,75 @@
+import styled from "styled-components";
+
+export const FormWrapper = styled.section`
+ max-width: 450px;
+ margin: 5px auto;
+`;
+
+export const StyledForm = styled.form`
+ display: flex;
+ flex-direction: column;
+ 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`
+ color: rgb(0, 0, 0);
+ font-size: 16px;
+ margin: 0px 0px 10px;
+`;
+
+export const StyledTextarea = styled.textarea`
+ outline: none;
+ width: 100%;
+ border: 1px solid #cccccc;
+ padding: 15px;
+ font-size: 15px;
+ font-family: inherit;
+ resize: none;
+
+ &::placeholder {
+ color: #aaa;
+ font-size: 13px;
+ }
+
+ &:focus {
+ border: 2px solid pink;
+ }
+`;
+
+export const StyledInfoCharacterText = styled.p`
+ font-size: 14px;
+ margin: 0px;
+ color: ${(props) => (props.$exceedsLimit ? "red" : "#333333")};
+`;
+
+export const StyledButton = styled.button`
+ display: flex;
+ width: 250px;
+ font-family: inherit;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ padding: 10px 16px;
+ border: none;
+ border-radius: 25px;
+ background-color: rgb(255, 166, 178);
+ color: black;
+ font-size: 16px;
+ cursor: pointer;
+ margin: 10px 0px;
+
+ &:hover {
+ background-color: pink;
+ }
+`;
+
+export const StyledErrorMessage = styled.p`
+ color: red;
+ margin: 0px;
+ font-size: 14px;
+ font-weight: 500;
+`;
diff --git a/src/components/HeartIcon.jsx b/src/components/HeartIcon.jsx
new file mode 100644
index 00000000..da822a9d
--- /dev/null
+++ b/src/components/HeartIcon.jsx
@@ -0,0 +1,11 @@
+import styled from "styled-components";
+import { HiHeart } from "react-icons/hi";
+
+export const HeartIcon = styled(HiHeart)`
+ color: red;
+ transition: transform 0.3s ease;
+
+ &:hover {
+ transform: scale(1.2);
+ }
+`;
diff --git a/src/components/LikeButton.jsx b/src/components/LikeButton.jsx
new file mode 100644
index 00000000..aeb15ee8
--- /dev/null
+++ b/src/components/LikeButton.jsx
@@ -0,0 +1,47 @@
+import styled from "styled-components";
+import { HeartIcon } from "./HeartIcon";
+
+const LikeContainer = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 6px;
+`;
+
+const HeartWrapper = styled.button`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ background-color: ${(props) => (props.$liked ? "#FFA6B2" : "#eaeaea")};
+
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ transition: transform 0.2s ease;
+`;
+
+const LikeCount = styled.span`
+ font-size: 14px;
+ color: #333333;
+ font-family: Arial, sans-serif;
+`;
+
+export const LikeButton = ({ hearts, onClick, isLiked }) => {
+ 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/LoginForm.jsx b/src/components/LoginForm.jsx
new file mode 100644
index 00000000..9afc1f71
--- /dev/null
+++ b/src/components/LoginForm.jsx
@@ -0,0 +1,45 @@
+import React, { useState, useContext } from "react";
+import { loginUser } from "../api/auth";
+import { AuthContext } from "../context/AuthContext";
+import { AuthForm, ErrorMessage, Input, SubmitButton } from "./AuthForm.styles";
+
+export function LoginForm({ onSuccess }) {
+ const { login } = useContext(AuthContext);
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [error, setError] = useState("");
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ try {
+ const res = await loginUser({ email, password });
+ login(res);
+ if (onSuccess) onSuccess();
+ } catch (err) {
+ setError(err.message);
+ }
+ };
+
+ return (
+
+ {error && {error}}
+ setEmail(e.target.value)}
+ required
+ />
+ setPassword(e.target.value)}
+ required
+ />
+
+ Login
+
+
+ );
+}
diff --git a/src/components/Logo.jsx b/src/components/Logo.jsx
new file mode 100644
index 00000000..c187a351
--- /dev/null
+++ b/src/components/Logo.jsx
@@ -0,0 +1,11 @@
+import styled from "styled-components";
+
+const StyledImage = styled.img`
+ width: 150px;
+ margin: 60px auto 10px;
+ display: block;
+`;
+
+export const Logo = () => {
+ return ;
+};
\ No newline at end of file
diff --git a/src/components/MessageCard.jsx b/src/components/MessageCard.jsx
new file mode 100644
index 00000000..fe1cb51d
--- /dev/null
+++ b/src/components/MessageCard.jsx
@@ -0,0 +1,156 @@
+import styled, { keyframes } from "styled-components";
+import { LikeButton } from "./LikeButton";
+import { getLikedThoughts } from "../utils/localLikes";
+
+const CardFadeIn = keyframes`
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+`;
+
+const CardWrapper = styled.section`
+ max-width: 450px;
+ margin: 5px auto;
+`;
+
+const Card = styled.div`
+ display: flex;
+ flex-direction: column;
+ background: rgb(255, 255, 255);
+ width: 100%;
+ border: 1px solid #333333;
+ padding: 10px 16px;
+ box-shadow: 7px 7px 0px rgb(0, 0, 0);
+ margin: 30px 0;
+ animation: ${CardFadeIn} 0.5s ease-out;
+ word-break: break-word;
+`;
+
+const BottomRow = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 16px;
+`;
+
+const LeftPart = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 8px;
+`;
+
+const RightPart = styled.div`
+ font-size: 14px;
+ font-family: Arial, sans-serif;
+ color: #333333;
+`;
+
+const MessageText = styled.p`
+ align-self: start;
+ font-weight: 500;
+ margin: 0;
+ padding-right: 30px;
+`;
+
+const Timestamp = styled.small`
+ font-size: 14px;
+ font-family: arial;
+ color: #333333;
+ align-self: flex-end;
+ margin: 10px;
+`;
+
+const ActionButton = styled.button`
+ background: transparent;
+ border: 1px solid #ccc;
+ border-radius: 16px;
+ padding: 4px 12px;
+ font-size: 12px;
+ font-family: Arial, sans-serif;
+ color: #666;
+ cursor: pointer;
+ margin-left: 8px;
+ transition: all 0.2s ease;
+
+ &:hover {
+ border-color: #333;
+ color: #333;
+ background: #f5f5f5;
+ }
+
+ &:active {
+ background: #e0e0e0;
+ }
+`;
+
+const getTimeAgo = (date) => {
+ const now = new Date();
+ const then = new Date(date);
+ 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,
+ onDelete,
+ onEdit,
+ currentUserId,
+}) => {
+ // OwnerâID if available
+ const ownerId = message.user;
+
+ // Check if logged-in user is the owner - convert ObjectId to string
+ const isOwner = currentUserId === String(ownerId);
+
+ const likedThoughts = getLikedThoughts();
+ const isLiked = !!likedThoughts[message._id];
+
+ return (
+
+
+ {message.message}
+
+
+ onLike(message._id)}
+ isLiked={isLiked}
+ />
+
+ {/* Conditional render of Delete/Edit */}
+ {isOwner && (
+ <>
+ onEdit(message._id)} title="Edit">
+ Edit
+
+ onDelete(message._id)} title="Delete">
+ Delete
+
+ >
+ )}
+
+ {getTimeAgo(message.createdAt)}
+
+
+
+ );
+};
diff --git a/src/components/MessageList.jsx b/src/components/MessageList.jsx
new file mode 100644
index 00000000..9200a987
--- /dev/null
+++ b/src/components/MessageList.jsx
@@ -0,0 +1,27 @@
+import { MessageCard } from "./MessageCard";
+import { Loader } from "./Loader";
+
+export const MessageList = ({ messages = [], loading, onLike, onDelete, onEdit, currentUserId }) => {
+ if (loading) {
+ return ;
+ }
+
+ const sortedMessages = [...messages].sort(
+ (a, b) => new Date(b.createdAt) - new Date(a.createdAt)
+ );
+
+ return (
+ <>
+ {sortedMessages.map((msg) => (
+ onEdit(msg._id)}
+ currentUserId={currentUserId}
+ />
+ ))}
+ >
+ );
+};
diff --git a/src/components/RegisterForm.jsx b/src/components/RegisterForm.jsx
new file mode 100644
index 00000000..aeaa0e7e
--- /dev/null
+++ b/src/components/RegisterForm.jsx
@@ -0,0 +1,46 @@
+import React, { useState, useContext } from "react";
+import { registerUser } from "../api/auth";
+import { AuthContext } from "../context/AuthContext";
+import { AuthForm, ErrorMessage, Input, SubmitButton } from "./AuthForm.styles";
+
+export function RegisterForm({ onSuccess }) {
+ const { login } = useContext(AuthContext);
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [error, setError] = useState("");
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ try {
+ const res = await registerUser({ email, password });
+ login(res);
+ if (onSuccess) onSuccess();
+ } catch (err) {
+ setError(err.message);
+ }
+ };
+
+ return (
+
+ {error && {error}}
+ setEmail(e.target.value)}
+ required
+ />
+ setPassword(e.target.value)}
+ required
+ />
+
+ Register
+
+
+ );
+}
diff --git a/src/context/AuthContext.jsx b/src/context/AuthContext.jsx
new file mode 100644
index 00000000..ce629479
--- /dev/null
+++ b/src/context/AuthContext.jsx
@@ -0,0 +1,30 @@
+import React, { createContext, useState, useEffect } from "react";
+
+export const AuthContext = createContext(null);
+
+export function AuthProvider({ children }) {
+ const [user, setUser] = useState(null);
+
+ useEffect(() => {
+ const stored = localStorage.getItem("user");
+ if (stored) setUser(JSON.parse(stored));
+ }, []);
+
+ const login = ({ token, user }) => {
+ localStorage.setItem("token", token);
+ localStorage.setItem("user", JSON.stringify(user));
+ setUser(user);
+ };
+
+ const logout = () => {
+ localStorage.removeItem("token");
+ localStorage.removeItem("user");
+ setUser(null);
+ };
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/hooks/useThoughts.js b/src/hooks/useThoughts.js
new file mode 100644
index 00000000..5a198082
--- /dev/null
+++ b/src/hooks/useThoughts.js
@@ -0,0 +1,119 @@
+// src/hooks/useThoughts.js
+import { useState, useEffect } from "react";
+import {
+ fetchThoughts,
+ postThought,
+ likeThought,
+ unlikeThought,
+ deleteThought,
+ updateThought,
+} from "../api/thoughts";
+import {
+ getLikedThoughts,
+ saveLikedThought,
+ removeLikedThought,
+ getLikeCount,
+} from "../utils/localLikes";
+
+export function useThoughts() {
+ const [messages, setMessages] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [posting, setPosting] = useState(false);
+ const [likeCount, setLikeCount] = useState(getLikeCount());
+
+ // 1) HÀmta listan frÄn backend
+ const getMessages = async () => {
+ setLoading(true);
+ try {
+ const data = await fetchThoughts();
+ setMessages(data.results);
+ } catch (err) {
+ console.error("Fetch failed", err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ getMessages();
+ }, []);
+
+ // 2) Posta en ny tanke
+ const addMessage = async (message) => {
+ const optimistic = {
+ _id: Date.now().toString(),
+ message,
+ hearts: 0,
+ createdAt: new Date().toISOString(),
+ };
+ setMessages((prev) => [optimistic, ...prev]);
+ setPosting(true);
+
+ try {
+ await postThought(message);
+ await getMessages();
+ } catch (err) {
+ console.error("Post failed", err);
+ } finally {
+ setPosting(false);
+ }
+ };
+
+ // 3) Gilla/ogilla
+ const toggleLike = async (id) => {
+ const liked = getLikedThoughts();
+ const isAlready = liked[id];
+
+ setMessages((prev) =>
+ prev.map((m) =>
+ m._id === id ? { ...m, hearts: m.hearts + (isAlready ? -1 : 1) } : m
+ )
+ );
+
+ try {
+ if (isAlready) {
+ await unlikeThought(id);
+ removeLikedThought(id);
+ } else {
+ await likeThought(id);
+ saveLikedThought(id);
+ }
+ setLikeCount(getLikeCount());
+ } catch (err) {
+ console.error("Like toggle failed", err);
+ }
+ };
+
+ // 4) Radera en tanke
+ const removeMessage = async (id) => {
+ try {
+ await deleteThought(id);
+ setMessages((prev) => prev.filter((m) => m._id !== id));
+ } catch (err) {
+ console.error("Delete failed", err);
+ }
+ };
+
+ // 5) Uppdatera en tanke
+ const saveMessage = async (id, fields) => {
+ try {
+ await updateThought(id, fields);
+ setMessages((prev) =>
+ prev.map((m) => (m._id === id ? { ...m, ...fields } : m))
+ );
+ } catch (err) {
+ console.error("Update failed", err);
+ }
+ };
+
+ return {
+ messages,
+ loading,
+ posting,
+ likeCount,
+ addMessage,
+ toggleLike,
+ removeMessage,
+ saveMessage,
+ };
+}
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..291a727b 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -1,12 +1,12 @@
-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';
+import { AuthProvider } from './context/AuthContext';
ReactDOM.createRoot(document.getElementById('root')).render(
-
+
+
+
-)
+);
\ No newline at end of file
diff --git a/src/utils/localLikes.js b/src/utils/localLikes.js
new file mode 100644
index 00000000..676a4498
--- /dev/null
+++ b/src/utils/localLikes.js
@@ -0,0 +1,24 @@
+// HĂ€mta alla gillade thoughts
+export const getLikedThoughts = () => {
+ const data = localStorage.getItem("likedThoughts");
+ return data ? JSON.parse(data) : {};
+};
+
+// Spara en ny like
+export const saveLikedThought = (id) => {
+ const current = getLikedThoughts();
+ const updated = { ...current, [id]: true };
+ localStorage.setItem("likedThoughts", JSON.stringify(updated));
+};
+
+// Ta bort en like
+export const removeLikedThought = (id) => {
+ const current = getLikedThoughts();
+ delete current[id];
+ localStorage.setItem("likedThoughts", JSON.stringify(current));
+};
+
+// RĂ€kna antalet likes
+export const getLikeCount = () => {
+ return Object.keys(getLikedThoughts()).length;
+};