diff --git a/README.md b/README.md index 41ebece2..325531bd 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ # Happy Thoughts +Link: https://think-happy.netlify.app/ \ No newline at end of file diff --git a/index.html b/index.html index d4492e94..30aea829 100644 --- a/index.html +++ b/index.html @@ -1,16 +1,27 @@ - - - - - Happy Thoughts - - -
- - - + + + + + + Happy Thoughts + + + +
+ + + + \ No newline at end of file diff --git a/package.json b/package.json index 2f66d295..5dacda00 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,14 @@ "preview": "vite preview" }, "dependencies": { + "@tailwindcss/vite": "^4.1.4", + "date-fns": "^4.1.0", + "motion": "^12.10.4", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-router": "^7.6.2", + "react-router-dom": "^7.6.2", + "tailwindcss": "^4.1.4" }, "devDependencies": { "@eslint/js": "^9.21.0", diff --git a/public/assets/github-btn.svg b/public/assets/github-btn.svg new file mode 100644 index 00000000..058f5ed7 --- /dev/null +++ b/public/assets/github-btn.svg @@ -0,0 +1,44 @@ + + + + + + + + diff --git a/public/assets/linkedin-btn.svg b/public/assets/linkedin-btn.svg new file mode 100644 index 00000000..bf017f96 --- /dev/null +++ b/public/assets/linkedin-btn.svg @@ -0,0 +1,44 @@ + + + + + + + + diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 00000000..4e8bc847 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..9c8e1f8a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,43 @@ -export const App = () => { +import { BrowserRouter, Routes, Route } from 'react-router' +import { AuthProvider } from './context/AuthContext' +import NavBar from "./sections/NavBar" +import Footer from "./sections/Footer" +import Home from './pages/Home' +import LogIn from './pages/LogIn' +import Register from './pages/Register' +import MyThoughts from './pages/MyThoughts' +import NotFound from './pages/NotFound' +import ProtectedPage from './pages/ProtectedPage' + +const App = () => { return ( -

Happy Thoughts

+ + +
+ +
+ + } /> + } /> + } /> + + + + } + /> + } /> + +
+
+
+
) } + +export default App diff --git a/src/components/ArrowButton.jsx b/src/components/ArrowButton.jsx new file mode 100644 index 00000000..cc33d6cd --- /dev/null +++ b/src/components/ArrowButton.jsx @@ -0,0 +1,16 @@ +const ArrowButton = ({ icon, isActive, onClick, ariaLabel }) => { + return ( + + ) +} + +export default ArrowButton + diff --git a/src/components/CancelButton.jsx b/src/components/CancelButton.jsx new file mode 100644 index 00000000..5c708257 --- /dev/null +++ b/src/components/CancelButton.jsx @@ -0,0 +1,15 @@ +const CancelButton = ({ onClick }) => { + return ( + + ) +} + +export default CancelButton \ No newline at end of file diff --git a/src/components/Error.jsx b/src/components/Error.jsx new file mode 100644 index 00000000..2c573463 --- /dev/null +++ b/src/components/Error.jsx @@ -0,0 +1,9 @@ +const Error = ({ text }) => { + return ( +
+

{text}

+
+ ) +} + +export default Error \ No newline at end of file diff --git a/src/components/FilterOption.jsx b/src/components/FilterOption.jsx new file mode 100644 index 00000000..b293dfa3 --- /dev/null +++ b/src/components/FilterOption.jsx @@ -0,0 +1,7 @@ +const FilterOption = ({ value }) => { + return ( + + ) +} + +export default FilterOption \ No newline at end of file diff --git a/src/components/LikeButton.jsx b/src/components/LikeButton.jsx new file mode 100644 index 00000000..ad957a5e --- /dev/null +++ b/src/components/LikeButton.jsx @@ -0,0 +1,14 @@ +const LikeButton = ({ likes, onLike }) => { + return ( + + ) +} + +export default LikeButton \ No newline at end of file diff --git a/src/components/ListControls.jsx b/src/components/ListControls.jsx new file mode 100644 index 00000000..adb25cd8 --- /dev/null +++ b/src/components/ListControls.jsx @@ -0,0 +1,66 @@ +import { useState } from "react" +import RadioOption from "./RadioOption" +import FilterOption from "./FilterOption" + +const ListControls = ({ sortBy, filterOn, onSort, onFilter }) => { + const [sorting, setSorting] = useState(sortBy) + const [filter, setFilter] = useState(filterOn) + + const sortOptions = [ + { id: "createdAt", label: "Recent" }, + { id: "hearts", label: "Popular" } + ] + const filterOptions = ["all", "travel", "food", "family", "friends", "humor", "nature", "wellness", "home", "entertainment", "work", "other"] + + const onSortingChange = (event) => { + setSorting(event.target.value) + onSort(event.target.value) + } + + const onFilterChange = (event) => { + setFilter(event.target.value) + onFilter(event.target.value) + } + + return ( +
+
+ +
+ Sort on: +
+ {sortOptions.map(({ id, label }) => ( + + ))} +
+
+ +
+ + +
+
+
+ ) +} + +export default ListControls \ No newline at end of file diff --git a/src/components/Loader.jsx b/src/components/Loader.jsx new file mode 100644 index 00000000..78f3d0c6 --- /dev/null +++ b/src/components/Loader.jsx @@ -0,0 +1,10 @@ +const Loader = () => { + return ( +
+
+

Loading thoughts...

+
+ ) +} + +export default Loader \ No newline at end of file diff --git a/src/components/MessageCard.jsx b/src/components/MessageCard.jsx new file mode 100644 index 00000000..8005532f --- /dev/null +++ b/src/components/MessageCard.jsx @@ -0,0 +1,210 @@ +import { useState } from "react" +import { formatDistance } from "date-fns" +import { useAuth } from "../context/AuthContext" +import MessageCardButton from "./MessageCardButton" +import LikeButton from "./LikeButton" +import Tag from "./Tag" +import SubmitButton from "./SubmitButton" +import CancelButton from "./CancelButton" + +const MessageCard = ({ message, onFilter, update }) => { + const { token } = useAuth() + const [error, setError] = useState("") + const [editedMessage, setEditedMessage] = useState("") + const [showInput, setShowInput] = useState(false) + const maxCharacters = 140 + const minCharacters = 5 + const url = "https://think-happy-api.onrender.com/thoughts" + // const url = "http://localhost:8080/thoughts" // local api + + const likeMessage = async () => { + try { + setError("") + const response = await fetch(`${url}/${message._id}/like`, { + method: "PATCH", + headers: { "Authorization": token } + }) + if (response.ok) { + const likedMessage = await response.json() + update((messages) => messages.map(m => + m._id === message._id ? { ...m, hearts: likedMessage.response.hearts } : m + )) + } + } catch (error) { + setError(`An error occured when liking thought: ${error.message}`) + } finally { + // + } + } + + const deleteMessage = async () => { + try { + setError("") + const response = await fetch(`${url}/${message._id}`, { + method: "DELETE", + headers: { "Authorization": token } + }) + const data = await response.json() + if (!response.ok) { + if (response.status === 401) { + setError("You need to be logged in to delete a thought.") + return + } + if (response.status === 404) { + setError("You are only allowed to delete your own thoughts.") + return + } + if (response.status === 500) { + setError("Server error. Please try again later.") + return + } + setError("Deleting thought failed." + data.message) + } + const deletedMessage = data.response + update((messages) => messages.filter(m => + m._id !== deletedMessage._id + )) + + } catch (error) { + setError(`An error occured when deleting thought: ${error.message}`) + } + } + + const editMessage = async (newMessage) => { + try { + setError("") + const response = await fetch(`${url}/${message._id}`, { + method: "PATCH", + body: JSON.stringify({ + message: newMessage, + }), + headers: { + "Content-Type": "application/json", + "Authorization": token + } + }) + const data = await response.json() + if (!response.ok) { + if (response.status === 401) { + setError("You need to be logged in to edit a thought.") + return + } + if (response.status === 404) { + setError("You are only allowed to edit your own thoughts.") + return + } + if (response.status === 500) { + setError("Server error. Please try again later.") + return + } + setError("Editing thought failed." + data.message) + } + const editedMessage = data.response + update((messages) => messages.map(m => + m._id === message._id ? { ...m, message: editedMessage.message } : m + )) + } catch (error) { + setError(`An error occured when editing thought: ${error.message}`) + } + } + + const onEdit = () => { + setShowInput(current => !current) + } + + const onCancel = () => { + setEditedMessage("") + setShowInput(false) + } + + const handleTyping = (event) => { + setEditedMessage(event.target.value) + } + + const onSubmitEditedMessage = (event) => { + event.preventDefault() + editMessage(editedMessage) + setEditedMessage("") + setShowInput(false) + } + + const handleKeyDown = (event) => { + if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault() + onSubmitEditedMessage(event) + } + } + + return ( +
+
+
+
+
    + {message.tags.map(tag => { + return + })} +
+
+ + +
+
+

+ {message.message} +

+ {showInput && +
+