summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--backend/src/main/java/com/blog/web/controllers/ArticleController.java2
-rw-r--r--backend/src/main/java/com/blog/web/dto/ArticleDto.java6
-rw-r--r--backend/src/main/java/com/blog/web/dto/ArticlePublicDto.java2
-rw-r--r--backend/src/main/java/com/blog/web/models/Article.java2
-rw-r--r--backend/src/main/java/com/blog/web/services/impl/ArticleServiceImpl.java22
-rw-r--r--frontend/src/pages/Home.tsx53
-rw-r--r--frontend/src/pages/articles/Article.tsx23
-rw-r--r--frontend/src/pages/articles/Edit.tsx142
-rw-r--r--frontend/src/routes/index.tsx12
9 files changed, 229 insertions, 35 deletions
diff --git a/backend/src/main/java/com/blog/web/controllers/ArticleController.java b/backend/src/main/java/com/blog/web/controllers/ArticleController.java
index b321cd2..e7890b0 100644
--- a/backend/src/main/java/com/blog/web/controllers/ArticleController.java
+++ b/backend/src/main/java/com/blog/web/controllers/ArticleController.java
@@ -78,7 +78,7 @@ public class ArticleController {
}
@PostMapping("/articles/edit/{articleId}")
- public String updateArticle(@PathVariable("articleId") Long articleId, @Valid @ModelAttribute("article") ArticleDto article, BindingResult result) {
+ public String updateArticle(@PathVariable("articleId") long articleId, @Valid @ModelAttribute("article") ArticleDto article, BindingResult result) {
if (result.hasErrors()) {
return "articles/edit";
}
diff --git a/backend/src/main/java/com/blog/web/dto/ArticleDto.java b/backend/src/main/java/com/blog/web/dto/ArticleDto.java
index 755b1f6..9e82c40 100644
--- a/backend/src/main/java/com/blog/web/dto/ArticleDto.java
+++ b/backend/src/main/java/com/blog/web/dto/ArticleDto.java
@@ -29,7 +29,7 @@ public class ArticleDto {
@JoinColumn(name = "created_by", nullable = false)
private UserEntity createdBy;
- public ArticleDto(long id, String title, String photoUrl, String content, UserEntity createdBy, LocalDateTime createdOn, LocalDateTime updatedOn) {
+ public ArticleDto(Long id, String title, String photoUrl, String content, UserEntity createdBy, LocalDateTime createdOn, LocalDateTime updatedOn) {
this.id = id;
this.title = title;
this.photoUrl = photoUrl;
@@ -111,4 +111,8 @@ public class ArticleDto {
public String getUsername() {
return createdBy.getUsername();
}
+
+ public Long getUserId() {
+ return createdBy.getId();
+ }
}
diff --git a/backend/src/main/java/com/blog/web/dto/ArticlePublicDto.java b/backend/src/main/java/com/blog/web/dto/ArticlePublicDto.java
index 5dac4fe..50dda43 100644
--- a/backend/src/main/java/com/blog/web/dto/ArticlePublicDto.java
+++ b/backend/src/main/java/com/blog/web/dto/ArticlePublicDto.java
@@ -13,7 +13,7 @@ public class ArticlePublicDto {
private LocalDateTime updatedOn;
private String createdBy;
- public ArticlePublicDto(long id, String title, String photoUrl, String content, String createdBy, LocalDateTime createdOn, LocalDateTime updatedOn) {
+ public ArticlePublicDto(Long id, String title, String photoUrl, String content, String createdBy, LocalDateTime createdOn, LocalDateTime updatedOn) {
this.id = id;
this.title = title;
this.photoUrl = photoUrl;
diff --git a/backend/src/main/java/com/blog/web/models/Article.java b/backend/src/main/java/com/blog/web/models/Article.java
index 78ad668..ed4ac1c 100644
--- a/backend/src/main/java/com/blog/web/models/Article.java
+++ b/backend/src/main/java/com/blog/web/models/Article.java
@@ -46,7 +46,7 @@ public class Article {
this.updatedOn = articleDto.getUpdatedOn();
}
- public long getId() {
+ public Long getId() {
return id;
}
diff --git a/backend/src/main/java/com/blog/web/services/impl/ArticleServiceImpl.java b/backend/src/main/java/com/blog/web/services/impl/ArticleServiceImpl.java
index 2f9de6c..04cc8be 100644
--- a/backend/src/main/java/com/blog/web/services/impl/ArticleServiceImpl.java
+++ b/backend/src/main/java/com/blog/web/services/impl/ArticleServiceImpl.java
@@ -61,13 +61,21 @@ public class ArticleServiceImpl implements ArticleService {
}
@Override
- public void updateArticle(ArticleDto articleDto) {
- final String username = SecurityUtil.getSessionUser();
- final UserEntity user = userRepository.findByUsername(username).orElse(null);
- if (user == null) {
- return;
- }
- final Article article = mapToArticle(articleDto);
+ public void updateArticle(ArticleDto newArticle) {
+ if(newArticle == null) { return; }
+ final Optional<ArticleDto> optExistingArticle = this.findArticleById(newArticle.getId());
+ if(optExistingArticle.isEmpty()) { return; } // cant find article, give up
+ final ArticleDto existingArticle = optExistingArticle.get();
+ Long ownerId = existingArticle.getUserId();
+
+ final Optional<UserEntity> optUser = userService.getLoggedInUser();
+ if (optUser.isEmpty()) { return; } // not logged in, not allowed to edit
+ final UserEntity user = optUser.get();
+ Long userId = user.getId();
+
+ if (!ownerId.equals(userId)) { return; } // logged in a different user, not allowed to edit
+
+ final Article article = mapToArticle(newArticle);
article.setCreatedBy(user);
articleRepository.save(article);
}
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx
index 71683e7..df9b4c1 100644
--- a/frontend/src/pages/Home.tsx
+++ b/frontend/src/pages/Home.tsx
@@ -5,6 +5,7 @@ type article = {
title: string;
photoUrl: string;
content: string;
+ createdBy: string;
createdOn: string;
updateOn: string;
};
@@ -14,11 +15,12 @@ type articleSearch = {
value: string | null;
};
-
export default function Home({
articleSearch,
+ username,
}: {
articleSearch: articleSearch;
+ username: string | null;
}) {
const [articles, setArticles] = useState<article[]>([]);
const [allArticles, setAllArticles] = useState<JSX.Element[]>([]);
@@ -40,12 +42,30 @@ export default function Home({
if (!response.ok) {
console.log(response);
alert("check console for error");
+ } else {
+ fetchArticles();
}
- else {
- fetchArticles();
- }
};
+ function renderButtons(article: article) {
+ return (
+ <div>
+ <a
+ href={`/article/edit/${article.id}`}
+ className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-2 ml-4 text-sm rounded"
+ >
+ Edit
+ </a>
+ <form onSubmit={handleDelete} method="post">
+ <input type="hidden" name="id" value={article.id} />
+ <button className="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 ml-4 text-sm rounded">
+ Delete
+ </button>
+ </form>
+ </div>
+ );
+ }
+
const fetchArticles = useCallback(() => {
let url;
if (articleSearch.value === null) {
@@ -68,7 +88,9 @@ export default function Home({
}, [articleSearch.value]);
// pull data when new search is given
- useEffect(() => { fetchArticles(); } , [articleSearch.value, fetchArticles]);
+ useEffect(() => {
+ fetchArticles();
+ }, [articleSearch.value, fetchArticles]);
// when new data is pulled update the articles shown
useEffect(() => {
@@ -80,7 +102,10 @@ export default function Home({
>
<div className="flex-1 bg-white rounded-t rounded-b-none overflow-hidden shadow-lg">
{/*th:href="@{/articles/{articleId}(articleId=${article.id})}"*/}
- <a className="flex flex-wrap no-underline hover:no-underline">
+ <a
+ href={`/article/${article.id}`}
+ className="flex flex-wrap no-underline hover:bg-sky-200 border-b-2 hover:no-underline"
+ >
<img
src={article.photoUrl}
className="h-full w-full rounded-t pb-6"
@@ -90,19 +115,7 @@ export default function Home({
</div>
<p className="text-gray-800 font-serif text-base px-6 mb-5"></p>
</a>
- {/*th:if="${user.id} == ${article.createdBy.id}"*/}
- <div></div>
- {/*th:href="@{/articles/edit/{articleId}(articleId=${article.id})}"*/}
- <a className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-2 ml-4 text-sm rounded">
- Edit
- </a>
- {/*th:href="@{/articles/delete/{articleId}(articleId=${article.id})}"*/}
- <form onSubmit={handleDelete} method="post">
- <input type="hidden" name="id" value={article.id}/>
- <button className="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 ml-4 text-sm rounded">
- Delete
- </button>
- </form>
+ {username == article?.createdBy && renderButtons(article)}
</div>
<div className="flex-none mt-auto bg-white rounded-b rounded-t-none overflow-hidden shadow-lg p-6">
<div className="flex items-center justify-between">
@@ -112,7 +125,7 @@ export default function Home({
</div>
)),
);
- }, [articles]);
+ }, [articles, username]);
return (
<>
diff --git a/frontend/src/pages/articles/Article.tsx b/frontend/src/pages/articles/Article.tsx
index 1a96ada..633e418 100644
--- a/frontend/src/pages/articles/Article.tsx
+++ b/frontend/src/pages/articles/Article.tsx
@@ -7,7 +7,7 @@ type article = {
photoUrl: string;
content: string;
createdOn: string;
- updateOn: string;
+ updatedOn: string;
};
export default function Article() {
@@ -28,8 +28,25 @@ export default function Article() {
return (
<>
- <h1>{articleData?.title}</h1>
- <div>{articleData?.content}</div>
+ <div className="text-center pt-16 md:pt-32">
+ <p className="text-sm md:text-base text-green-500 font-bold">
+ {articleData?.createdOn}
+ </p>
+ <h1 className="font-bold break-normal text-3xl md:text-5xl">
+ {articleData?.title}
+ </h1>
+ </div>
+ <div
+ className="container w-full max-w-6xl mx-auto bg-white bg-cover mt-8 rounded"
+ style={{ background: `url('${articleData?.photoUrl}'); height: 75vh;` }}
+ ></div>
+ <div className="container max-w-5xl mx-auto -mt-32">
+ <div className="mx-0 sm:mx-6">
+ <div className="bg-white w-full p-8 md:p-24 text-xl md:text-2xl text-gray-800 leading-normal">
+ <div>{articleData?.content}</div>
+ </div>
+ </div>
+ </div>
</>
);
}
diff --git a/frontend/src/pages/articles/Edit.tsx b/frontend/src/pages/articles/Edit.tsx
new file mode 100644
index 0000000..4d8e2d8
--- /dev/null
+++ b/frontend/src/pages/articles/Edit.tsx
@@ -0,0 +1,142 @@
+import { useState, useEffect, FormEvent, ChangeEvent } from "react";
+import { useParams, useNavigate } from "react-router-dom";
+
+type article = {
+ id: number;
+ title: string;
+ photoUrl: string;
+ content: string;
+ createdOn: string;
+ updatedOn: string;
+};
+
+export default function EditArticle() {
+ const navigate = useNavigate();
+ const { id } = useParams();
+ const [articleData, setArticleData] = useState<article>({
+ id: 0,
+ title: "",
+ photoUrl: "",
+ content: "",
+ createdOn: "",
+ updatedOn: "",
+ });
+
+ useEffect(() => {
+ const url = `${import.meta.env.VITE_API_TITLE}/api/v1/article/${id}`;
+ fetch(url)
+ .then((response) => {
+ if (response.ok) {
+ return response.json();
+ }
+ throw new Error("Network response was not ok.");
+ })
+ .then((response) => setArticleData(response));
+ }, [id]);
+
+ const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
+ setArticleData({ ...articleData, [e.target.name]: e.target.value });
+ };
+
+ const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
+ e.preventDefault(); //stops submit from happening
+
+ const target = e.target as typeof e.target & {
+ title: { value: string };
+ photoUrl: { value: string };
+ content: { value: string };
+ };
+
+ const formData = new FormData();
+ formData.append("title", target.title.value);
+ formData.append("photoUrl", target.photoUrl.value);
+ formData.append("content", target.content.value);
+
+ const response = await fetch(
+ `${import.meta.env.VITE_API_TITLE}/api/v1/articles/edit/${articleData.id}`,
+ {
+ credentials: "include",
+ method: "post",
+ body: formData,
+ },
+ );
+ if (response.ok) {
+ navigate("/");
+ } else {
+ console.log(response);
+ alert("check console for error");
+ }
+ };
+
+ return (
+ <>
+ <div className="flex justify-center bg-white p-12">
+ <form onSubmit={handleSubmit} method="post" className="w-full max-w-lg">
+ <input type="hidden" />
+ <div className="flex flex-wrap -mx-3 mb-6">
+ <div className="w-full md:w-1/2 px-3 mb-6 md:mb-0">
+ <label className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2">
+ Title
+ </label>
+ <input
+ className="appearance-none block w-full bg-gray-200 text-gray-700 border rounded py-3 px-4 mb-3 leading-tight focus:outline-none focus:bg-white"
+ id="title"
+ type="text"
+ name="title"
+ value={articleData.title}
+ onChange={handleChange}
+ placeholder="Yep"
+ />
+ <p className="text-red-500 text-xs italic">
+ Please fill out this field.
+ </p>
+ </div>
+ <div className="w-full md:w-1/2 px-3">
+ <label className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2">
+ Photo URL
+ </label>
+ <input
+ className="appearance-none block w-full bg-gray-200 text-gray-700 border border-gray-200 rounded py-3 px-4 leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
+ id="photoUrl"
+ type="text"
+ name="photoUrl"
+ value={articleData.photoUrl}
+ onChange={handleChange}
+ placeholder="Doe"
+ />
+ <p className="text-red-500 text-xs italic">
+ Please fill out this field.
+ </p>
+ </div>
+ </div>
+ <div className="flex flex-wrap -mx-3 mb-6">
+ <div className="w-full px-3">
+ <label className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2">
+ Content
+ </label>
+ <input
+ className="appearance-none block w-full bg-gray-200 text-gray-700 border border-gray-200 rounded py-3 px-4 leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
+ id="content"
+ type="text"
+ name="content"
+ value={articleData.content}
+ onChange={handleChange}
+ placeholder="Doe"
+ />
+ <p className="text-red-500 text-xs italic">
+ Please fill out this field.
+ </p>
+ </div>
+ </div>
+ <div className="flex flex-wrap mb-2"></div>
+ <button
+ type="submit"
+ className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
+ >
+ Update
+ </button>
+ </form>
+ </div>
+ </>
+ );
+}
diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx
index cee3529..df38459 100644
--- a/frontend/src/routes/index.tsx
+++ b/frontend/src/routes/index.tsx
@@ -4,6 +4,7 @@ import Home from "../pages/Home";
import Layout from "../components/Layout";
import Article from "../pages/articles/Article";
import NewArticle from "../pages/articles/New";
+import EditArticle from "../pages/articles/Edit";
import Register from "../pages/auth/Register";
import Login from "../pages/auth/Login";
@@ -33,9 +34,18 @@ export default function Index() {
<Layout user={userProp} articleSearch={articleSearchProp} />
}
>
- <Route index element={<Home articleSearch={articleSearchProp} />} />
+ <Route
+ index
+ element={
+ <Home
+ username={userProp.value}
+ articleSearch={articleSearchProp}
+ />
+ }
+ />
<Route path="/article/:id" element={<Article />} />
<Route path="/article/new" element={<NewArticle />} />
+ <Route path="/article/edit/:id" element={<EditArticle />} />
<Route path="register" element={<Register />} />
<Route path="login" element={<Login user={userProp} />} />
</Route>