Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/application/reply-to-shout/reply-to-shout.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { beforeEach, describe, expect, it, vitest } from "vitest";

import { MAX_NUM_SHOUTS_PER_DAY } from "@/domain/me";
import { createMockFile } from "@/test/create-mock-file";

import { ErrorMessages, replyToShout } from "./reply-to-shout";
Expand Down Expand Up @@ -50,7 +51,10 @@ describe("replyToShout", () => {
});

it("should return an error if the user has made too many shouts", async () => {
mockGetMe.mockResolvedValueOnce({ ...mockMe, numShoutsPastDay: 5 });
mockGetMe.mockResolvedValueOnce({
...mockMe,
numShoutsPastDay: MAX_NUM_SHOUTS_PER_DAY,
});

const result = await replyToShout(
{ recipientHandle, shoutId, message, files },
Expand Down
6 changes: 4 additions & 2 deletions src/application/reply-to-shout/reply-to-shout.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useCallback } from "react";

import { hasExceededShoutLimit } from "@/domain/me";
import { hasBlockedUser } from "@/domain/user";
import MediaService from "@/infrastructure/media";
import ShoutService from "@/infrastructure/shout";
import UserService from "@/infrastructure/user";
Expand Down Expand Up @@ -33,15 +35,15 @@ export async function replyToShout(
{ getMe, getUser, saveImage, createReply, createShout }: typeof dependencies
) {
const me = await getMe();
if (me.numShoutsPastDay >= 5) {
if (hasExceededShoutLimit(me)) {
return { error: ErrorMessages.TooManyShouts };
}

const recipient = await getUser(recipientHandle);
if (!recipient) {
return { error: ErrorMessages.RecipientNotFound };
}
if (recipient.blockedUserIds.includes(me.id)) {
if (hasBlockedUser(recipient, me.id)) {
return { error: ErrorMessages.AuthorBlockedByRecipient };
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Link } from "react-router-dom";

import { LoginDialog } from "@/components/login-dialog";
import { Button } from "@/components/ui/button";
import { Me } from "@/domain";
import { Me } from "@/domain/me";
import AuthService from "@/infrastructure/auth";
import UserService from "@/infrastructure/user";

Expand Down
24 changes: 12 additions & 12 deletions src/components/shout-list/shout-list.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Shout } from "@/components/shout";
import { Image, Shout as IShout, User } from "@/domain";
import { Image, getImageById } from "@/domain/media";
import { Shout as IShout } from "@/domain/shout";
import { User, getUserById } from "@/domain/user";

interface ShoutListProps {
shouts: IShout[];
Expand All @@ -10,17 +12,15 @@ interface ShoutListProps {
export function ShoutList({ shouts, users, images }: ShoutListProps) {
return (
<ul className="flex flex-col gap-4 items-center">
{shouts.map((shout) => {
const author = users.find((u) => u.id === shout.authorId);
const image = shout.imageId
? images.find((i) => i.id === shout.imageId)
: undefined;
return (
<li key={shout.id} className="max-w-sm w-full">
<Shout shout={shout} author={author} image={image} />
</li>
);
})}
{shouts.map((shout) => (
<li key={shout.id} className="max-w-sm w-full">
<Shout
shout={shout}
author={getUserById(users, shout.authorId)}
image={getImageById(images, shout.imageId)}
/>
</li>
))}
</ul>
);
}
4 changes: 3 additions & 1 deletion src/components/shout/reply-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { isAuthenticated as isUserAuthenticated } from "@/domain/me";
import UserService from "@/infrastructure/user";

interface ReplyFormElements extends HTMLFormControlsCollection {
Expand Down Expand Up @@ -46,7 +47,8 @@ export function ReplyDialog({

useEffect(() => {
UserService.getMe()
.then((me) => setIsAuthenticated(Boolean(me)))
.then(isUserAuthenticated)
.then(setIsAuthenticated)
.catch(() => setHasError(true))
.finally(() => setIsLoading(false));
}, []);
Expand Down
15 changes: 4 additions & 11 deletions src/components/shout/shout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import {
CardFooter,
CardHeader,
} from "@/components/ui/card";
import { Image, Shout as IShout, User } from "@/domain";
import { Image } from "@/domain/media";
import { Shout as IShout } from "@/domain/shout";
import { User, fallbackAuthor } from "@/domain/user";

import { ReplyDialog } from "./reply-dialog";

Expand All @@ -20,16 +22,7 @@ interface ShoutProps {
image?: Image;
}

const defaultAuthor: User = {
id: "invalid",
handle: "Deleted",
avatar:
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTIgMGMtNi42MjcgMC0xMiA1LjM3My0xMiAxMnM1LjM3MyAxMiAxMiAxMiAxMi01LjM3MyAxMi0xMi01LjM3My0xMi0xMi0xMnptOSAxMmMwIDEuOTQtLjYyNCAzLjczNS0xLjY3MiA1LjIwN2wtMTIuNTM1LTEyLjUzNWMxLjQ3Mi0xLjA0OCAzLjI2Ny0xLjY3MiA1LjIwNy0xLjY3MiA0Ljk2MiAwIDkgNC4wMzggOSA5em0tMTggMGMwLTEuOTQuNjI0LTMuNzM1IDEuNjcyLTUuMjA3bDEyLjUzNCAxMi41MzRjLTEuNDcxIDEuMDQ5LTMuMjY2IDEuNjczLTUuMjA2IDEuNjczLTQuOTYyIDAtOS00LjAzOC05LTl6Ii8+PC9zdmc+",
blockedUserIds: [],
followerIds: [],
};

export function Shout({ shout, author = defaultAuthor, image }: ShoutProps) {
export function Shout({ shout, author = fallbackAuthor, image }: ShoutProps) {
return (
<Card key={shout.id} className="w-full">
<Link to={`/user/${author.handle}`}>
Expand Down
29 changes: 0 additions & 29 deletions src/domain/index.ts

This file was deleted.

1 change: 1 addition & 0 deletions src/domain/me/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./me";
40 changes: 40 additions & 0 deletions src/domain/me/me.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, it } from "vitest";

import { isAuthenticated, hasExceededShoutLimit } from "./me";

const mockMe = {
id: "1",
handle: "test",
avatar: "test",
numShoutsPastDay: 0,
blockedUserIds: [],
followerIds: [],
};

describe("Me domain", () => {
describe("isAuthenticated", () => {
it("should be true is me is defined", () => {
expect(isAuthenticated(mockMe)).toEqual(true);
});

it("should be false if me is not defined", () => {
expect(isAuthenticated(undefined)).toEqual(false);
});
});

describe("hasExceededShoutLimit", () => {
it("should be false if numShoutsPastDay is less than MAX_NUM_SHOUTS_PER_DAY", () => {
expect(hasExceededShoutLimit(mockMe)).toEqual(false);
});

it("should be true if numShoutsPastDay is equal to MAX_NUM_SHOUTS_PER_DAY", () => {
const me = { ...mockMe, numShoutsPastDay: 5 };
expect(hasExceededShoutLimit(me)).toEqual(true);
});

it("should be true if numShoutsPastDay is greater than MAX_NUM_SHOUTS_PER_DAY", () => {
const me = { ...mockMe, numShoutsPastDay: 6 };
expect(hasExceededShoutLimit(me)).toEqual(true);
});
});
});
15 changes: 15 additions & 0 deletions src/domain/me/me.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { User } from "@/domain/user";

export const MAX_NUM_SHOUTS_PER_DAY = 5;

export interface Me extends User {
numShoutsPastDay: number;
}

export function isAuthenticated(me?: Me) {
return Boolean(me);
}

export function hasExceededShoutLimit(me: Me) {
return me.numShoutsPastDay >= MAX_NUM_SHOUTS_PER_DAY;
}
1 change: 1 addition & 0 deletions src/domain/media/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./media";
32 changes: 32 additions & 0 deletions src/domain/media/media.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, expect, it } from "vitest";

import { getImageById } from "./media";

const mockImage = {
id: "1",
url: "test",
};

describe("Media domain", () => {
describe("getImageById", () => {
it("should be able to get image by id", () => {
const image = getImageById([mockImage], "1");
expect(image).toEqual(mockImage);
});

it("should return undefined if image is not found", () => {
const image = getImageById([{ ...mockImage, id: "2" }], "1");
expect(image).toEqual(undefined);
});

it("should return undefined if provided images are not defined", () => {
const image = getImageById(undefined, "1");
expect(image).toEqual(undefined);
});

it("should return undefined if provided image id is not defined", () => {
const image = getImageById([mockImage], undefined);
expect(image).toEqual(undefined);
});
});
});
9 changes: 9 additions & 0 deletions src/domain/media/media.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface Image {
id: string;
url: string;
}

export function getImageById(images?: Image[], imageId?: string) {
if (!imageId || !images) return;
return images.find((i) => i.id === imageId);
}
1 change: 1 addition & 0 deletions src/domain/shout/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./shout";
11 changes: 11 additions & 0 deletions src/domain/shout/shout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface Shout {
id: string;
createdAt: number;
authorId: string;
text: string;
likes: number;
reshouts: number;
imageId?: string;
replies: string[];
replyTo?: string;
}
1 change: 1 addition & 0 deletions src/domain/user/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./user";
60 changes: 60 additions & 0 deletions src/domain/user/user.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, expect, it } from "vitest";

import { getUserById, hasBlockedUser } from "./user";

const mockUser = {
id: "1",
handle: "test",
avatar: "test",
numShoutsPastDay: 0,
blockedUserIds: [],
followerIds: [],
};

describe("User domain", () => {
describe("getUserById", () => {
it("should be able to get user by id", () => {
const user = getUserById([mockUser], "1");
expect(user).toEqual(mockUser);
});

it("should return undefined if user is not found", () => {
const user = getUserById([{ ...mockUser, id: "2" }], "1");
expect(user).toEqual(undefined);
});

it("should return undefined if provided users are not defined", () => {
const user = getUserById(undefined, "1");
expect(user).toEqual(undefined);
});

it("should return undefined if provided user id is not defined", () => {
const user = getUserById([mockUser], undefined);
expect(user).toEqual(undefined);
});
});

describe("hasBlockedUser", () => {
it("should be false if user has not blocked the user", () => {
const user = { ...mockUser, blockedUserIds: ["2"] };
const hasBlocked = hasBlockedUser(user, "3");
expect(hasBlocked).toEqual(false);
});

it("should be true if user has blocked the user", () => {
const user = { ...mockUser, blockedUserIds: ["2"] };
const hasBlocked = hasBlockedUser(user, "2");
expect(hasBlocked).toEqual(true);
});

it("should be false if user is not defined", () => {
const hasBlocked = hasBlockedUser(undefined, "2");
expect(hasBlocked).toEqual(false);
});

it("should be false if user id is not defined", () => {
const hasBlocked = hasBlockedUser(mockUser, undefined);
expect(hasBlocked).toEqual(false);
});
});
});
27 changes: 27 additions & 0 deletions src/domain/user/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export interface User {
id: string;
handle: string;
avatar: string;
info?: string;
blockedUserIds: string[];
followerIds: string[];
}

export const fallbackAuthor: User = {
id: "invalid",
handle: "Deleted",
avatar:
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTIgMGMtNi42MjcgMC0xMiA1LjM3My0xMiAxMnM1LjM3MyAxMiAxMiAxMiAxMi01LjM3MyAxMi0xMi01LjM3My0xMi0xMi0xMnptOSAxMmMwIDEuOTQtLjYyNCAzLjczNS0xLjY3MiA1LjIwN2wtMTIuNTM1LTEyLjUzNWMxLjQ3Mi0xLjA0OCAzLjI2Ny0xLjY3MiA1LjIwNy0xLjY3MiA0Ljk2MiAwIDkgNC4wMzggOSA5em0tMTggMGMwLTEuOTQuNjI0LTMuNzM1IDEuNjcyLTUuMjA3bDEyLjUzNCAxMi41MzRjLTEuNDcxIDEuMDQ5LTMuMjY2IDEuNjczLTUuMjA2IDEuNjczLTQuOTYyIDAtOS00LjAzOC05LTl6Ii8+PC9zdmc+",
blockedUserIds: [],
followerIds: [],
};

export function getUserById(users?: User[], userId?: string) {
if (!userId || !users) return;
return users.find((u) => u.id === userId);
}

export function hasBlockedUser(user?: User, userId?: string) {
if (!user || !userId) return false;
return user.blockedUserIds.includes(userId);
}
2 changes: 1 addition & 1 deletion src/infrastructure/media/transform.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Image } from "@/domain";
import { Image } from "@/domain/media";

import { ImageDto } from "./dto";

Expand Down
2 changes: 1 addition & 1 deletion src/infrastructure/shout/transform.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Shout } from "@/domain";
import { Shout } from "@/domain/shout";

import { ShoutDto } from "./dto";

Expand Down
3 changes: 2 additions & 1 deletion src/infrastructure/user/transform.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Me, User } from "@/domain";
import { Me } from "@/domain/me";
import { User } from "@/domain/user";

import { MeDto, UserDto } from "./dto";

Expand Down
Loading