Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Teollan committed Aug 31, 2024
1 parent aeee258 commit 58a2624
Show file tree
Hide file tree
Showing 37 changed files with 870 additions and 357 deletions.
59 changes: 42 additions & 17 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,17 @@ import { PostsList } from './components/PostsList';
import { PostDetails } from './components/PostDetails';
import { UserSelector } from './components/UserSelector';
import { Loader } from './components/Loader';
import { Notification } from './components/Notification';
import { Future } from './components/Future';

import { useAppContext } from './BLoC/App/AppContext';

import { NotificationType } from './enums';
import { NoUserSelectedError } from './errors/NoUserSelectedError';

export const App: React.FC = () => {
const { posts, selectedPost } = useAppContext();

return (
<main className="section">
<div className="container">
Expand All @@ -21,22 +30,38 @@ export const App: React.FC = () => {
</div>

<div className="block" data-cy="MainContent">
<p data-cy="NoSelectedUser">No user selected</p>

<Loader />

<div
className="notification is-danger"
data-cy="PostsLoadingError"
>
Something went wrong!
</div>

<div className="notification is-warning" data-cy="NoPostsYet">
No posts yet
</div>
<Future
future={posts}
whilePending={() => <Loader />}
whileError={error => {
if (error instanceof NoUserSelectedError) {
return <p data-cy="NoSelectedUser">No user selected</p>;
}

<PostsList />
return (
<Notification
type={NotificationType.Error}
text="Something went wrong!"
data-cy="PostsLoadingError"
/>
);
}}
whileReady={value => {
return (
<>
{value?.length === 0 ? (
<Notification
type={NotificationType.Warning}
text="No posts yet"
data-cy="NoPostsYet"
/>
) : (
<PostsList />
)}
</>
);
}}
/>
</div>
</div>
</div>
Expand All @@ -48,11 +73,11 @@ export const App: React.FC = () => {
'is-parent',
'is-8-desktop',
'Sidebar',
'Sidebar--open',
{ 'Sidebar--open': Boolean(selectedPost) },
)}
>
<div className="tile is-child box is-success ">
<PostDetails />
{selectedPost && <PostDetails />}
</div>
</div>
</div>
Expand Down
107 changes: 107 additions & 0 deletions src/BLoC/App/AppContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React, { PropsWithChildren, useContext, useState } from 'react';

import { Comment, CommentData, Post, User } from '../../types';
import { FutureValue, useFuture } from '../../components/Future';

import { postsService } from '../../services/PostsService';
import { commentsService } from '../../services/CommentsService';
import { getUsers } from '../../utils/users';
import { NoUserSelectedError } from '../../errors/NoUserSelectedError';

type Context = {
users: FutureValue<User[]>;
selectedUser: User | null;

selectUser: (user: User) => void;

posts: FutureValue<Post[]>;
selectedPost: Post | null;

selectPost: (post: Post) => void;
clearPostSelection: () => void;

comments: FutureValue<Comment[]>;

leaveComment: (comment: CommentData) => Promise<void>;
deleteComment: (comment: Comment) => Promise<void>;
};

const AppContext = React.createContext<Context | null>(null);

export const AppContextProvider = ({ children }: PropsWithChildren) => {
const users = useFuture<User[]>(getUsers, []);
const [selectedUser, setSelectedUser] = useState<User | null>(null);

const posts = useFuture(() => {
if (!selectedUser) {
return Promise.reject(new NoUserSelectedError());
}

return postsService.getUserPosts(selectedUser as User);
}, [selectedUser]);
const [selectedPost, setSelectedPost] = useState<Post | null>(null);

const comments = useFuture(() => {
if (!selectedPost) {
return Promise.reject();
}

return commentsService.getPostComments(selectedPost as Post);
}, [selectedPost]);

function selectUser(user: User) {
setSelectedUser(user);
setSelectedPost(null);
}

function selectPost(post: Post) {
setSelectedPost(post);
}

function clearPostSelection() {
setSelectedPost(null);
}

async function leaveComment(comment: CommentData) {
const newComment = await commentsService.leaveComment(
selectedPost as Post,
comment,
);

comments.setImmediately([...comments.value, newComment]);
}

async function deleteComment(target: Comment) {
comments.setImmediately(
comments.value.filter(comment => comment.id !== target.id),
);

try {
await commentsService.deleteComment(target);
} catch {
comments.setImmediately([...comments.value]);
}
}

const context: Context = {
users,
selectedUser,

selectUser,

posts,
selectedPost,

selectPost,
clearPostSelection,

comments,

leaveComment,
deleteComment,
};

return <AppContext.Provider value={context}>{children}</AppContext.Provider>;
};

export const useAppContext = () => useContext(AppContext) as Context;
33 changes: 33 additions & 0 deletions src/components/CommentCard/CommentCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useAppContext } from '../../BLoC/App/AppContext';
import { Comment } from '../../types';

type Props = {
comment: Comment;
};

export const CommentCard = ({ comment }: Props) => {
const { deleteComment } = useAppContext();

return (
<article className="message is-small" data-cy="Comment">
<div className="message-header">
<a href={`mailto:${comment.email}`} data-cy="CommentAuthor">
{comment.name}
</a>
<button
onClick={() => deleteComment(comment)}
data-cy="CommentDelete"
type="button"
className="delete is-small"
aria-label="delete"
>
delete button
</button>
</div>

<div className="message-body" data-cy="CommentBody">
{comment.body}
</div>
</article>
);
};
1 change: 1 addition & 0 deletions src/components/CommentCard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './CommentCard';
31 changes: 31 additions & 0 deletions src/components/Future/Future.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ReactNode } from 'react';
import { FutureValue, FutureState } from './useFuture';

type Props<T> = {
future: FutureValue<T>;
whilePending?: () => ReactNode;
whileError?: (error: unknown) => ReactNode;
whileReady?: (value: T) => ReactNode;
};

export function Future<T>({
future,
whilePending,
whileError,
whileReady,
}: Props<T>) {
return (
<>
{(() => {
switch (future.state) {
case FutureState.Pending:
return whilePending && whilePending();
case FutureState.Error:
return whileError && whileError(future.error);
case FutureState.Ready:
return whileReady && whileReady(future.value);
}
})()}
</>
);
}
2 changes: 2 additions & 0 deletions src/components/Future/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './Future';
export * from './useFuture';
52 changes: 52 additions & 0 deletions src/components/Future/useFuture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect, useState } from 'react';

export enum FutureState {
Pending,
Ready,
Error,
}

export type FutureValue<T> = {
value: T;
setImmediately: (value: T) => void;
state: FutureState;
error?: unknown;
};

export function useFuture<T>(
promise: () => Promise<T>,
deps: React.DependencyList,
): FutureValue<T> {
const [value, setValue] = useState<T | undefined>(undefined);
const [state, setState] = useState(FutureState.Pending);
const [error, setError] = useState<unknown>(undefined);

useEffect(() => {
async function update() {
setState(FutureState.Pending);

try {
setValue(await promise());
setState(FutureState.Ready);
} catch (e) {
setError(e);
setState(FutureState.Error);
}
}

update();
}, deps);

return {
get value() {
return value as T;
},
setImmediately: (newValue: T) => {
setValue(newValue);
setState(FutureState.Ready);
},
state,
error,
};
}
3 changes: 2 additions & 1 deletion src/components/Loader/Loader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import './Loader.scss';

export const Loader = () => (
export const Loader: React.FC = () => (
<div className="Loader" data-cy="Loader">
<div className="Loader__content" />
</div>
Expand Down
Loading

0 comments on commit 58a2624

Please sign in to comment.