deleting posts

This commit is contained in:
Stepan Usatiuk
2023-12-16 21:07:39 +01:00
parent d98906a23f
commit f556f67db6
9 changed files with 105 additions and 10 deletions

View File

@@ -11,9 +11,31 @@
word-wrap: anywhere;
}
.createdDate {
.footer {
margin-top: 0.3rem;
font-size: 0.7rem;
color: #A0A0A0;
display: flex;
flex: auto;
width: 100%;
.info {
flex-grow: 1;
align-self: start;
}
.actions {
align-self: end;
button {
background: none;
border: none;
padding: 0;
text-decoration: none;
cursor: pointer;
color: inherit;
font-size: inherit;
}
}
}
}

View File

@@ -1,16 +1,43 @@
import "./Post.scss";
import { Form } from "react-router-dom";
export function Post({
text,
createdDate,
actions,
id,
}: {
text: string;
createdDate: string;
actions: boolean;
id: number;
}) {
return (
<div className={"post"}>
<span className={"text"}>{text}</span>
<span className={"createdDate"}>{createdDate}</span>
<div className={"footer"}>
<div className={"info"}>
<span className={"createdDate"}>{createdDate}</span>
</div>
{actions && (
<div className={"actions"}>
<Form method={"delete"}>
<input
hidden={true}
name={"postToDeleteId"}
value={id}
/>
<button
name="intent"
value="deletePost"
type={"submit"}
>
delete
</button>
</Form>
</div>
)}
</div>
</div>
);
}

View File

@@ -9,7 +9,7 @@ export interface IProfileProps {
self: boolean;
}
export function Profile(props: IProfileProps) {
export function Profile({ self }: IProfileProps) {
const loaderData = useLoaderData() as LoaderToType<
typeof profileSelfLoader
>;
@@ -31,7 +31,9 @@ export function Profile(props: IProfileProps) {
<div className={"newPost"}>
<Form method="post">
<textarea placeholder={"Write something!"} name="text" />
<button type="submit">Post</button>
<button name="intent" value="post" type="submit">
Post
</button>
</Form>
</div>
<div className={"posts"}>
@@ -43,6 +45,8 @@ export function Profile(props: IProfileProps) {
text={p.text}
createdDate={`${date.toUTCString()}`}
key={p.id}
id={p.id}
actions={self}
/>
);
})}

View File

@@ -3,7 +3,7 @@ import { ActionFunctionArgs, redirect } from "react-router-dom";
import { login } from "./api/Token";
import { isError } from "./api/dto";
import { setToken } from "./api/utils";
import { post } from "./api/Post";
import { createPost, deletePost } from "./api/Post";
export type ActionToType<T extends (...args: any) => any> =
| Exclude<Awaited<ReturnType<T>>, Response>
@@ -52,5 +52,12 @@ export async function signupAction({ request }: ActionFunctionArgs) {
export async function profileSelfAction({ request }: ActionFunctionArgs) {
const formData = await request.formData();
return await post(formData.get("text")!.toString());
const intent = formData.get("intent")!.toString();
if (intent == "post") {
return await createPost(formData.get("text")!.toString());
} else if (intent == "deletePost") {
return await deletePost(
parseInt(formData.get("postToDeleteId")!.toString()),
);
}
}

View File

@@ -1,11 +1,21 @@
import { PostToArrResp, PostToResp, TPostToArrResp, TPostToResp } from "./dto";
import {
NoContentToResp,
PostToArrResp,
PostToResp,
TNoContentToResp,
TPostToArrResp,
TPostToResp,
} from "./dto";
import { fetchJSONAuth } from "./utils";
export async function post(text: string): Promise<TPostToResp> {
export async function createPost(text: string): Promise<TPostToResp> {
return fetchJSONAuth("/post", "POST", PostToResp, {
text,
});
}
export async function deletePost(id: number): Promise<TNoContentToResp> {
return fetchJSONAuth(`/post/${id.toString()}`, "DELETE", NoContentToResp);
}
export async function getPosts(author: string): Promise<TPostToArrResp> {
return fetchJSONAuth(`/post?author=${author}`, "GET", PostToArrResp);

View File

@@ -10,6 +10,12 @@ function CreateAPIResponse<T extends z.ZodTypeAny>(obj: T) {
return z.union([ErrorTo, obj]);
}
export const NoContentTo = z.object({});
export type TNoContentTo = z.infer<typeof NoContentTo>;
export const NoContentToResp = CreateAPIResponse(NoContentTo);
export type TNoContentToResp = z.infer<typeof NoContentToResp>;
export const PersonSignupTo = z.object({
username: z.string(),
fullName: z.string(),

View File

@@ -47,7 +47,11 @@ export async function fetchJSON<T, P extends { parse: (arg: string) => T }>(
headers: reqHeaders(),
body: reqBody(),
});
return parser.parse(await response.json());
const json = await response.json().catch(() => {
return {};
});
return parser.parse(json);
}
export async function fetchJSONAuth<T, P extends { parse: (arg: string) => T }>(

View File

@@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.security.Principal;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
@@ -55,4 +56,16 @@ public class PostController {
return PostMapper.makeDto(post.get());
}
@DeleteMapping(path = "/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(Principal principal, @PathVariable long id) {
var read = postService.readById(id);
if (read.isEmpty()) return;
if (!Objects.equals(read.get().getAuthor().getId(), principal.getName())) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
}
postService.deleteById(id);
}
}

View File

@@ -76,7 +76,9 @@ public class WebSecurityConfig {
@Bean
CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
var config = new CorsConfiguration().applyPermitDefaultValues();
config.setAllowedMethods(List.of("*"));
source.registerCorsConfiguration("/**", config);
return source;
}
}