diff --git a/src/application/reply-to-shout/reply-to-shout.test.ts b/src/application/reply-to-shout/reply-to-shout.test.ts
index fb94a3f..f0c7c0b 100644
--- a/src/application/reply-to-shout/reply-to-shout.test.ts
+++ b/src/application/reply-to-shout/reply-to-shout.test.ts
@@ -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";
@@ -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 },
diff --git a/src/application/reply-to-shout/reply-to-shout.ts b/src/application/reply-to-shout/reply-to-shout.ts
index 91e655f..e55ac2e 100644
--- a/src/application/reply-to-shout/reply-to-shout.ts
+++ b/src/application/reply-to-shout/reply-to-shout.ts
@@ -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";
@@ -33,7 +35,7 @@ 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 };
}
@@ -41,7 +43,7 @@ export async function replyToShout(
if (!recipient) {
return { error: ErrorMessages.RecipientNotFound };
}
- if (recipient.blockedUserIds.includes(me.id)) {
+ if (hasBlockedUser(recipient, me.id)) {
return { error: ErrorMessages.AuthorBlockedByRecipient };
}
diff --git a/src/components/header/header.tsx b/src/components/header/header.tsx
index 679c822..02a1f95 100644
--- a/src/components/header/header.tsx
+++ b/src/components/header/header.tsx
@@ -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";
diff --git a/src/components/shout-list/shout-list.tsx b/src/components/shout-list/shout-list.tsx
index f6e8419..c1cc7a4 100644
--- a/src/components/shout-list/shout-list.tsx
+++ b/src/components/shout-list/shout-list.tsx
@@ -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[];
@@ -10,17 +12,15 @@ interface ShoutListProps {
export function ShoutList({ shouts, users, images }: ShoutListProps) {
return (
- {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 (
- -
-
-
- );
- })}
+ {shouts.map((shout) => (
+ -
+
+
+ ))}
);
}
diff --git a/src/components/shout/reply-dialog.tsx b/src/components/shout/reply-dialog.tsx
index cfe3829..db7ada6 100644
--- a/src/components/shout/reply-dialog.tsx
+++ b/src/components/shout/reply-dialog.tsx
@@ -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 {
@@ -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));
}, []);
diff --git a/src/components/shout/shout.tsx b/src/components/shout/shout.tsx
index 4c77c4d..1a760ee 100644
--- a/src/components/shout/shout.tsx
+++ b/src/components/shout/shout.tsx
@@ -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";
@@ -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 (
diff --git a/src/domain/index.ts b/src/domain/index.ts
deleted file mode 100644
index 432e89c..0000000
--- a/src/domain/index.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-export interface User {
- id: string;
- handle: string;
- avatar: string;
- info?: string;
- blockedUserIds: string[];
- followerIds: string[];
-}
-
-export interface Me extends User {
- numShoutsPastDay: number;
-}
-
-export interface Shout {
- id: string;
- createdAt: number;
- authorId: string;
- text: string;
- likes: number;
- reshouts: number;
- imageId?: string;
- replies: string[];
- replyTo?: string;
-}
-
-export interface Image {
- id: string;
- url: string;
-}
diff --git a/src/domain/me/index.ts b/src/domain/me/index.ts
new file mode 100644
index 0000000..3c25512
--- /dev/null
+++ b/src/domain/me/index.ts
@@ -0,0 +1 @@
+export * from "./me";
diff --git a/src/domain/me/me.test.ts b/src/domain/me/me.test.ts
new file mode 100644
index 0000000..6da381d
--- /dev/null
+++ b/src/domain/me/me.test.ts
@@ -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);
+ });
+ });
+});
diff --git a/src/domain/me/me.ts b/src/domain/me/me.ts
new file mode 100644
index 0000000..0431edf
--- /dev/null
+++ b/src/domain/me/me.ts
@@ -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;
+}
diff --git a/src/domain/media/index.ts b/src/domain/media/index.ts
new file mode 100644
index 0000000..fa8208c
--- /dev/null
+++ b/src/domain/media/index.ts
@@ -0,0 +1 @@
+export * from "./media";
diff --git a/src/domain/media/media.test.ts b/src/domain/media/media.test.ts
new file mode 100644
index 0000000..104c7ab
--- /dev/null
+++ b/src/domain/media/media.test.ts
@@ -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);
+ });
+ });
+});
diff --git a/src/domain/media/media.ts b/src/domain/media/media.ts
new file mode 100644
index 0000000..6a6c7f0
--- /dev/null
+++ b/src/domain/media/media.ts
@@ -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);
+}
diff --git a/src/domain/shout/index.ts b/src/domain/shout/index.ts
new file mode 100644
index 0000000..65b9aa9
--- /dev/null
+++ b/src/domain/shout/index.ts
@@ -0,0 +1 @@
+export * from "./shout";
diff --git a/src/domain/shout/shout.ts b/src/domain/shout/shout.ts
new file mode 100644
index 0000000..41dc34e
--- /dev/null
+++ b/src/domain/shout/shout.ts
@@ -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;
+}
diff --git a/src/domain/user/index.ts b/src/domain/user/index.ts
new file mode 100644
index 0000000..7616f9e
--- /dev/null
+++ b/src/domain/user/index.ts
@@ -0,0 +1 @@
+export * from "./user";
diff --git a/src/domain/user/user.test.ts b/src/domain/user/user.test.ts
new file mode 100644
index 0000000..82f3827
--- /dev/null
+++ b/src/domain/user/user.test.ts
@@ -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);
+ });
+ });
+});
diff --git a/src/domain/user/user.ts b/src/domain/user/user.ts
new file mode 100644
index 0000000..6685d21
--- /dev/null
+++ b/src/domain/user/user.ts
@@ -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);
+}
diff --git a/src/infrastructure/media/transform.ts b/src/infrastructure/media/transform.ts
index 444f756..c967ef7 100644
--- a/src/infrastructure/media/transform.ts
+++ b/src/infrastructure/media/transform.ts
@@ -1,4 +1,4 @@
-import { Image } from "@/domain";
+import { Image } from "@/domain/media";
import { ImageDto } from "./dto";
diff --git a/src/infrastructure/shout/transform.ts b/src/infrastructure/shout/transform.ts
index 4a305d0..99f70e3 100644
--- a/src/infrastructure/shout/transform.ts
+++ b/src/infrastructure/shout/transform.ts
@@ -1,4 +1,4 @@
-import { Shout } from "@/domain";
+import { Shout } from "@/domain/shout";
import { ShoutDto } from "./dto";
diff --git a/src/infrastructure/user/transform.ts b/src/infrastructure/user/transform.ts
index bbb697f..411f4f6 100644
--- a/src/infrastructure/user/transform.ts
+++ b/src/infrastructure/user/transform.ts
@@ -1,4 +1,5 @@
-import { Me, User } from "@/domain";
+import { Me } from "@/domain/me";
+import { User } from "@/domain/user";
import { MeDto, UserDto } from "./dto";
diff --git a/src/pages/feed/feed.tsx b/src/pages/feed/feed.tsx
index 1eea6d3..d9a4614 100644
--- a/src/pages/feed/feed.tsx
+++ b/src/pages/feed/feed.tsx
@@ -2,7 +2,9 @@ import { useEffect, useState } from "react";
import { LoadingView } from "@/components/loading";
import { ShoutList } from "@/components/shout-list";
-import { Image, Shout, User } from "@/domain";
+import { Image } from "@/domain/media";
+import { Shout } from "@/domain/shout";
+import { User } from "@/domain/user";
import FeedService from "@/infrastructure/feed";
export function Feed() {
diff --git a/src/pages/user-profile/user-info.tsx b/src/pages/user-profile/user-info.tsx
index 8a65c23..ea757c9 100644
--- a/src/pages/user-profile/user-info.tsx
+++ b/src/pages/user-profile/user-info.tsx
@@ -1,4 +1,4 @@
-import { User } from "@/domain";
+import { User } from "@/domain/user";
interface UserInfoProps {
user: User;
diff --git a/src/pages/user-profile/user-profile.tsx b/src/pages/user-profile/user-profile.tsx
index 0ecfece..4dc6327 100644
--- a/src/pages/user-profile/user-profile.tsx
+++ b/src/pages/user-profile/user-profile.tsx
@@ -4,7 +4,9 @@ import { Navigate, useParams } from "react-router";
import { useGetUserProfile } from "@/application/get-user-profile";
import { LoadingSpinner } from "@/components/loading";
import { ShoutList } from "@/components/shout-list";
-import { Image, Shout, User } from "@/domain";
+import { Image } from "@/domain/media";
+import { Shout } from "@/domain/shout";
+import { User } from "@/domain/user";
import { UserInfo } from "./user-info";