Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create note #4

Merged
merged 7 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +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-red">
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-red">
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
3 changes: 2 additions & 1 deletion tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ module.exports = {
gray: "#6c6868",
lightGray: "#c4c2c2",
error: "#ff0000",
neutral: "#E5E5E5"
neutral: "#E5E5E5",
red: "#ff0000"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is already a color here with the same hex code. So you can reference the color error instead of red

},
fontSize: {
xs: ".75rem",
Expand Down
Loading