posts posting

This commit is contained in:
Stepan Usatiuk
2023-12-16 18:32:35 +01:00
parent 22aa0585ea
commit 3cf1d03df4
19 changed files with 368 additions and 28 deletions

View File

@@ -1,6 +1,5 @@
#appRoot { #appRoot {
font-family: sans-serif; font-family: sans-serif;
max-width: 50rem; max-width: 50rem;
margin-left: auto; margin: 0 auto;
margin-right: auto;
} }

View File

@@ -10,10 +10,13 @@ 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, signupAction } from "./actions"; import { loginAction, profileSelfAction, signupAction } from "./actions";
import { homeLoader } from "./loaders"; import { homeLoader, profileSelfLoader } from "./loaders";
import { isError } from "./api/dto"; import { isError } from "./api/dto";
import { Feed } from "./Feed"; import { Feed } from "./Feed";
import { Messages } from "./Messages";
import { Profile } from "./Profile";
import { getSelf } from "./api/Person";
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@@ -29,18 +32,25 @@ const router = createBrowserRouter([
{ {
path: "/home", path: "/home",
loader: async () => { loader: async () => {
if (getToken() == null) { return await homeLoader();
return redirect("/login");
}
const ret = await homeLoader();
if (isError(ret)) {
deleteToken();
return redirect("/");
}
return ret;
}, },
element: <Home />, element: <Home />,
children: [{ path: "feed", element: <Feed /> }], children: [
{ path: "feed", element: <Feed /> },
{ path: "messages", element: <Messages /> },
{
path: "profile",
loader: async () => {
return await profileSelfLoader();
},
action: profileSelfAction,
element: <Profile self={true} />,
},
{
path: "profile/:username",
element: <Profile self={false} />,
},
],
}, },
{ {
path: "/login", path: "/login",

View File

@@ -1,4 +1,30 @@
.authForm { .authForm {
width: 20rem;
margin: 0 auto;
display: flex;
flex-direction: column;
padding: 2rem 0;
.errors {
margin: 2rem 0;
color: red;
}
form {
display: flex;
flex-direction: column;
* {
margin: 0.3rem 0;
}
label {
margin-bottom: 0;
}
input {
margin-top: 0;
}
}
} }

59
client/src/Home.scss Normal file
View File

@@ -0,0 +1,59 @@
#Home {
display: flex;
flex-direction: row;
min-height: 100vh;
#HomeSidebar {
min-width: 15rem;
min-height: 100vh;
border-right: solid gray 1px;
border-radius: 7px;
padding: 2rem 1rem;
#SidebarUserInfo {
display: flex;
flex-direction: column;
}
* {
margin: 0.5rem 0;
}
#SidebarNav {
a {
color: black;
text-decoration: none;
min-width: 100%;
height: 2rem;
display: flex;
flex-direction: column;
justify-content: center;
text-align: left;
padding-left: 1rem;
font-size: 1.15rem;
border-radius: 7px;
border: 1px solid #F0F0F0;
&.active {
background-color: #EFEFEF;
}
&.pending {
color: gray;
background-color: #FFFFFF;
}
}
}
}
#HomeContent {
margin: 0;
min-width: 100%;
}
}

View File

@@ -1,21 +1,46 @@
import { Outlet, useLoaderData } from "react-router-dom"; import { NavLink, NavLinkProps, Outlet, useLoaderData } from "react-router-dom";
import { homeLoader } from "./loaders"; import { homeLoader, LoaderToType } from "./loaders";
import { isError } from "./api/dto"; import { isError } from "./api/dto";
import "./Home.scss";
export function Home() { export function Home() {
const loaderData = useLoaderData() as const loaderData = useLoaderData() as LoaderToType<typeof homeLoader>;
| Awaited<ReturnType<typeof homeLoader>>
| undefined;
if (!loaderData || isError(loaderData)) { if (!loaderData || isError(loaderData)) {
return <div>Error</div>; return <div>Error</div>;
} }
const activePendingClassName = ({
isActive,
isPending,
}: {
isActive: boolean;
isPending: boolean;
}) => (isActive ? "active" : isPending ? "pending" : "");
return ( return (
<> <div id="Home">
<a>username: {loaderData.username}</a> <div id="HomeSidebar">
<a>name: {loaderData.fullName}</a> <div id="SidebarUserInfo">
<Outlet /> <a> username: {loaderData.username}</a>
</> <a>name: {loaderData.fullName}</a>
</div>
<div id="SidebarNav">
<NavLink to={"feed"} className={activePendingClassName}>
Feed
</NavLink>{" "}
<NavLink to={"messages"} className={activePendingClassName}>
Messages
</NavLink>
<NavLink to={"profile"} className={activePendingClassName}>
Profile
</NavLink>
</div>
</div>
<div id="HomeContent">
<Outlet />
</div>
</div>
); );
} }

