mirror of
https://github.com/usatiuk/y.git
synced 2025-10-28 10:37:47 +01:00
simple add remove follower
This commit is contained in:
9
client/package-lock.json
generated
9
client/package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "yclient",
|
"name": "yclient",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.21.0",
|
"react-router-dom": "^6.21.0",
|
||||||
@@ -4725,6 +4726,14 @@
|
|||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jwt-decode": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"browserslist": "> 0.5%, last 2 versions, not dead",
|
"browserslist": "> 0.5%, last 2 versions, not dead",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.21.0",
|
"react-router-dom": "^6.21.0",
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ import { deleteToken, getToken } from "./api/utils";
|
|||||||
import { Login } from "./Login";
|
import { Login } from "./Login";
|
||||||
import { Signup } from "./Signup";
|
import { Signup } from "./Signup";
|
||||||
import { Home } from "./Home";
|
import { Home } from "./Home";
|
||||||
import { loginAction, profileSelfAction, signupAction } from "./actions";
|
import {
|
||||||
|
loginAction,
|
||||||
|
profileSelfAction,
|
||||||
|
signupAction,
|
||||||
|
userListAction,
|
||||||
|
} from "./actions";
|
||||||
import { homeLoader, profileLoader, userListLoader } from "./loaders";
|
import { homeLoader, profileLoader, userListLoader } from "./loaders";
|
||||||
import { isError } from "./api/dto";
|
import { isError } from "./api/dto";
|
||||||
import { Feed } from "./Feed";
|
import { Feed } from "./Feed";
|
||||||
@@ -39,7 +44,12 @@ const router = createBrowserRouter([
|
|||||||
children: [
|
children: [
|
||||||
{ path: "feed", element: <Feed /> },
|
{ path: "feed", element: <Feed /> },
|
||||||
{ path: "messages", element: <Messages /> },
|
{ path: "messages", element: <Messages /> },
|
||||||
{ path: "users", element: <UserList />, loader: userListLoader },
|
{
|
||||||
|
path: "users",
|
||||||
|
element: <UserList />,
|
||||||
|
loader: userListLoader,
|
||||||
|
action: userListAction,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "profile",
|
path: "profile",
|
||||||
loader: profileLoader,
|
loader: profileLoader,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { homeLoader, LoaderToType } from "./loaders";
|
|||||||
import { isError } from "./api/dto";
|
import { isError } from "./api/dto";
|
||||||
|
|
||||||
import "./Home.scss";
|
import "./Home.scss";
|
||||||
|
import { HomeContextType } from "./HomeContext";
|
||||||
|
|
||||||
export function Home() {
|
export function Home() {
|
||||||
const loaderData = useLoaderData() as LoaderToType<typeof homeLoader>;
|
const loaderData = useLoaderData() as LoaderToType<typeof homeLoader>;
|
||||||
@@ -42,7 +43,9 @@ export function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="HomeContent">
|
<div id="HomeContent">
|
||||||
<Outlet />
|
<Outlet
|
||||||
|
context={{ user: loaderData } satisfies HomeContextType}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
10
client/src/HomeContext.tsx
Normal file
10
client/src/HomeContext.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { TPersonTo } from "./api/dto";
|
||||||
|
import { useOutletContext } from "react-router-dom";
|
||||||
|
|
||||||
|
export type HomeContextType = {
|
||||||
|
user: TPersonTo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useHomeContext() {
|
||||||
|
return useOutletContext<HomeContextType>();
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { Form, Link, useLoaderData } from "react-router-dom";
|
|||||||
import { LoaderToType, profileLoader } from "./loaders";
|
import { LoaderToType, profileLoader } from "./loaders";
|
||||||
import { isError } from "./api/dto";
|
import { isError } from "./api/dto";
|
||||||
import { Post } from "./Post";
|
import { Post } from "./Post";
|
||||||
|
import { useHomeContext } from "./HomeContext";
|
||||||
|
|
||||||
export interface IProfileProps {
|
export interface IProfileProps {
|
||||||
self: boolean;
|
self: boolean;
|
||||||
@@ -11,6 +12,8 @@ export interface IProfileProps {
|
|||||||
export function Profile({ self }: IProfileProps) {
|
export function Profile({ self }: IProfileProps) {
|
||||||
const loaderData = useLoaderData() as LoaderToType<typeof profileLoader>;
|
const loaderData = useLoaderData() as LoaderToType<typeof profileLoader>;
|
||||||
|
|
||||||
|
const homeContext = useHomeContext();
|
||||||
|
|
||||||
if (!loaderData || isError(loaderData)) {
|
if (!loaderData || isError(loaderData)) {
|
||||||
return <div>Error</div>;
|
return <div>Error</div>;
|
||||||
}
|
}
|
||||||
@@ -22,8 +25,8 @@ export function Profile({ self }: IProfileProps) {
|
|||||||
return (
|
return (
|
||||||
<div className={"profileView"}>
|
<div className={"profileView"}>
|
||||||
<div className={"profileInfo"}>
|
<div className={"profileInfo"}>
|
||||||
<span className={"fullName"}>{loaderData.user.fullName}</span>
|
<span className={"fullName"}>{homeContext.user.fullName}</span>
|
||||||
<span className={"username"}>{loaderData.user.fullName}</span>
|
<span className={"username"}>{homeContext.user.fullName}</span>
|
||||||
</div>
|
</div>
|
||||||
{self && (
|
{self && (
|
||||||
<div className={"newPost"}>
|
<div className={"newPost"}>
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import "./ProfileCard.scss";
|
import "./ProfileCard.scss";
|
||||||
import { Link } from "react-router-dom";
|
import { Form, Link } from "react-router-dom";
|
||||||
|
|
||||||
export function ProfileCard({
|
export function ProfileCard({
|
||||||
username,
|
username,
|
||||||
fullName,
|
fullName,
|
||||||
|
uuid,
|
||||||
|
actions,
|
||||||
|
alreadyFollowing,
|
||||||
}: {
|
}: {
|
||||||
username: string;
|
username: string;
|
||||||
fullName: string;
|
fullName: string;
|
||||||
|
uuid: string;
|
||||||
|
actions: boolean;
|
||||||
|
alreadyFollowing: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={"profileCard"}>
|
<div className={"profileCard"}>
|
||||||
@@ -16,6 +22,30 @@ export function ProfileCard({
|
|||||||
<Link to={`/home/profile/${username}`} className={"username"}>
|
<Link to={`/home/profile/${username}`} className={"username"}>
|
||||||
{username}
|
{username}
|
||||||
</Link>
|
</Link>
|
||||||
|
{actions &&
|
||||||
|
(alreadyFollowing ? (
|
||||||
|
<Form method={"put"}>
|
||||||
|
<input hidden={true} value={uuid} name={"uuid"} />
|
||||||
|
<button
|
||||||
|
type={"submit"}
|
||||||
|
name={"intent"}
|
||||||
|
value={"unfollow"}
|
||||||
|
>
|
||||||
|
unfollow
|
||||||
|
</button>
|
||||||
|
</Form>
|
||||||
|
) : (
|
||||||
|
<Form method={"put"}>
|
||||||
|
<input hidden={true} value={uuid} name={"uuid"} />
|
||||||
|
<button
|
||||||
|
type={"submit"}
|
||||||
|
name={"intent"}
|
||||||
|
value={"follow"}
|
||||||
|
>
|
||||||
|
follow
|
||||||
|
</button>
|
||||||
|
</Form>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,33 @@ import { useLoaderData } from "react-router-dom";
|
|||||||
import { LoaderToType, userListLoader } from "./loaders";
|
import { LoaderToType, userListLoader } from "./loaders";
|
||||||
import { isError } from "./api/dto";
|
import { isError } from "./api/dto";
|
||||||
import { ProfileCard } from "./ProfileCard";
|
import { ProfileCard } from "./ProfileCard";
|
||||||
|
import { useHomeContext } from "./HomeContext";
|
||||||
|
|
||||||
export function UserList() {
|
export function UserList() {
|
||||||
const loaderData = useLoaderData() as LoaderToType<typeof userListLoader>;
|
const loaderData = useLoaderData() as LoaderToType<typeof userListLoader>;
|
||||||
|
const homeContext = useHomeContext();
|
||||||
|
|
||||||
if (!loaderData || isError(loaderData)) {
|
if (!loaderData) {
|
||||||
return <div>Error</div>;
|
return <div>Error</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { people, following } = loaderData;
|
||||||
|
if (isError(following) || isError(people)) {
|
||||||
|
return <div>Error</div>;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{loaderData.map((u) => {
|
{people.map((u) => {
|
||||||
return (
|
return (
|
||||||
<ProfileCard
|
<ProfileCard
|
||||||
username={u.username}
|
username={u.username}
|
||||||
fullName={u.fullName}
|
fullName={u.fullName}
|
||||||
|
uuid={u.uuid}
|
||||||
key={u.uuid}
|
key={u.uuid}
|
||||||
|
actions={homeContext.user.uuid != u.uuid}
|
||||||
|
alreadyFollowing={following.some(
|
||||||
|
(f) => f.uuid == u.uuid,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { signup } from "./api/Person";
|
import { addFollower, removeFollower, signup } from "./api/Person";
|
||||||
import { ActionFunctionArgs, redirect } from "react-router-dom";
|
import { ActionFunctionArgs, redirect } from "react-router-dom";
|
||||||
import { login } from "./api/Token";
|
import { login } from "./api/Token";
|
||||||
import { isError } from "./api/dto";
|
import { isError } from "./api/dto";
|
||||||
@@ -61,3 +61,14 @@ export async function profileSelfAction({ request }: ActionFunctionArgs) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function userListAction({ request }: ActionFunctionArgs) {
|
||||||
|
const formData = await request.formData();
|
||||||
|
const intent = formData.get("intent")!.toString();
|
||||||
|
console.log(intent);
|
||||||
|
if (intent == "follow") {
|
||||||
|
return await addFollower(formData.get("uuid")!.toString());
|
||||||
|
} else if (intent == "unfollow") {
|
||||||
|
return await removeFollower(formData.get("uuid")!.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { fetchJSON, fetchJSONAuth } from "./utils";
|
import { fetchJSON, fetchJSONAuth } from "./utils";
|
||||||
import {
|
import {
|
||||||
|
NoContentToResp,
|
||||||
PersonToArrResp,
|
PersonToArrResp,
|
||||||
PersonToResp,
|
PersonToResp,
|
||||||
|
TNoContentToResp,
|
||||||
TPersonToArrResp,
|
TPersonToArrResp,
|
||||||
TPersonToResp,
|
TPersonToResp,
|
||||||
} from "./dto";
|
} from "./dto";
|
||||||
@@ -30,6 +32,10 @@ export async function getAllPerson(): Promise<TPersonToArrResp> {
|
|||||||
return fetchJSONAuth("/person", "GET", PersonToArrResp);
|
return fetchJSONAuth("/person", "GET", PersonToArrResp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getFollowing(): Promise<TPersonToArrResp> {
|
||||||
|
return fetchJSONAuth("/person/following", "GET", PersonToArrResp);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getPersonByUsername(
|
export async function getPersonByUsername(
|
||||||
username: string,
|
username: string,
|
||||||
): Promise<TPersonToResp> {
|
): Promise<TPersonToResp> {
|
||||||
@@ -39,3 +45,15 @@ export async function getPersonByUsername(
|
|||||||
PersonToResp,
|
PersonToResp,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function addFollower(uuid: string): Promise<TNoContentToResp> {
|
||||||
|
return fetchJSONAuth("/person/following/" + uuid, "PUT", NoContentToResp);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeFollower(uuid: string): Promise<TNoContentToResp> {
|
||||||
|
return fetchJSONAuth(
|
||||||
|
"/person/following/" + uuid,
|
||||||
|
"DELETE",
|
||||||
|
NoContentToResp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,22 @@ export async function deletePost(id: number): Promise<TNoContentToResp> {
|
|||||||
return fetchJSONAuth(`/post/${id.toString()}`, "DELETE", NoContentToResp);
|
return fetchJSONAuth(`/post/${id.toString()}`, "DELETE", NoContentToResp);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPosts(author: string): Promise<TPostToArrResp> {
|
export async function getPostsByAuthorUuid(
|
||||||
return fetchJSONAuth(`/post?author=${author}`, "GET", PostToArrResp);
|
author: string,
|
||||||
|
): Promise<TPostToArrResp> {
|
||||||
|
return fetchJSONAuth(
|
||||||
|
`/post/by-author-uuid?author=${author}`,
|
||||||
|
"GET",
|
||||||
|
PostToArrResp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPostsByAuthorUsername(
|
||||||
|
author: string,
|
||||||
|
): Promise<TPostToArrResp> {
|
||||||
|
return fetchJSONAuth(
|
||||||
|
`/post/by-author-username?author=${author}`,
|
||||||
|
"GET",
|
||||||
|
PostToArrResp,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
// import { apiRoot } from "~src/env";
|
// import { apiRoot } from "~src/env";
|
||||||
|
|
||||||
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
import { isError, TErrorTo } from "./dto";
|
||||||
|
|
||||||
const apiRoot: string = "http://localhost:8080";
|
const apiRoot: string = "http://localhost:8080";
|
||||||
|
|
||||||
let token: string | null;
|
let token: string | null;
|
||||||
@@ -16,6 +19,12 @@ export function getToken(): string | null {
|
|||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getTokenUserUuid(): string | null {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) return null;
|
||||||
|
return jwtDecode(token).sub ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
export function deleteToken(): void {
|
export function deleteToken(): void {
|
||||||
token = null;
|
token = null;
|
||||||
localStorage.removeItem("jwt_token");
|
localStorage.removeItem("jwt_token");
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { getAllPerson, getPersonByUsername, getSelf } from "./api/Person";
|
import {
|
||||||
import { deleteToken, getToken } from "./api/utils";
|
getAllPerson,
|
||||||
|
getFollowing,
|
||||||
|
getPersonByUsername,
|
||||||
|
getSelf,
|
||||||
|
} from "./api/Person";
|
||||||
|
import { deleteToken, getToken, getTokenUserUuid } from "./api/utils";
|
||||||
import { redirect } from "react-router-dom";
|
import { redirect } from "react-router-dom";
|
||||||
import { isError } from "./api/dto";
|
import { isError } from "./api/dto";
|
||||||
import { getPosts } from "./api/Post";
|
import { getPostsByAuthorUsername, getPostsByAuthorUuid } from "./api/Post";
|
||||||
|
|
||||||
export type LoaderToType<T extends (...args: any) => any> =
|
export type LoaderToType<T extends (...args: any) => any> =
|
||||||
| Exclude<Awaited<ReturnType<T>>, Response>
|
| Exclude<Awaited<ReturnType<T>>, Response>
|
||||||
@@ -25,7 +30,7 @@ export async function homeLoader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function userListLoader() {
|
export async function userListLoader() {
|
||||||
return await getAllPerson();
|
return { people: await getAllPerson(), following: await getFollowing() };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function profileLoader({
|
export async function profileLoader({
|
||||||
@@ -33,27 +38,31 @@ export async function profileLoader({
|
|||||||
}: {
|
}: {
|
||||||
params: { username?: string };
|
params: { username?: string };
|
||||||
}) {
|
}) {
|
||||||
const self = await getCheckUserSelf();
|
const selfUuid = getTokenUserUuid();
|
||||||
if (!self || self instanceof Response || isError(self)) {
|
if (!selfUuid) return redirect("/");
|
||||||
return self;
|
if (selfUuid == params.username) {
|
||||||
}
|
|
||||||
|
|
||||||
if (self.username == params.username) {
|
|
||||||
return redirect("/home/profile");
|
return redirect("/home/profile");
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = params.username
|
const posts = params.username
|
||||||
? await getPersonByUsername(params.username)
|
? await getPostsByAuthorUsername(params.username)
|
||||||
: self;
|
: await getPostsByAuthorUuid(selfUuid);
|
||||||
if (!user || user instanceof Response || isError(user)) {
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
const posts = await getPosts(user.uuid);
|
const retUser = params.username
|
||||||
|
? await getPersonByUsername(params.username)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(params.username && !retUser) ||
|
||||||
|
retUser instanceof Response ||
|
||||||
|
isError(retUser)
|
||||||
|
) {
|
||||||
|
return retUser;
|
||||||
|
}
|
||||||
|
|
||||||
if (isError(posts)) {
|
if (isError(posts)) {
|
||||||
return { user, posts: null };
|
return { user: retUser, posts: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { user, posts };
|
return { user: retUser, posts };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.usatiuk.tjv.y.server.entity.Person;
|
|||||||
import com.usatiuk.tjv.y.server.service.PersonService;
|
import com.usatiuk.tjv.y.server.service.PersonService;
|
||||||
import com.usatiuk.tjv.y.server.service.exceptions.UserAlreadyExistsException;
|
import com.usatiuk.tjv.y.server.service.exceptions.UserAlreadyExistsException;
|
||||||
import com.usatiuk.tjv.y.server.service.exceptions.UserNotFoundException;
|
import com.usatiuk.tjv.y.server.service.exceptions.UserNotFoundException;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@@ -79,4 +80,16 @@ public class PersonController {
|
|||||||
return personService.getFollowing(principal.getName()).stream().map(PersonMapper::makeDto);
|
return personService.getFollowing(principal.getName()).stream().map(PersonMapper::makeDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PutMapping(path = "/following/{uuid}")
|
||||||
|
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||||
|
public void addFollowing(Principal principal, @PathVariable String uuid) throws UserNotFoundException {
|
||||||
|
personService.addFollower(principal.getName(), uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping(path = "/following/{uuid}")
|
||||||
|
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||||
|
public void deleteFollowing(Principal principal, @PathVariable String uuid) throws UserNotFoundException {
|
||||||
|
personService.removeFollower(principal.getName(), uuid);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,8 +36,16 @@ public class PostController {
|
|||||||
return PostMapper.makeDto(postService.create(post));
|
return PostMapper.makeDto(postService.create(post));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping(path = "/by-author-uuid")
|
||||||
public Stream<PostTo> readAllByAuthor(@RequestParam Optional<String> author) {
|
public Stream<PostTo> readAllByAuthorUuid(@RequestParam Optional<String> author) {
|
||||||
|
if (author.isPresent())
|
||||||
|
return postService.readByAuthorId(author.get()).stream().map(PostMapper::makeDto);
|
||||||
|
else
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "/by-author-username")
|
||||||
|
public Stream<PostTo> readAllByAuthorUsername(@RequestParam Optional<String> author) {
|
||||||
if (author.isPresent())
|
if (author.isPresent())
|
||||||
return postService.readByAuthorId(author.get()).stream().map(PostMapper::makeDto);
|
return postService.readByAuthorId(author.get()).stream().map(PostMapper::makeDto);
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import java.util.Collection;
|
|||||||
public interface PostRepository extends PagingAndSortingRepository<Post, Long>, CrudRepository<Post, Long> {
|
public interface PostRepository extends PagingAndSortingRepository<Post, Long>, CrudRepository<Post, Long> {
|
||||||
Collection<Post> findByAuthorUuid(String authorUuid);
|
Collection<Post> findByAuthorUuid(String authorUuid);
|
||||||
|
|
||||||
|
Collection<Post> findByAuthorUsername(String authorUsername);
|
||||||
|
|
||||||
@Query(value = "SELECT p FROM Post p " +
|
@Query(value = "SELECT p FROM Post p " +
|
||||||
"WHERE EXISTS " +
|
"WHERE EXISTS " +
|
||||||
"(SELECT u FROM Person u LEFT JOIN u.following f where u.uuid = :personUuid and f.uuid = p.author.uuid)")
|
"(SELECT u FROM Person u LEFT JOIN u.following f where u.uuid = :personUuid and f.uuid = p.author.uuid)")
|
||||||
|
|||||||
@@ -17,4 +17,8 @@ public interface PersonService extends CrudService<Person, String> {
|
|||||||
Collection<Person> getFollowers(String uuid) throws UserNotFoundException;
|
Collection<Person> getFollowers(String uuid) throws UserNotFoundException;
|
||||||
|
|
||||||
Collection<Person> getFollowing(String uuid) throws UserNotFoundException;
|
Collection<Person> getFollowing(String uuid) throws UserNotFoundException;
|
||||||
|
|
||||||
|
void addFollower(String follower, String followee) throws UserNotFoundException;
|
||||||
|
|
||||||
|
void removeFollower(String follower, String followee) throws UserNotFoundException;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.usatiuk.tjv.y.server.entity.Person;
|
|||||||
import com.usatiuk.tjv.y.server.repository.PersonRepository;
|
import com.usatiuk.tjv.y.server.repository.PersonRepository;
|
||||||
import com.usatiuk.tjv.y.server.service.exceptions.UserAlreadyExistsException;
|
import com.usatiuk.tjv.y.server.service.exceptions.UserAlreadyExistsException;
|
||||||
import com.usatiuk.tjv.y.server.service.exceptions.UserNotFoundException;
|
import com.usatiuk.tjv.y.server.service.exceptions.UserNotFoundException;
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
import org.springframework.data.repository.CrudRepository;
|
import org.springframework.data.repository.CrudRepository;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -15,11 +16,13 @@ import java.util.Optional;
|
|||||||
public class PersonServiceImpl extends CrudServiceImpl<Person, String> implements PersonService {
|
public class PersonServiceImpl extends CrudServiceImpl<Person, String> implements PersonService {
|
||||||
private final PersonRepository personRepository;
|
private final PersonRepository personRepository;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final EntityManager entityManager;
|
||||||
|
|
||||||
public PersonServiceImpl(PersonRepository personRepository,
|
public PersonServiceImpl(PersonRepository personRepository,
|
||||||
PasswordEncoder passwordEncoder) {
|
PasswordEncoder passwordEncoder, EntityManager entityManager) {
|
||||||
this.personRepository = personRepository;
|
this.personRepository = personRepository;
|
||||||
this.passwordEncoder = passwordEncoder;
|
this.passwordEncoder = passwordEncoder;
|
||||||
|
this.entityManager = entityManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -59,4 +62,18 @@ public class PersonServiceImpl extends CrudServiceImpl<Person, String> implement
|
|||||||
public Collection<Person> getFollowing(String uuid) throws UserNotFoundException {
|
public Collection<Person> getFollowing(String uuid) throws UserNotFoundException {
|
||||||
return personRepository.findById(uuid).orElseThrow(UserNotFoundException::new).getFollowing();
|
return personRepository.findById(uuid).orElseThrow(UserNotFoundException::new).getFollowing();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addFollower(String follower, String followee) throws UserNotFoundException {
|
||||||
|
var person = personRepository.findById(follower).orElseThrow(UserNotFoundException::new);
|
||||||
|
person.getFollowing().add(entityManager.getReference(Person.class, followee));
|
||||||
|
personRepository.save(person);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeFollower(String follower, String followee) throws UserNotFoundException {
|
||||||
|
var person = personRepository.findById(follower).orElseThrow(UserNotFoundException::new);
|
||||||
|
person.getFollowing().remove(entityManager.getReference(Person.class, followee));
|
||||||
|
personRepository.save(person);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,7 @@ import java.util.Collection;
|
|||||||
public interface PostService extends CrudService<Post, Long> {
|
public interface PostService extends CrudService<Post, Long> {
|
||||||
Collection<Post> readByAuthorId(String authorUuid);
|
Collection<Post> readByAuthorId(String authorUuid);
|
||||||
|
|
||||||
|
Collection<Post> readByAuthorUsername(String authorUsername);
|
||||||
|
|
||||||
Collection<Post> readByPersonFollowees(String personUuid);
|
Collection<Post> readByPersonFollowees(String personUuid);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ public class PostServiceImpl extends CrudServiceImpl<Post, Long> implements Post
|
|||||||
return postRepository.findByAuthorUuid(authorId);
|
return postRepository.findByAuthorUuid(authorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Post> readByAuthorUsername(String authorUsername) {
|
||||||
|
return postRepository.findByAuthorUsername(authorUsername);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<Post> readByPersonFollowees(String personUuid) {
|
public Collection<Post> readByPersonFollowees(String personUuid) {
|
||||||
return postRepository.findByPersonFollowees(personUuid);
|
return postRepository.findByPersonFollowees(personUuid);
|
||||||
|
|||||||
@@ -119,4 +119,25 @@ public class PersonControllerTest extends DemoDataDbTest {
|
|||||||
Assertions.assertIterableEquals(Arrays.asList(personToResponse), List.of(PersonMapper.makeDto(person2), PersonMapper.makeDto(person1)));
|
Assertions.assertIterableEquals(Arrays.asList(personToResponse), List.of(PersonMapper.makeDto(person2), PersonMapper.makeDto(person1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAddFollowing() {
|
||||||
|
var response = restTemplate.exchange(addr + "/person/following/" + person3.getUuid(),
|
||||||
|
HttpMethod.PUT, new HttpEntity<>(createAuthHeaders(person1Auth)), Object.class);
|
||||||
|
|
||||||
|
Assertions.assertNotNull(response);
|
||||||
|
Assertions.assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode());
|
||||||
|
|
||||||
|
var response2 = restTemplate.exchange(addr + "/person/following",
|
||||||
|
HttpMethod.GET, new HttpEntity<>(createAuthHeaders(person1Auth)), PersonTo[].class);
|
||||||
|
|
||||||
|
Assertions.assertNotNull(response2);
|
||||||
|
Assertions.assertEquals(HttpStatus.OK, response2.getStatusCode());
|
||||||
|
|
||||||
|
var personToResponse = response2.getBody();
|
||||||
|
Assertions.assertNotNull(personToResponse);
|
||||||
|
|
||||||
|
Assertions.assertEquals(1, personToResponse.length);
|
||||||
|
Assertions.assertIterableEquals(Arrays.asList(personToResponse), List.of(PersonMapper.makeDto(person3)));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ public class PostControllerTest extends DemoDataDbTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldGetByAuthor() {
|
void shouldGetByAuthor() {
|
||||||
var response = restTemplate.exchange(addr + "/post?author=" + person1.getUuid(), HttpMethod.GET,
|
var response = restTemplate.exchange(addr + "/post/by-author-uuid?author=" + person1.getUuid(), HttpMethod.GET,
|
||||||
HttpEntity.EMPTY, PostTo[].class);
|
HttpEntity.EMPTY, PostTo[].class);
|
||||||
|
|
||||||
Assertions.assertEquals(HttpStatus.OK, response.getStatusCode());
|
Assertions.assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||||
|
|||||||
Reference in New Issue
Block a user