Skip to content

Commit

Permalink
Merge pull request #4 from learn-academy-2024-alpha/create-note
Browse files Browse the repository at this point in the history
Create note
  • Loading branch information
Rlemus93 authored May 15, 2024
2 parents 67c388c + 7afc3ae commit 01b7757
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 13 deletions.
30 changes: 28 additions & 2 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,48 @@ const App = () => {
const loggedInUser = localStorage.getItem("user")
if (loggedInUser) {
setUser(JSON.parse(loggedInUser))
} else {
navigate("/")
}
}, [])

const signedInUser = (userData) => {
setUser(userData)
navigate("/main")
}

const signedOutUser = () => {
setUser(null)
navigate("/")
}

const createNote = async (newNote) => {
try {
const postResponse = await fetch("http://localhost:3000/notes", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(newNote)
})
console.log(postResponse)
if (!postResponse.ok) {
throw new Error("Server responded with status: " + postResponse.status)
}
await postResponse.json()
} catch (error) {
console.error("Error in createNote:", error)
alert("Oops something went wrong: " + error.message)
}
}

return (
<>
<Header signedOutUser={signedOutUser} />

<Header
user={user}
signedOutUser={signedOutUser}
createNote={createNote}
/>
<Routes>
<Route path="/" element={<Landing signedInUser={signedInUser} />} />
{user && <Route path="/main" element={<Main />} />}
Expand Down
2 changes: 0 additions & 2 deletions src/__tests__/Header.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ test("renders the header component", () => {

const headerLogo = screen.getByAltText(/black graphic of a note and a pencil/)
expect(headerLogo).toBeInTheDocument()
const editLogo = screen.getByAltText(/black graphic of a notepad/)
expect(editLogo).toBeInTheDocument()
const addUserLogo = screen.getByAltText(/black graphic of a add user button/)
expect(addUserLogo).toBeInTheDocument()
const binLogo = screen.getByAltText(/black graphic of a trash bin/)
Expand Down
86 changes: 86 additions & 0 deletions src/__tests__/NewModal.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from "react"
import { render, fireEvent, screen } from "@testing-library/react"
import { act } from "react"
import NewModal from "../components/NewModal"

const mockCreateNote = jest.fn()
const user = {
id: "user123"
}

describe("NewModal component tests", () => {
beforeEach(() => {
render(<NewModal createNote={mockCreateNote} user={user} />)
})

test("should toggle the modal on button click", () => {
const openModalButton = screen.getByAltText("Edit icon")

expect(screen.queryByText("Create a New Note")).not.toBeInTheDocument()

act(() => {
fireEvent.click(openModalButton)
})
expect(screen.getByText("Create a New Note")).toBeInTheDocument()

act(() => {
fireEvent.click(screen.getByRole("button", { name: "Close" }))
})
expect(screen.queryByText("Create a New Note")).not.toBeInTheDocument()
})

test("should handle form input and submit", async () => {
act(() => {
fireEvent.click(screen.getByAltText("Edit icon"))
})

await act(async () => {
fireEvent.submit(screen.getByText("Create Note"))
})
expect(screen.getByText("Title is required")).toBeInTheDocument()
expect(screen.getByText("Content is required")).toBeInTheDocument()

await act(async () => {
fireEvent.change(screen.getByLabelText("Title"), {
target: { value: "Test Title" }
})
fireEvent.change(screen.getByLabelText("Content"), {
target: { value: "Test Content" }
})
fireEvent.click(screen.getByLabelText("Public"))
})

expect(screen.getByLabelText("Title").value).toBe("Test Title")
expect(screen.getByLabelText("Content").value).toBe("Test Content")
expect(screen.getByLabelText("Public").checked).toBeTruthy()

await act(async () => {
fireEvent.submit(screen.getByText("Create Note"))
})

expect(mockCreateNote).toHaveBeenCalledWith({
title: "Test Title",
content: "Test Content",
public: true,
creator: "user123"
})

expect(screen.queryByText("Title is required")).not.toBeInTheDocument()
expect(screen.queryByText("Content is required")).not.toBeInTheDocument()
})

test("should display error when submitting empty form", async () => {
act(() => {
fireEvent.click(screen.getByAltText("Edit icon"))
})

await act(async () => {
fireEvent.submit(screen.getByText("Create Note"))
})

expect(screen.getByText("Title is required")).toBeInTheDocument()
expect(screen.getByText("Content is required")).toBeInTheDocument()

expect(screen.getByText("Create a New Note")).toBeInTheDocument()
})
})
10 changes: 3 additions & 7 deletions src/components/Header.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from "react"
import noteLogo from "../assets/note-logo.png"
import addUser from "../assets/add-user.png"
import edit from "../assets/edit.png"
import bin from "../assets/bin.png"
import logOut from "../assets/logout.png"
import NewModal from "./NewModal"

const Header = ({ signedOutUser }) => {
const Header = ({ signedOutUser, createNote, user }) => {
const signOut = async () => {
try {
const signOutResponse = await fetch("http://localhost:3000/logout", {
Expand Down Expand Up @@ -33,11 +33,7 @@ const Header = ({ signedOutUser }) => {
alt="black graphic of a note and a pencil"
className="mx-4 my-2 flex h-7 justify-start"
/>
<img
src={edit}
alt="black graphic of a notepad"
className="mx-4 my-2 h-7"
/>
<NewModal createNote={createNote} user={user} />
</div>
<div className="flex justify-end">
<img
Expand Down
146 changes: 146 additions & 0 deletions src/components/NewModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import React, { useReducer } from "react"
import { Button, Modal, Radio, Label, TextInput } from "flowbite-react"
import edit from "../assets/edit.png"

const initialState = {
openModal: false,
title: "",
content: "",
public: true,
errors: { title: false, content: false }
}

function reducer(state, action) {
switch (action.type) {
case "TOGGLE_MODAL":
return { ...state, openModal: !state.openModal }
case "SET_FIELD":
return { ...state, [action.field]: action.value }
case "SET_ERROR":
return {
...state,
errors: { ...state.errors, [action.field]: action.value }
}
default:
throw new Error("Unhandled action type: " + action.type)
}
}

const NewModal = ({ createNote, user }) => {
const [state, dispatch] = useReducer(reducer, initialState)

const handleInputChange = (field, value) => {
dispatch({ type: "SET_FIELD", field, value })
if (state.errors[field]) {
dispatch({ type: "SET_ERROR", field, value: false })
}
}

const handleFormSubmit = async (e) => {
e.preventDefault()
const { title, content, public: isPublic } = state
let errors = {}
if (!title) errors.title = true
if (!content) errors.content = true

if (Object.keys(errors).length) {
for (let error in errors) {
dispatch({ type: "SET_ERROR", field: error, value: true })
}
return
}

if (user && user.id) {
await createNote({
title,
content,
public: isPublic,
creator: user.id
})
dispatch({ type: "TOGGLE_MODAL" })
} else {
console.error("User data is not available")
}
}

return (
<>
<button onClick={() => dispatch({ type: "TOGGLE_MODAL" })}>
<img src={edit} alt="Edit icon" className="mx-4 my-2 h-7" />
</button>
<Modal
className="m-auto h-4/5 w-1/2 bg-gray "
show={state.openModal}
onClose={() => dispatch({ type: "TOGGLE_MODAL" })}
>
<Modal.Header className="m-3 text-xl font-semibold"></Modal.Header>
<h1 className="mb-3 text-center text-3xl font-semibold">
<u>Create a New Note</u>
</h1>
<form onSubmit={handleFormSubmit}>
<fieldset className=" p-10">
<div className="flex justify-center gap-4">
<Radio
id="public"
name="public"
value="true"
checked={state.public}
onChange={() => handleInputChange("public", true)}
/>
<Label htmlFor="public">Public</Label>
<Radio
id="private"
name="public"
value="false"
checked={!state.public}
onChange={() => handleInputChange("public", false)}
/>
<Label htmlFor="private">Private</Label>
</div>
</fieldset>
<Modal.Body>
<Label htmlFor="title" className="mb-2 block text-center">
Title
</Label>
<input
id="title"
type="text"
className="mx-auto flex w-64 rounded-lg border-2 p-4"
value={state.title}
onChange={(e) => handleInputChange("title", e.target.value)}
/>
{state.errors.title && (
<p className="mt-1 text-center text-xs text-error">
Title is required
</p>
)}

<Label htmlFor="content" className="mb-2 block text-center">
Content
</Label>
<textarea
className="mx-auto w-full rounded-lg border-2 p-4"
id="content"
rows="6"
value={state.content}
onChange={(e) => handleInputChange("content", e.target.value)}
/>
{state.errors.content && (
<p className="mt-1 text-center text-xs text-error">
Content is required
</p>
)}
</Modal.Body>
<Button
className="focus:ring-blue-300 text-black mx-auto mb-4 mt-4 flex w-1/2 justify-center rounded bg-lightGray px-4 py-2 font-bold shadow-lg transition duration-150 ease-in-out hover:bg-neutral focus:outline-none focus:ring-4 focus:ring-opacity-50"
type="submit"
>
Create Note
</Button>
</form>
</Modal>
</>
)
}

export default NewModal
4 changes: 2 additions & 2 deletions src/components/SignIn.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ const SignIn = ({ setFormStatus, signedInUser }) => {
const [error, setError] = useState(false)

const preloadedValues = {
email: "[email protected]",
password: "yrL4YgmuQ"
email: "[email protected]",
password: "FPjhxi8BeL7ov6Rl"
}
const {
register,
Expand Down

0 comments on commit 01b7757

Please sign in to comment.