View File

@@ -25,7 +25,7 @@ export function Login() {
return ( return (
<div className="authForm"> <div className="authForm">
{errors} <div className={"errors"}>{errors}</div>
<Form method="post"> <Form method="post">
<label htmlFor="fname">Username:</label> <label htmlFor="fname">Username:</label>
<input type="text" name="username" /> <input type="text" name="username" />

3
client/src/Messages.tsx Normal file
View File

@@ -0,0 +1,3 @@
export function Messages() {
return <a>Messages</a>;
}

60
client/src/Profile.scss Normal file
View File

@@ -0,0 +1,60 @@
.profileView {
min-width: 100%;
display: flex;
flex-direction: column;
.profileInfo {
min-width: 100%;
border-bottom: solid gray 1px;
border-right: solid gray 1px;
border-bottom-right-radius: 7px;
padding: 2rem;
display: flex;
flex-direction: column;
.fullName {
font-size: 1.5rem;
}
.username {
}
}
.newPost {
display: flex;
flex-direction: column;
margin: 2rem;
min-height: 6rem;
border: solid gray 1px;
border-radius: 7px;
form {
min-height: 100%;
display: flex;
flex-direction: row;
flex: auto;
textarea {
flex-grow: 1;
border: 0px;
border-radius: 7px 0 0 7px;
padding: 7px;
resize: none;
}
button {
border: 0px;
border-radius: 0 7px 7px 0;
}
}
}
.posts {
padding: 2rem;
}
}

43
client/src/Profile.tsx Normal file
View File

@@ -0,0 +1,43 @@
import "./Profile.scss";
import { Form, Link, useLoaderData } from "react-router-dom";
import { LoaderToType, profileSelfLoader } from "./loaders";
import { isError } from "./api/dto";
import { ProfileCard } from "./ProfileCard";
export interface IProfileProps {
self: boolean;
}
export function Profile(props: IProfileProps) {
const loaderData = useLoaderData() as LoaderToType<
typeof profileSelfLoader
>;
if (!loaderData || isError(loaderData)) {
return <div>Error</div>;
}
return (
<div className={"profileView"}>
<div className={"profileInfo"}>
<span className={"fullName"}>{loaderData.user.fullName}</span>
<span className={"username"}>{loaderData.user.fullName}</span>
</div>
<div className={"newPost"}>
<Form method="post">
<textarea placeholder={"Write something!"} name="text" />
<button type="submit">Post</button>
</Form>
</div>
<div className={"posts"}>
{loaderData.posts &&
loaderData.posts.map((p) => {
return (
<div key={p.id} className={"post"}>
{p.text}
</div>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,15 @@
.profileCard {
width: 15rem;
height: 5rem;
display: flex;
flex-direction: column;
border: 1px solid #EFEFEF;
border-radius: 7px;
padding: 1rem;
.fullName{
font-size: 1.3rem;
}
.username{
}
}

View File

@@ -0,0 +1,16 @@
import "./ProfileCard.scss";
export function ProfileCard({
username,
fullName,
}: {
username: string;
fullName: string;
}) {
return (
<div className={"profileCard"}>
<span className={"fullName"}>{fullName}</span>
<span className={"username"}>{username}</span>
</div>
);
}

View File

@@ -25,7 +25,7 @@ export function Signup() {
return ( return (
<div className="authForm"> <div className="authForm">
{errors} <div className={"errors"}>{errors}</div>
<Form method="post"> <Form method="post">
<label htmlFor="fname">Username:</label> <label htmlFor="fname">Username:</label>
<input type="text" name="username" /> <input type="text" name="username" />

View File

@@ -3,6 +3,7 @@ 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";
import { setToken } from "./api/utils"; import { setToken } from "./api/utils";
import { post } from "./api/Post";
export async function loginAction({ request }: ActionFunctionArgs) { export async function loginAction({ request }: ActionFunctionArgs) {
const formData = await request.formData(); const formData = await request.formData();
@@ -44,3 +45,8 @@ export async function signupAction({ request }: ActionFunctionArgs) {
return ret; return ret;
} }
export async function profileSelfAction({ request }: ActionFunctionArgs) {
const formData = await request.formData();
return await post(formData.get("text")!.toString());
}

12
client/src/api/Post.ts Normal file
View File

@@ -0,0 +1,12 @@
import { PostToArrResp, PostToResp, TPostToArrResp, TPostToResp } from "./dto";
import { fetchJSONAuth } from "./utils";
export async function post(text: string): Promise<TPostToResp> {
return fetchJSONAuth("/post", "POST", PostToResp, {
text,
});
}
export async function getPosts(author: string): Promise<TPostToArrResp> {
return fetchJSONAuth(`/post?author=${author}`, "GET", PostToArrResp);
}

View File

@@ -41,6 +41,22 @@ export type TTokenTo = z.infer<typeof TokenTo>;
export const TokenToResp = CreateAPIResponse(TokenTo); export const TokenToResp = CreateAPIResponse(TokenTo);
export type TTokenToResp = z.infer<typeof TokenToResp>; export type TTokenToResp = z.infer<typeof TokenToResp>;
export const PostTo = z.object({
id: z.number(),
authorUuid: z.string(),
text: z.string(),
});
export type TPostTo = z.infer<typeof PostTo>;
export const PostToArr = z.array(PostTo);
export type TPostToArr = z.infer<typeof PostToArr>;
export const PostToResp = CreateAPIResponse(PostTo);
export type TPostToResp = z.infer<typeof PostToResp>;
export const PostToArrResp = CreateAPIResponse(PostToArr);
export type TPostToArrResp = z.infer<typeof PostToArrResp>;
export function isError(value: unknown): value is TErrorTo { export function isError(value: unknown): value is TErrorTo {
return ErrorTo.safeParse(value).success; return ErrorTo.safeParse(value).success;
} }

13
client/src/index.scss Normal file
View File

@@ -0,0 +1,13 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
* {
margin: 0;
}
body {
min-height: 100vh;
}

View File

@@ -1,6 +1,8 @@
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { App } from "./App"; import { App } from "./App";
import "./index.scss";
const container = document.getElementById("app")!; const container = document.getElementById("app")!;
const root = createRoot(container); const root = createRoot(container);
root.render(<App />); root.render(<App />);

View File

@@ -1,5 +1,40 @@
import { getSelf } from "./api/Person"; import { getSelf } from "./api/Person";
import { deleteToken, getToken } from "./api/utils";
import { redirect } from "react-router-dom";
import { isError } from "./api/dto";
import { getPosts } from "./api/Post";
export type LoaderToType<T extends (...args: any) => any> =
| Exclude<Awaited<ReturnType<T>>, Response>
| undefined;
export async function getCheckUserSelf() {
if (getToken() == null) {
return redirect("/login");
}
const ret = await getSelf();
if (isError(ret)) {
deleteToken();
return redirect("/");
}
return ret;
}
export async function homeLoader() { export async function homeLoader() {
return await getSelf(); return await getCheckUserSelf();
}
export async function profileSelfLoader() {
const user = await getCheckUserSelf();
if (user instanceof Response) {
return user;
}
const posts = await getPosts(user.uuid);
if (isError(posts)) {
return { user, posts: null };
}
return { user, posts };
} }

View File

@@ -44,7 +44,7 @@ public class PersonController {
return PersonMapper.makeDto(found.get()); return PersonMapper.makeDto(found.get());
} }
@GetMapping(path = "") @GetMapping
public PersonTo getSelf(Principal principal) throws UserNotFoundException { public PersonTo getSelf(Principal principal) throws UserNotFoundException {
Optional<Person> found = personService.readById(principal.getName()); Optional<Person> found = personService.readById(principal.getName());