diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..901336d --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +PORT=5000 +REACT_APP_API_BASE_URL=http://localhost:3000 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4d29575..017ccd2 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ # misc .DS_Store +.env .env.local .env.development.local .env.test.local @@ -21,3 +22,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +.idea \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b4446c4..8e312ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,12 +15,15 @@ "@types/node": "^16.18.119", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", + "lucide-react": "^0.454.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.53.1", "react-router-dom": "^6.27.0", "react-scripts": "5.0.1", "typescript": "^4.9.5", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "zustand": "^5.0.1" }, "devDependencies": { "tailwindcss": "^3.4.14" @@ -11257,6 +11260,14 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.454.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.454.0.tgz", + "integrity": "sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -13906,6 +13917,22 @@ "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==", "license": "MIT" }, + "node_modules/react-hook-form": { + "version": "7.53.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.1.tgz", + "integrity": "sha512-6aiQeBda4zjcuaugWvim9WsGqisoUk+etmFEsSUMm451/Ic8L/UAb7sRtMj3V+Hdzm6mMjU1VhiSzYUZeBm0Vg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -17472,6 +17499,35 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.1.tgz", + "integrity": "sha512-pRET7Lao2z+n5R/HduXMio35TncTlSW68WsYBq2Lg1ASspsNGjpwLAsij3RpouyV6+kHMwwwzP0bZPD70/Jx/w==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index b9c508a..e849e26 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,15 @@ "@types/node": "^16.18.119", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", + "lucide-react": "^0.454.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.53.1", "react-router-dom": "^6.27.0", "react-scripts": "5.0.1", "typescript": "^4.9.5", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "zustand": "^5.0.1" }, "scripts": { "start": "react-scripts start", diff --git a/src/App.tsx b/src/App.tsx index c5f57e7..7f2c7c9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,14 @@ import { Outlet } from "react-router-dom"; + import Navigation from "./components/navigation"; +import Notifications from "./components/notifications/notifications.component"; + export default function App() { return ( -
- +
+ +
); } diff --git a/src/adapters/message.adapter.ts b/src/adapters/message.adapter.ts new file mode 100644 index 0000000..6799064 --- /dev/null +++ b/src/adapters/message.adapter.ts @@ -0,0 +1,6 @@ +import Message from "../types/message"; + +export interface MessageAdapter { + sendMessage: (message: Message) => Promise; + fetchMessages: (receiverId: string) => Promise; +} \ No newline at end of file diff --git a/src/assets/images/block-cursor.png b/src/assets/images/block-cursor.png new file mode 100644 index 0000000..e51225d Binary files /dev/null and b/src/assets/images/block-cursor.png differ diff --git a/src/assets/images/normal-cursor.png b/src/assets/images/normal-cursor.png new file mode 100644 index 0000000..f9a2f7a Binary files /dev/null and b/src/assets/images/normal-cursor.png differ diff --git a/src/assets/images/pointer-cursor.png b/src/assets/images/pointer-cursor.png new file mode 100644 index 0000000..5dd671f Binary files /dev/null and b/src/assets/images/pointer-cursor.png differ diff --git a/src/assets/images/text-cursor.png b/src/assets/images/text-cursor.png new file mode 100644 index 0000000..c2122b7 Binary files /dev/null and b/src/assets/images/text-cursor.png differ diff --git a/src/common/error.common.ts b/src/common/error.common.ts new file mode 100644 index 0000000..0a8ce42 --- /dev/null +++ b/src/common/error.common.ts @@ -0,0 +1,23 @@ +export class CommonError extends Error { + public statusCode: number; + public errorCode: string; + public details?: any; + + constructor(statusCode: number, errorCode: string, message: string, details?: any) { + super(message); + this.statusCode = statusCode; + this.errorCode = errorCode; + this.details = details; + + Object.setPrototypeOf(this, CommonError.prototype); + } + + toJSON() { + return { + statusCode: this.statusCode, + errorCode: this.errorCode, + message: this.message, + details: this.details || null, + }; + } +} diff --git a/src/components/add-friend.component.tsx b/src/components/add-friend.component.tsx new file mode 100644 index 0000000..bd8d3b9 --- /dev/null +++ b/src/components/add-friend.component.tsx @@ -0,0 +1,100 @@ +import { useUserStore } from "../stores/user.store"; +import { SubmitHandler, useForm } from "react-hook-form"; +import { sendFriendRequest } from "../services/friend-request.service"; +import ShareButton from "./send-invite-link.component"; +import { useState } from "react"; +import Button from "./buttons/button"; + +type FormInputs = { + content: string; +} + +export default function AddFriend() { + const user = useUserStore(); + const [error, setError] = useState(null); + const { register, handleSubmit, reset } = useForm({ + defaultValues: { + content: "" + } + }); + + const onSubmit: SubmitHandler = async (input) => { + try { + await sendFriendRequest(input.content); + reset(); + } catch (err) { + console.error(err); + setError((err as Error).message); + } + } + + const sendFriendRequest = async (receiverId: string) => { + try { + const randomuuid = crypto.randomUUID(); + const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}/social/friend-request/${randomuuid}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ receiverId: receiverId }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message); + } + } catch (error) { + console.error('Error sending friend request:', error); + throw error; + } + } + + return ( +
+
+
+
+

☺☻

+

Adress Book

+ +
+

This console's Wii number

+
+

{user.id}

+
+
+
+
+
+
+ +
+
+

Wii Number

+
+
+ +
+
+ +
+ ); +} \ No newline at end of file diff --git a/src/components/buttons/button.tsx b/src/components/buttons/button.tsx new file mode 100644 index 0000000..0d98284 --- /dev/null +++ b/src/components/buttons/button.tsx @@ -0,0 +1,28 @@ +interface ButtonProps { + label: string; + onClick?: (params?: any) => any; + type?: "submit" | "button"; + disabled?: boolean; + variant?: "primary" | "secondary" | "tertiary"; + className?: string; +} + +export default function Button({ + label, + onClick, + type = "button", + disabled, + variant = "primary", + className, +}: ButtonProps) { + return ( + + ); +} diff --git a/src/components/buttons/icon.button.tsx b/src/components/buttons/icon.button.tsx new file mode 100644 index 0000000..b83d2e4 --- /dev/null +++ b/src/components/buttons/icon.button.tsx @@ -0,0 +1,17 @@ +interface IconButtonProps { + children?: React.ReactNode; + onClick: (params?: any) => any; + className?: string; +} + +export default function IconButton({ + children, + onClick, + className, +}: IconButtonProps) { + return ( + + ); +} diff --git a/src/components/cards/friend-placeholder.card.css b/src/components/cards/friend-placeholder.card.css new file mode 100644 index 0000000..eddc4f4 --- /dev/null +++ b/src/components/cards/friend-placeholder.card.css @@ -0,0 +1,9 @@ +.friend-card-placeholder { + @apply border-4 border-zinc-400 aspect-video rounded-[5%/40%] bg-slate-50 shadow-[inset_0px_-64px_32px_rgba(0,0,0,0.2)] cursor-not-allowed; + @apply bg-gradient-to-b from-zinc-300 via-zinc-300 to-zinc-300; + @apply bg-[length:7px_7px]; +} + +.friend-card-placeholder:hover { + cursor: url("../../assets/images/block-cursor.png"), not-allowed; +} diff --git a/src/components/cards/friend-placeholder.card.tsx b/src/components/cards/friend-placeholder.card.tsx new file mode 100644 index 0000000..8668a6f --- /dev/null +++ b/src/components/cards/friend-placeholder.card.tsx @@ -0,0 +1,9 @@ +import "./friend-placeholder.card.css"; + +export default function FriendCardPlaceholder() { + return ( +
+ Wii Chat +
+ ); +} diff --git a/src/components/cards/friend.card.style.css b/src/components/cards/friend.card.style.css new file mode 100644 index 0000000..70a4633 --- /dev/null +++ b/src/components/cards/friend.card.style.css @@ -0,0 +1,32 @@ +@layer { + .friend-card { + @apply flex justify-center items-center font-bold text-2xl uppercase; + @apply cursor-pointer border-4 border-zinc-400 aspect-video rounded-[5%/40%] bg-slate-50 shadow-[inset_0px_-64px_32px_rgba(0,0,0,0.2)] hover:scale-105 transition-all duration-300; + @apply bg-[length:7px_7px]; + } + + .friend-card:hover { + cursor: url("../../assets/images/pointer-cursor.png"), not-allowed; + } +} + +.friend-card-red { + @apply friend-card; + @apply text-red-400; + @apply border-4 border-red-300 bg-red-50 shadow-[inset_0px_-64px_32px_rgba(255,0,0,0.2)]; + @apply bg-gradient-to-b from-red-100 via-red-200 to-red-200; +} + +.friend-card-blue { + @apply friend-card; + @apply text-blue-500; + @apply border-4 border-blue-300 bg-blue-50 shadow-[inset_0px_-64px_32px_rgba(0,0,255,0.2)]; + @apply bg-gradient-to-b from-blue-200 via-blue-200 to-blue-200; +} + +.friend-card-green { + @apply friend-card; + @apply text-green-500; + @apply border-4 border-green-300 bg-green-50 shadow-[inset_0px_-64px_32px_rgba(0,255,0,0.2)]; + @apply bg-gradient-to-b from-green-200 via-green-200 to-green-200; +} diff --git a/src/components/cards/friend.card.tsx b/src/components/cards/friend.card.tsx new file mode 100644 index 0000000..a279de5 --- /dev/null +++ b/src/components/cards/friend.card.tsx @@ -0,0 +1,24 @@ +import { Friend } from "../../types/friend"; +import "./friend.card.style.css"; + +interface FriendCardProps { + friend: Friend; +} + +export default function FriendCard({ friend }: FriendCardProps) { + const cardClasses = [ + "friend-card-red", + "friend-card-blue", + "friend-card-green", + ]; + + const getRandomCardClass = () => { + return cardClasses[Math.floor(Math.random() * cardClasses.length)]; + }; + + return ( +
+ {friend.username} +
+ ); +} diff --git a/src/components/cards/message.card.tsx b/src/components/cards/message.card.tsx new file mode 100644 index 0000000..23213fe --- /dev/null +++ b/src/components/cards/message.card.tsx @@ -0,0 +1,74 @@ +import Message from "../../types/message"; +import { dateFormater } from "../../utils/dateFormater"; + +interface MessageCardProps { + message: Message; + isSender?: boolean; + error?: boolean; + handleError?: () => void; +} + +export default function MessageCard({ + message, + isSender = true, + error = false, + handleError, +}: MessageCardProps) { + return ( +
+
+ +
+

+ {error ? ( +

+ Error{" "} + + resend message + {" "} +

+ ) : ( + dateFormater(message.sendAt) + )} +

+
+ ); +} + +interface MessageTextProps { + message: string; +} +function MessageText({ message }: MessageTextProps) { + return ( +
+ {message.split(" ").map((word, index) => { + if (word.startsWith("http://") || word.startsWith("https://")) { + return ( + + {word + " "} + + ); + } else { + return {word} ; + } + })} +
+ ); +} diff --git a/src/components/forms/auth.form.tsx b/src/components/forms/auth.form.tsx new file mode 100644 index 0000000..da6826e --- /dev/null +++ b/src/components/forms/auth.form.tsx @@ -0,0 +1,62 @@ +import { useState } from "react"; +import { useForm, SubmitHandler } from "react-hook-form"; +import { UserDTO } from "../../dtos/user.dto"; +import Button from "../buttons/button"; + +interface AuthFormProps { + submitFn: (data: UserDTO) => Promise; + children?: React.ReactNode; + label: string; +} + +export default function AuthForm({ submitFn, children, label }: AuthFormProps) { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm(); + + const [resError, setResError] = useState(); + + const onSubmit: SubmitHandler = async (data) => { + try { + await submitFn(data); + } catch (error: any) { + setResError(error.message); + } + }; + + return ( +
+ + + {resError && {resError}} + {children ?? children} +
+ + + + + + + ); +} \ No newline at end of file diff --git a/src/components/guards/guest-route.guard.tsx b/src/components/guards/guest-route.guard.tsx new file mode 100644 index 0000000..e899d7a --- /dev/null +++ b/src/components/guards/guest-route.guard.tsx @@ -0,0 +1,19 @@ +import { Outlet, useNavigate } from "react-router-dom"; +import { useEffect } from "react"; +import { checkUserAuth } from "../../services/auth.service"; + +export default function GuestRoute() { + const navigate = useNavigate(); + + const checkAuth = async () => { + const isAuth = await checkUserAuth(); + if (isAuth) { + navigate("/"); + } + }; + + useEffect(() => { + checkAuth(); + }, []); + return ; +} diff --git a/src/components/guards/procteded-route.guard.tsx b/src/components/guards/procteded-route.guard.tsx new file mode 100644 index 0000000..ee41ebf --- /dev/null +++ b/src/components/guards/procteded-route.guard.tsx @@ -0,0 +1,37 @@ +import { Outlet, useLoaderData, useNavigate } from "react-router-dom"; +import { useEffect } from "react"; +import { checkUserAuth, getCurrentUser } from "../../services/auth.service"; +import { useUserStore } from "../../stores/user.store"; +import { useFriendStore } from "../../stores/friend.store"; +import { useNotificationStore } from "../../stores/notification.store"; +import Notification from "../../types/notification"; + +export default function ProtectedRoute() { + const navigate = useNavigate(); + const { id, updateUser, clearUser } = useUserStore(); + const { clearFriends } = useFriendStore(); + const { setNotifications } = useNotificationStore(); + + useEffect(() => { + const checkAuth = async () => { + const isAuth = await checkUserAuth(); + if (!isAuth) { + clearUser(); + clearFriends(); + navigate("/login"); + } else if (!id && isAuth) { + const currentUser = await getCurrentUser(); + updateUser(currentUser); + } + }; + + checkAuth(); + }, [id, navigate, clearUser, clearFriends, updateUser]); + + const initialNotifications = useLoaderData() as Notification[]; + useEffect(() => { + setNotifications(initialNotifications); + }, [initialNotifications, setNotifications]); + + return ; +} diff --git a/src/components/loaders/messages.loader.tsx b/src/components/loaders/messages.loader.tsx new file mode 100644 index 0000000..54ae657 --- /dev/null +++ b/src/components/loaders/messages.loader.tsx @@ -0,0 +1,8 @@ +import { MessageService } from "../../services/message.service"; + +export const MessagesLoader = async ({ params }: { params: Record }) => { + const messageService = new MessageService(); + const { receiverId } = params; + if (!receiverId) throw new Error("Receiver ID missing"); + return await messageService.fetchMessages(receiverId); +}; diff --git a/src/components/loaders/notifications.loader.tsx b/src/components/loaders/notifications.loader.tsx new file mode 100644 index 0000000..4ac0fc2 --- /dev/null +++ b/src/components/loaders/notifications.loader.tsx @@ -0,0 +1,14 @@ +import Notification from "../../types/notification"; + +export const NotificationsLoader = ({ + params, +}: { + params: Record; +}) => { + const storedNotifications: string | null = + localStorage.getItem("notifications"); + const parsedNotifications: Notification[] = storedNotifications + ? JSON.parse(storedNotifications) + : []; + return parsedNotifications; +}; diff --git a/src/components/loaders/spinner/loader.component.tsx b/src/components/loaders/spinner/loader.component.tsx new file mode 100644 index 0000000..60c5837 --- /dev/null +++ b/src/components/loaders/spinner/loader.component.tsx @@ -0,0 +1,9 @@ +import './loader.css'; + +export default function Loader() { + return ( +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/loaders/spinner/loader.css b/src/components/loaders/spinner/loader.css new file mode 100644 index 0000000..3cb8dd0 --- /dev/null +++ b/src/components/loaders/spinner/loader.css @@ -0,0 +1,16 @@ +.loader { + width: 50px; + padding: 8px; + aspect-ratio: 1; + border-radius: 50%; + background: #25b09b; + --_m: + conic-gradient(#0000 10%,#000), + linear-gradient(#000 0 0) content-box; + -webkit-mask: var(--_m); + mask: var(--_m); + -webkit-mask-composite: source-out; + mask-composite: subtract; + animation: l3 1s infinite linear; +} +@keyframes l3 {to{transform: rotate(1turn)}} \ No newline at end of file diff --git a/src/components/navigation.tsx b/src/components/navigation.tsx index 1704522..76c06a5 100644 --- a/src/components/navigation.tsx +++ b/src/components/navigation.tsx @@ -1,12 +1,78 @@ -import * as React from "react"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; +import { logoutUser } from "../services/auth.service"; +import { useFriendStore } from "../stores/friend.store"; +import { useUserStore } from "../stores/user.store"; +import { countUnseenNotifications } from "../utils/count-unseen-notifications"; +import { useEffect } from "react"; +import { useNotificationStore } from "../stores/notification.store"; +import IconButton from "./buttons/icon.button"; + +import { DoorOpen, Users, Mail } from "lucide-react"; export default function Navigation() { + const { clearUser } = useUserStore(); + const { clearFriends } = useFriendStore(); + const { notifications } = useNotificationStore(); + const navigate = useNavigate(); + + const handleLogout = () => { + clearUser(); + clearFriends(); + logoutUser(); + navigate("/login"); + }; + + useEffect(() => { + countUnseenNotifications(notifications); + }, [notifications]); + return ( -
- Chats - Friends - Settings +
+
+
+ navigate("/")}> +
Chats
+
+ navigate("/friends")} + > + + +
+

+ Wii Chat! +

+
+ navigate("/notifications")} + > + + + + + handleLogout()}> + + +
+
); -} \ No newline at end of file +} diff --git a/src/components/notifications/friend-request-accepted.component.tsx b/src/components/notifications/friend-request-accepted.component.tsx new file mode 100644 index 0000000..bd41de2 --- /dev/null +++ b/src/components/notifications/friend-request-accepted.component.tsx @@ -0,0 +1,39 @@ +import { useEffect } from "react"; +import { countUnseenNotifications } from "../../utils/count-unseen-notifications"; +import Notification from "../../types/notification"; +import { useNotificationStore } from "../../stores/notification.store"; +import { NotificationService, EventName } from "../../services/notification.service"; + +export default function FriendRequestAccepted({ notificationService }: { notificationService: NotificationService }) { + + const { notifications, addNotification } = useNotificationStore(); + + function saveAcceptedRequest(request: any) { + const notification: Notification = { + id: crypto.randomUUID(), + type: "friend-request-accepted", + emitterId: request.userId, + receivedAt: new Date().toISOString(), + status: "my-friend-request-accepted", + isSeen: false, + } + + addNotification(notification); + countUnseenNotifications(notifications); + } + + + + useEffect(() => { + const handleAcceptedFriendRequest = (request: any) => { + saveAcceptedRequest(request); + } + const eventSource = notificationService.eventListener(handleAcceptedFriendRequest, EventName.FRIEND_REQUEST_ACCEPTED); + + return () => { + eventSource.close(); + }; + }, []) + + return null +} \ No newline at end of file diff --git a/src/components/notifications/friend-request-received.component.tsx b/src/components/notifications/friend-request-received.component.tsx new file mode 100644 index 0000000..249a936 --- /dev/null +++ b/src/components/notifications/friend-request-received.component.tsx @@ -0,0 +1,42 @@ +import { useEffect } from "react"; +import { countUnseenNotifications } from "../../utils/count-unseen-notifications"; +import Notification from "../../types/notification"; +import { useNotificationStore } from "../../stores/notification.store"; +import { NotificationService, EventName } from "../../services/notification.service"; + +export default function FriendRequestReceived({ notificationService }: { notificationService: NotificationService }) { + const { notifications, addNotification } = useNotificationStore(); + + function saveReceivedRequest(request: any) { + const notification: Notification = { + id: request.id, + type: "friend-request-received", + emitterId: request.senderId, + receivedAt: request.requestedAt, + didIAccept: false, + status: "pending-request", + isSeen: false, + }; + addNotification(notification); + const existingNotifications = JSON.parse( + localStorage.getItem("notifications") || "[]" + ); + const updatedNotifications = [notification, ...existingNotifications]; + localStorage.setItem("notifications", JSON.stringify(updatedNotifications)); + countUnseenNotifications(notifications); + } + + useEffect(() => { + + const handleNewFriendRequest = (request: any) => { + saveReceivedRequest(request); + }; + const eventSource = notificationService.eventListener(handleNewFriendRequest, EventName.FRIEND_REQUEST_RECEIVED); + + return () => { + eventSource.close(); + }; + }, []); + + return null; +} diff --git a/src/components/notifications/notifications.component.tsx b/src/components/notifications/notifications.component.tsx new file mode 100644 index 0000000..017af47 --- /dev/null +++ b/src/components/notifications/notifications.component.tsx @@ -0,0 +1,16 @@ +import FriendRequestReceived from "./friend-request-accepted.component"; +import FriendRequestAccepted from "./friend-request-received.component"; +import { NotificationService } from "../../services/notification.service"; +import { useNotificationStore } from "../../stores/notification.store"; + +export default function Notifications() { + + const { service } = useNotificationStore(); + + return ( + <> + + + + ); +} diff --git a/src/components/send-invite-link.component.tsx b/src/components/send-invite-link.component.tsx new file mode 100644 index 0000000..94e9035 --- /dev/null +++ b/src/components/send-invite-link.component.tsx @@ -0,0 +1,35 @@ +import { Share } from 'lucide-react'; +import { useUserStore } from '../stores/user.store'; +import Button from './buttons/button'; + +const ShareButton = ({ title = 'Wii chat friend request', text = 'Hey I want to be your friend on Wii chat' }) => { + const user = useUserStore(); + + const handleShare = async () => { + + if (navigator.share) { + try { + await navigator.share({ + title, + text, + url: 'invite-link/' + user.id + }); + } catch (err) { + + } + } else { + alert('Web Share API is not supported in your browser'); + } + }; + + return ( + - -
- - ))} - - {friends.length} Friends - -
- {friends.map((friend) => ( -
- profile -
- ))} -
- - - - ); -} \ No newline at end of file diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx deleted file mode 100644 index 3081f29..0000000 --- a/src/pages/HomePage.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function HomePage() { - return ( -
-

Home Page

-
- ); -} \ No newline at end of file diff --git a/src/pages/ChatPage.tsx b/src/pages/chat-list.page.tsx similarity index 85% rename from src/pages/ChatPage.tsx rename to src/pages/chat-list.page.tsx index 8302423..7156492 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/chat-list.page.tsx @@ -1,19 +1,12 @@ import { useEffect, useState } from "react"; import data from "../mock/user-data.json"; +import Chat from "../types/chat"; -interface Chat { - id: number; - name: string; - username: string; - profilePicture: string; - createdAt: string; -} -export default function ChatPage() { +export default function ChatListPage() { const [chats, setChats] = useState([]); useEffect(() => { - console.log(data); setChats(data); }, []); diff --git a/src/pages/chat.page.tsx b/src/pages/chat.page.tsx new file mode 100644 index 0000000..4ef6a65 --- /dev/null +++ b/src/pages/chat.page.tsx @@ -0,0 +1,174 @@ +import { useLoaderData, useNavigate, useParams } from "react-router-dom"; +import { SubmitHandler, useForm } from "react-hook-form"; +import { useEffect, useRef, useState } from "react"; + +import Message from "../types/message"; +import Button from "../components/buttons/button"; +import MessageCard from "../components/cards/message.card"; +import { useFriendStore } from "../stores/friend.store"; +import { Friend } from "../types/friend"; + +import { useMessageStore } from "../stores/message.store"; +import { useUserStore } from "../stores/user.store"; + +import { + FalseMessageService, + MessageService, +} from "../services/message.service"; +import { MessageAdapter } from "../adapters/message.adapter"; +import { BadRequestError } from "../errors/bad-request.error"; +import { useNotificationStore } from "../stores/notification.store"; +import { EventName } from "../services/notification.service"; + +const MAX_MESSAGE_LENGTH = 255; + +export default function ChatPage() { + type FormInputs = { + content: string; + }; + + const initialMessages = useLoaderData() as Message[]; + + const navigate = useNavigate(); + + const { receiverId } = useParams(); + + const { messages, setMessages, addMessage, updateErrorLastMessage } = + useMessageStore(); + const { id } = useUserStore(); + + const { service } = useNotificationStore(); + + const messageService: MessageAdapter = new FalseMessageService(); + // const messageService: MessageAdapter = new MessageService(); + + const { getFriendById } = useFriendStore(); + + const [currentFriend, setCurrentFriend] = useState(); + + const [charactersLeft, setCharactersLeft] = useState(MAX_MESSAGE_LENGTH); + + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ + defaultValues: { + content: "", + }, + }); + + const sendMessage = async (message: Message) => { + try { + await messageService.sendMessage(message); + } catch (error) { + if (error instanceof BadRequestError) { + navigate("/"); + } else { + updateErrorLastMessage(true); + } + } + }; + + const onSubmit: SubmitHandler = async (input) => { + const messageId = crypto.randomUUID(); + if (!receiverId) return; + const message: Message = { + id: messageId, + content: input.content, + receiverId: receiverId, + emitterId: id, + sendAt: new Date().toISOString(), + }; + addMessage(message); + await sendMessage(message); + setCharactersLeft(MAX_MESSAGE_LENGTH); + reset(); + }; + + const retryMessage = async (message: Message) => { + updateErrorLastMessage(false); + await sendMessage(message); + }; + + useEffect(() => { + if (!receiverId) return; + + setCurrentFriend(getFriendById(receiverId)); + setMessages(initialMessages); + + const handleNewMessage = (message: Message) => { + addMessage(message); + }; + + const eventSource = service.eventListener( + handleNewMessage, + EventName.MESSAGE_RECEIVED + ); + + return () => { + eventSource.close(); + }; + }, [receiverId]); + + const messagesBox = useRef(null); + function scrollToBottom() { + if (messagesBox.current) { + messagesBox.current.scrollTop = messagesBox.current.scrollHeight; + } + } + useEffect(() => { + scrollToBottom(); + console.log(messages); + }, [messages]); + + return ( +
+ {currentFriend && ( +

{currentFriend.username} Chat

+ )} +
+ {messages.map((message, index) => ( + retryMessage(message)} + /> + ))} +
+ +
+
+ +
+
+ {charactersLeft !== MAX_MESSAGE_LENGTH && ( +

+ {charactersLeft} characters left +

+ )} +
+ ); +} diff --git a/src/pages/friend.page.tsx b/src/pages/friend.page.tsx new file mode 100644 index 0000000..befbdb2 --- /dev/null +++ b/src/pages/friend.page.tsx @@ -0,0 +1,71 @@ +import { useEffect, useState } from "react"; +import FriendRequestCard from "../components/friend-request-card.component"; +import { fetchFriendRequests } from "../services/friend-request.service"; +import Loader from "../components/loaders/spinner/loader.component"; +import { FriendRequest } from "../types/friend-request"; +import AddFriend from "../components/add-friend.component"; +import { useNotificationStore } from "../stores/notification.store"; +import { EventName } from "../services/notification.service"; + +export default function FriendPage() { + const [friendRequests, setFriendRequests] = useState([]); + const [loading, setLoading] = useState(true); + const { service } = useNotificationStore(); + + const removeFriendRequest = (friendRequest: FriendRequest) => { + setFriendRequests((prevRequests) => + prevRequests.filter((request) => request.id !== friendRequest.id) + ); + }; + + + useEffect(() => { + loadInitialRequests(); + + const handleNewFriendRequest = (data: any) => { + setFriendRequests(prevRequests => { return [data, ...prevRequests]; }); + }; + const eventSource = service.eventListener(handleNewFriendRequest, EventName.FRIEND_REQUEST_RECEIVED); + + return () => { + eventSource.close(); + }; + + }, []); + + + + + + async function loadInitialRequests() { + try { + const requests = await fetchFriendRequests(); + setFriendRequests(requests); + } catch (error) { + console.error("Error loading friend requests:", error); + } finally { + setLoading(false); + } + } + + return ( +
+ {loading ? : null} + +
+
+ +
+ {/* Friends Requests */} + + {friendRequests.map((request) => ( +
+ +
+ ))} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/pages/home.page.tsx b/src/pages/home.page.tsx new file mode 100644 index 0000000..8707196 --- /dev/null +++ b/src/pages/home.page.tsx @@ -0,0 +1,54 @@ +import { useEffect } from "react"; +import { getUserFriends } from "../services/friend.service"; +import { useNavigate } from "react-router-dom"; +import { useFriendStore } from "../stores/friend.store"; +import FriendCard from "../components/cards/friend.card"; +import FriendCardPlaceholder from "../components/cards/friend-placeholder.card"; + +export default function HomePage() { + const { friends, setFriends } = useFriendStore(); + + const loadFriends = async () => { + if (friends.length === 0) { + const friends = await getUserFriends(); + setFriends(friends); + } + }; + + const sortedByDateFriendsList = friends.sort((a, b) => { + return new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime(); + }); + + useEffect(() => { + loadFriends(); + }, []); + + const navigate = useNavigate(); + + const redirectUserToChatPage = ( + event: React.MouseEvent, + userId: string + ) => { + event.preventDefault(); + navigate(`/chats/${userId}`); + }; + + return ( +
+
    + {sortedByDateFriendsList.map((friend) => ( +
  • redirectUserToChatPage(e, friend.userId)} + key={friend.userId} + > + +
  • + ))} + {friends.length < 20 && + Array.from({ length: 20 - friends.length }).map((_, index) => ( + + ))} +
+
+ ); +} diff --git a/src/pages/invite-link.page.tsx b/src/pages/invite-link.page.tsx new file mode 100644 index 0000000..4e94040 --- /dev/null +++ b/src/pages/invite-link.page.tsx @@ -0,0 +1,23 @@ +import { useNavigate, useParams } from "react-router-dom"; +import Loader from "../components/loaders/spinner/loader.component"; +import { useEffect, useState } from "react"; +import { sendFriendRequest } from "../services/friend-request.service"; + +export default function InviteLinkPage() { + let linkData = useParams(); + const navigate = useNavigate(); + let inviteSend = false + + useEffect(() => { + if (linkData.senderId && !inviteSend) { + sendFriendRequest(linkData.senderId) + inviteSend = true + } + navigate("/") + }, []); + + + return ( + + ); +} \ No newline at end of file diff --git a/src/pages/login.page.tsx b/src/pages/login.page.tsx new file mode 100644 index 0000000..f9f2ab2 --- /dev/null +++ b/src/pages/login.page.tsx @@ -0,0 +1,35 @@ +import { useNavigate } from "react-router-dom"; +import { getCurrentUser, loginUser } from "../services/auth.service"; +import AuthForm from "../components/forms/auth.form"; +import { UserDTO } from "../dtos/user.dto"; +import { useUserStore } from "../stores/user.store"; +import Button from "../components/buttons/button"; + +export default function LoginPage() { + const navigate = useNavigate(); + const { updateUser } = useUserStore(); + + const onSubmit = async (data: UserDTO) => { + await loginUser(data); + + const currentUser = await getCurrentUser(); + updateUser(currentUser); + + navigate("/"); + }; + return ( +
+

+ Wii Login +

+ +
+ ); +} diff --git a/src/pages/notifications.page.tsx b/src/pages/notifications.page.tsx new file mode 100644 index 0000000..f706865 --- /dev/null +++ b/src/pages/notifications.page.tsx @@ -0,0 +1,105 @@ +import { useEffect, useState } from "react"; +import { Friend } from "../types/friend"; +import { getUserFriends } from "../services/friend.service"; +import Notification from "../types/notification"; +import { countUnseenNotifications } from "../utils/count-unseen-notifications"; +import { useNotificationStore } from "../stores/notification.store"; + +export default function NotificationListPage() { + const { notifications, setNotifications } = useNotificationStore(); + + const [friends, setFriends] = useState([]); + + useEffect(() => { + getUserFriends().then((fetchedFriends) => { + setFriends(fetchedFriends); + + const updatedNotifications = notifications.map( + (notification: Notification) => { + if (notification.isSeen === false) { + return { + ...notification, + isSeen: true, + }; + } + if (notification.status === "my-friend-request-accepted") { + const selectedFriend = fetchedFriends.find( + (friend: Friend) => friend.userId === notification.emitterId + ); + + if (selectedFriend) { + return { + ...notification, + status: "friend-accepted", + emitterUsername: selectedFriend.username, + }; + } + } + if (notification.didIAccept) { + const selectedFriend = fetchedFriends.find( + (friend: Friend) => friend.userId === notification.emitterId + ); + + if (selectedFriend) { + return { + ...notification, + status: "friend-accepted", + emitterUsername: selectedFriend.username, + }; + } + } + return notification; + } + ); + + setNotifications(updatedNotifications); + localStorage.setItem( + "notifications", + JSON.stringify(updatedNotifications) + ); + countUnseenNotifications(notifications); + }); + }, []); + + if (!notifications || notifications.length === 0) { + return ( +
+ Notifications +

No notifications to display

+
+ ); + } + + return ( +
+ Notifications +
+ {notifications.map((notification: Notification) => ( +
+
+ {notification.status === "friend-accepted" && ( + + {notification.emitterUsername}'s request approved by you + + )} + {notification.status === "my-friend-request-accepted" && ( + + {notification.emitterId} accepted your friend request + + )} + {notification.status === "pending-request" && ( + {notification.emitterId} sent you a friend request + )} +
+
+ ))} +
+
+ ); +} diff --git a/src/pages/register.page.tsx b/src/pages/register.page.tsx new file mode 100644 index 0000000..5158a04 --- /dev/null +++ b/src/pages/register.page.tsx @@ -0,0 +1,29 @@ +import { useNavigate } from "react-router-dom"; +import { registerUser } from "../services/auth.service"; +import AuthForm from "../components/forms/auth.form"; +import { UserDTO } from "../dtos/user.dto"; +import Button from "../components/buttons/button"; + +export default function RegisterPage() { + const navigate = useNavigate(); + + const onSubmit = async (data: UserDTO) => { + await registerUser(data); + navigate("/"); + }; + return ( +
+

+ Wii Register +

+ +
+ ); +} diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts new file mode 100644 index 0000000..522fad6 --- /dev/null +++ b/src/services/auth.service.ts @@ -0,0 +1,79 @@ +import { UserDTO } from "../dtos/user.dto"; +import { User } from "../types/user"; + +export async function loginUser(data: UserDTO) { + const response = await fetch( + `${process.env.REACT_APP_API_BASE_URL}/auth/login`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify(data), + } + ); + if (!response.ok) { + const { message } = await response.json(); + throw new Error(message); + } +} + +export async function registerUser(data: UserDTO) { + const response = await fetch( + `${process.env.REACT_APP_API_BASE_URL}/auth/register`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify(data), + } + ); + if (!response.ok) { + const { message } = await response.json(); + throw new Error(message); + } +} + +export async function logoutUser() { + + const response = await fetch( + `${process.env.REACT_APP_API_BASE_URL}/auth/logout`, + { + method: "POST", + credentials: "include", + } + ); + if (!response.ok) { + const { message } = await response.json(); + throw new Error(message); + } + localStorage.clear(); +} + +export async function getCurrentUser(): Promise { + const response = await fetch( + `${process.env.REACT_APP_API_BASE_URL}/auth/me`, + { + credentials: "include", + } + ); + if (!response.ok) { + const { message } = await response.json(); + throw new Error(message); + } + return response.json(); +} + +export async function checkUserAuth() { + const response = await fetch( + `${process.env.REACT_APP_API_BASE_URL}/auth/me`, + { + credentials: "include", + } + ); + + return response.ok ? true : false; +} diff --git a/src/services/friend-request.service.ts b/src/services/friend-request.service.ts new file mode 100644 index 0000000..0e86c0b --- /dev/null +++ b/src/services/friend-request.service.ts @@ -0,0 +1,48 @@ +import { FriendRequest } from "../types/friend-request"; + +export async function fetchFriendRequests() { + try { + const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}/social/friend-requests`, { + credentials: 'include' + }); + const friendRequests = await response.json(); + return friendRequests.map((request: FriendRequest) => ({ + ...request, + })); + } catch (error) { + console.error('Error fetching friend requests:', error); + throw error; + } +} + +export const sendFriendRequest = async (receiverId: string) => { + try { + const randomuuid = crypto.randomUUID(); + await fetch(`${process.env.REACT_APP_API_BASE_URL}/social/friend-request/${randomuuid}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ receiverId: receiverId }), + }); + } catch (error) { + console.error('Error sending friend request:', error); + throw error; + } +} + +export const acceptRequest = async (requestId: string) => { + try { + await fetch(`${process.env.REACT_APP_API_BASE_URL}/social/friend-request/${requestId}/accept`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + }); + } catch (error) { + console.error('Error accepting friend request:', error); + throw error; + } +} \ No newline at end of file diff --git a/src/services/friend.service.ts b/src/services/friend.service.ts new file mode 100644 index 0000000..cee7e9c --- /dev/null +++ b/src/services/friend.service.ts @@ -0,0 +1,18 @@ +import { Friend } from "../types/friend"; + +export const getUserFriends = async (): Promise => { + const response = await fetch( + `${process.env.REACT_APP_API_BASE_URL}/social/friends`, + { + method: "GET", + credentials: "include", + } + ); + if (!response.ok) { + const { message } = await response.json(); + console.error(message); + return []; + } + + return (await response.json()) as Friend[]; +}; diff --git a/src/services/message.service.ts b/src/services/message.service.ts new file mode 100644 index 0000000..14d878f --- /dev/null +++ b/src/services/message.service.ts @@ -0,0 +1,102 @@ +import Message from "../types/message"; +import { MessageAdapter } from "../adapters/message.adapter"; +import { BadRequestError } from "../errors/bad-request.error"; + +export class MessageService implements MessageAdapter { + sendMessage = async (message: Message): Promise => { + console.log( + `${process.env.REACT_APP_API_BASE_URL}/chat/${message.id}/send` + ); + console.log({ receiverId: message.receiverId, content: message.content }); + const response = await fetch( + `${process.env.REACT_APP_API_BASE_URL}/chat/${message.id}/send`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify({ + receiverId: message.receiverId, + content: message.content, + }), + } + ); + console.log(response); + + if (response.status === 400) { + throw new BadRequestError("You are not friend with this user"); + } else { + if (!response.ok) { + throw new Error(`Error ${response.status}: ${response.statusText}`); + } + } + }; + + fetchMessages = async (receiverId: string): Promise => { + const response = await fetch( + `${process.env.REACT_APP_API_BASE_URL}/messages/${receiverId}/`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + } + ); + if (!response.ok) { + throw new Error(`Error ${response.status}: ${response.statusText}`); + } + return await response.json(); + }; +} + +export class FalseMessageService implements MessageAdapter { + async sendMessage(message: Message): Promise { + const random = Math.random() * 10; + + if (random < 5) { + const response = await fetch( + `${process.env.REACT_APP_API_BASE_URL}/chat/${message.id}/send`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify({ + receiverId: message.receiverId, + content: message.content, + }), + } + ); + //TODO: Gérer plusieurs erreurs dont erreur connexion + if (response.status === 400) { + throw new BadRequestError("You are not friend with this user"); + } else { + if (!response.ok) { + throw new Error(`Error ${response.status}: ${response.statusText}`); + } + } + } else { + throw new Error("Method not implemented."); + } + } + + fetchMessages = async (receiverId: string): Promise => { + const response = await fetch( + `${process.env.REACT_APP_API_BASE_URL}/messages/${receiverId}/`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + } + ); + if (!response.ok) { + throw new Error(`Error ${response.status}: ${response.statusText}`); + } + return await response.json(); + }; +} diff --git a/src/services/notification.service.ts b/src/services/notification.service.ts new file mode 100644 index 0000000..e0e1ec4 --- /dev/null +++ b/src/services/notification.service.ts @@ -0,0 +1,22 @@ +import Message from "../types/message"; + +export enum EventName { + FRIEND_REQUEST_RECEIVED = 'friend-request-received', + FRIEND_REQUEST_ACCEPTED = 'friend-request-accepted', + MESSAGE_RECEIVED = 'message-received', +} + +export class NotificationService { + + eventListener = (onElementReceived: (data: any) => void, eventName: EventName) => { + const eventSource = new EventSource(`${process.env.REACT_APP_API_BASE_URL}/notifications`, { withCredentials: true }) + eventSource.addEventListener(eventName, (event) => { + const data = JSON.parse(event.data); + onElementReceived(data); + }) + eventSource.onerror = (error) => { + eventSource.close(); + }; + return eventSource; + } +} \ No newline at end of file diff --git a/src/stores/friend.store.ts b/src/stores/friend.store.ts new file mode 100644 index 0000000..8bb19fc --- /dev/null +++ b/src/stores/friend.store.ts @@ -0,0 +1,25 @@ +import { create } from "zustand/react"; +import { Friend } from "../types/friend"; + +type States = { + friends: Friend[]; +}; + +type Actions = { + setFriends: (friends: Friend[]) => void; + addFriend: (friend: Friend) => void; + clearFriends: () => void; + getFriendById: (userId: string) => Friend | undefined; +}; + +export const useFriendStore = create((set, get) => ({ + friends: [], + setFriends: (friends: Friend[]) => set({ friends }), + addFriend: (friend: Friend) => + set((state) => ({ friends: [...state.friends, friend] })), + clearFriends: () => set({ friends: [] }), + getFriendById: (userId: string): Friend | undefined => + useFriendStore + .getState() + .friends.find((friend: Friend) => friend.userId === userId), +})); diff --git a/src/stores/message.store.ts b/src/stores/message.store.ts new file mode 100644 index 0000000..58bca4e --- /dev/null +++ b/src/stores/message.store.ts @@ -0,0 +1,26 @@ +import { create } from "zustand/react"; + +import Message from "../types/message"; + +type MessageStore = { + messages: Message[]; + setMessages: (messages: Message[]) => void; + addMessage: (message: Message) => void; + updateErrorLastMessage: (error: boolean) => void; +}; + +export const useMessageStore = create((set) => ({ + messages: [], + setMessages: (messages: Message[]) => set({ messages }), + addMessage: (message: Message) => + set((state) => ({ messages: [message, ...state.messages] })), + updateErrorLastMessage: (error: boolean) => + set((state) => { + const messages = [...state.messages]; + messages[0] = { + ...messages[0], + error, + }; + return { messages }; + }), +})); diff --git a/src/stores/notification.store.ts b/src/stores/notification.store.ts new file mode 100644 index 0000000..4431037 --- /dev/null +++ b/src/stores/notification.store.ts @@ -0,0 +1,30 @@ +import { create } from "zustand/react"; + +import Notification from "../types/notification"; +import { EventName, NotificationService } from "../services/notification.service"; + +const notificationService = new NotificationService(); + +type NotificationsStore = { + notifications: Notification[]; + service: NotificationService; + setNotifications: (notifications: Notification[]) => void; + addNotification: (notification: Notification) => void; + getMessagesNotifications: () => Notification[]; + getFriendRequestReceivedNotifications: () => Notification[]; + getFriendRequestAcceptedNotifications: () => Notification[]; +}; + +export const useNotificationStore = create((set, get) => ({ + notifications: [], + service: notificationService, + setNotifications: (notifications: Notification[]) => set({ notifications }), + addNotification: (notification: Notification) => + set((state) => ({ notifications: [notification, ...state.notifications] })), + getMessagesNotifications: () => + get().notifications.filter((notification) => notification.type === EventName.MESSAGE_RECEIVED), + getFriendRequestReceivedNotifications: () => + get().notifications.filter((notification) => notification.type === EventName.FRIEND_REQUEST_RECEIVED), + getFriendRequestAcceptedNotifications: () => + get().notifications.filter((notification) => notification.type === EventName.FRIEND_REQUEST_ACCEPTED), +})); diff --git a/src/stores/user.store.ts b/src/stores/user.store.ts new file mode 100644 index 0000000..fd2bef4 --- /dev/null +++ b/src/stores/user.store.ts @@ -0,0 +1,18 @@ +import { create } from "zustand"; +import { User } from "../types/user"; + +type State = { + username: string; + id: string; +}; + +type Action = { + clearUser: () => void; + updateUser: (newUser: User) => void; +}; +export const useUserStore = create((set) => ({ + username: "", + id: "", + clearUser: () => set({ username: "", id: "" }), + updateUser: (newUser) => set({ username: newUser.username, id: newUser.id }), +})); diff --git a/src/types/chat.ts b/src/types/chat.ts new file mode 100644 index 0000000..928cf71 --- /dev/null +++ b/src/types/chat.ts @@ -0,0 +1,7 @@ +export default interface Chat { + id: number; + name: string; + username: string; + profilePicture: string; + createdAt: string; +} \ No newline at end of file diff --git a/src/types/friend-request.ts b/src/types/friend-request.ts new file mode 100644 index 0000000..fd47801 --- /dev/null +++ b/src/types/friend-request.ts @@ -0,0 +1,5 @@ +export interface FriendRequest { + id: string; + senderId: string; + requestedAt: string; +} \ No newline at end of file diff --git a/src/types/friend.ts b/src/types/friend.ts new file mode 100644 index 0000000..4d5256c --- /dev/null +++ b/src/types/friend.ts @@ -0,0 +1,5 @@ +export interface Friend { + userId: string; + username: string; + startedAt: string; +} diff --git a/src/types/message.ts b/src/types/message.ts new file mode 100644 index 0000000..df043e8 --- /dev/null +++ b/src/types/message.ts @@ -0,0 +1,8 @@ +export default interface Message { + id: string; + emitterId: string; + receiverId: string; + content: string; + sendAt: string; + error?: boolean; +} \ No newline at end of file diff --git a/src/types/notification.ts b/src/types/notification.ts new file mode 100644 index 0000000..502d6a6 --- /dev/null +++ b/src/types/notification.ts @@ -0,0 +1,10 @@ +export default interface Notification { + id: string; + type: string; + emitterId?: string; + emitterUsername?: string; + receivedAt: string; + didIAccept?: boolean; + status?: string; + isSeen: boolean; +} \ No newline at end of file diff --git a/src/types/user.ts b/src/types/user.ts new file mode 100644 index 0000000..769bd33 --- /dev/null +++ b/src/types/user.ts @@ -0,0 +1,4 @@ +export interface User { + username: string; + id: string; +} diff --git a/src/utils/count-unseen-notifications.tsx b/src/utils/count-unseen-notifications.tsx new file mode 100644 index 0000000..6a27df0 --- /dev/null +++ b/src/utils/count-unseen-notifications.tsx @@ -0,0 +1,15 @@ +import Notification from "../types/notification"; + +export function countUnseenNotifications(notifications: Notification[]) { + // console.log("count"); + + let count = 0; + notifications.map((notification: Notification) => { + if (notification.isSeen == false || notification.didIAccept == false) { + count = count + 1; + } + }); + // console.log(count); + + return count.toString(); +} \ No newline at end of file diff --git a/src/utils/dateFormater.tsx b/src/utils/dateFormater.tsx new file mode 100644 index 0000000..c191245 --- /dev/null +++ b/src/utils/dateFormater.tsx @@ -0,0 +1,27 @@ +/** + * A many time ago the request was sended + * @param dateString + * @returns +*/ + +export function dateFormater(dateString: string) { + + const date = new Date(dateString); + const now = new Date(); + + const diffMs = now.getTime() - date.getTime(); + const diffSec = Math.floor(diffMs / 1000); + const diffMin = Math.floor(diffSec / 60); + const diffHeure = Math.floor(diffMin / 60); + const diffJour = Math.floor(diffHeure / 24); + + if (diffJour > 0) { + return `${diffJour} day${diffJour > 1 ? 's' : ''} ago`; + } else if (diffHeure > 0) { + return ` ${diffHeure} hour${diffHeure > 1 ? 's' : ''} ago`; + } else if (diffMin > 0) { + return `${diffMin} minute${diffMin > 1 ? 's' : ''} ago`; + } else { + return `${diffSec} second${diffSec > 1 ? 's' : ''} ago`; + } +} \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js index c0958ec..9e12a2f 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,11 +1,9 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: [ - "./src/**/*.{js,jsx,ts,tsx}", - ], + content: ["./src/**/*.{js,jsx,ts,tsx}"], theme: { extend: {}, }, + safelist: ["btn", "primary-btn", "secondary-btn", "tertiary-btn"], plugins: [], -} - +};