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 {
font-family: sans-serif;
max-width: 50rem;
margin-left: auto;
margin-right: auto;
margin: 0 auto;
}

View File

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

View File

@@ -1,4 +1,30 @@
.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 { homeLoader } from "./loaders";
import { NavLink, NavLinkProps, Outlet, useLoaderData } from "react-router-dom";
import { homeLoader, LoaderToType } from "./loaders";
import { isError } from "./api/dto";
import "./Home.scss";
export function Home() {
const loaderData = useLoaderData() as
| Awaited<ReturnType<typeof homeLoader>>
| undefined;
const loaderData = useLoaderData() as LoaderToType<typeof homeLoader>;
if (!loaderData || isError(loaderData)) {
return <div>Error</div>;
}
const activePendingClassName = ({
isActive,
isPending,
}: {
isActive: boolean;
isPending: boolean;
}) => (isActive ? "active" : isPending ? "pending" : "");
return (
<>
<a>username: {loaderData.username}</a>
<a>name: {loaderData.fullName}</a>
<Outlet />
</>
<div id="Home">
<div id="HomeSidebar">
<div id="SidebarUserInfo">
<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 (
<div className="authForm">
{errors}
<div className={"errors"}>{errors}</div>
<Form method="post">
<label htmlFor="fname">Username:</label>
<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 (
<div className="authForm">
{errors}
<div className={"errors"}>{errors}</div>
<Form method="post">
<label htmlFor="fname">Username:</label>
<input type="text" name="username" />

View File

@@ -3,6 +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";
export async function loginAction({ request }: ActionFunctionArgs) {
const formData = await request.formData();
@@ -44,3 +45,8 @@ export async function signupAction({ request }: ActionFunctionArgs) {
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 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 {
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 { App } from "./App";
import "./index.scss";
const container = document.getElementById("app")!;
const root = createRoot(container);
root.render(<App />);

View File

@@ -1,5 +1,40 @@
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() {
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());
}
@GetMapping(path = "")
@GetMapping
public PersonTo getSelf(Principal principal) throws UserNotFoundException {
Optional<Person> found = personService.readById(principal.getName());