Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
43906a0
[MISC][CLEANUP] remove useless line
Notgoyome Feb 15, 2026
3850d31
[NOTIFICATIONS][FEATURE]: add notifications inbox
Notgoyome Feb 15, 2026
d3d7ed1
[MISC][FEATURE]: add dev docker compose to get hot reload
Notgoyome Feb 15, 2026
2091eb5
[CONFIG][FIX]: update BASE URL fallback to use localhost for OpenAPI
Notgoyome Feb 22, 2026
90a2826
[NOTIFICATIONS][FEATURE]: add NotificationsService and NotificationTe…
Notgoyome Feb 22, 2026
2276177
[NOTIFICATIONS][FEATURE]: better notifications and reading/init notif…
Notgoyome Feb 22, 2026
483d4e5
[NOTIFICATIONS][REFACTO]: refactor notification read handling and imp…
Notgoyome Feb 28, 2026
a4c93d2
[NOTIFICATIONS][FEAT]: api generate
Notgoyome Feb 28, 2026
c6cf3cd
[NOTIFICATIONS][CLEANUP]: remove comments
Notgoyome Feb 28, 2026
14b2266
[NOTIFICATIONS][item]:notification item as a component for menu lisib…
Notgoyome Feb 28, 2026
b179283
[DOCS][UPDATE]: clean readme
Notgoyome Feb 28, 2026
9019972
[NOTIFICATIONS][FEAT]: error handling
Notgoyome Feb 28, 2026
d25454d
[NOTIFICATIONS][FEAT]: replace Typography with MenuItem for better in…
Notgoyome Feb 28, 2026
df060f3
[CONFIG][UPDATE]: change default BASE URL to "none"
Notgoyome Feb 28, 2026
58d34b3
[NOTIFICATIONS][UPDATE]: remove cursor pointer style from Notificatio…
Notgoyome Feb 28, 2026
465c254
[NOTIFICATIONS][UPDATE]: add error handling for marking notifications…
Notgoyome Feb 28, 2026
6f7ade7
[NOTIFICATIONS][UPDATE]: refactor NotificationEntry to use MenuItem a…
Notgoyome Feb 28, 2026
b9328ea
[API][FIX] revert
Notgoyome Mar 1, 2026
31b971b
[NOTIFICATIONS][UPDATE]: refactor backend URL retrieval to use OpenAP…
Notgoyome Mar 1, 2026
5c88326
[NOTIFICATIONS][UPDATE]: removed eslint disable next line
Notgoyome Mar 1, 2026
a4ff490
[DEV][REMOVE]: delete docker-compose.dev.yml file
Notgoyome Mar 1, 2026
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: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ bun run dev

## Docker

for dev, with hot reload feature
```bash
docker compose -f docker-compose.dev.yml up
```

for prod
```bash
docker compose up
```
Expand Down
17 changes: 4 additions & 13 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
services:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ialso have a docker-compose.dev.yml file on my frontend branch, this will conflict -- I'll see if I either keep your version or mine

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

frontend:
image: oven/bun:latest
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using oven/bun:latest makes the dev environment non-reproducible (the image can change under you and break hot reload or dependency installs). Pin to a specific Bun tag/version (and optionally document the required Bun version) to keep dev setups stable.

Suggested change
image: oven/bun:latest
image: oven/bun:1.1.34

Copilot uses AI. Check for mistakes.
working_dir: /app
volumes:
- .:/app
- node_modules:/app/node_modules
ports:
- "3001:3001"
environment:
- CHOKIDAR_USEPOLLING=true
command: sh -lc "bun install --frozen-lockfile && bun run dev --host 0.0.0.0 --port 3001"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this, please declare a rule in the package.json file instead of writing commands to do the work so that we can also run it outside of Docker

Copy link
Copy Markdown
Collaborator Author

@Notgoyome Notgoyome Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will change depending of your choice 👍


volumes:
node_modules: {}
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
"@vitejs/plugin-react": "^4.3.4",
"cypress": "^15.2.0",
"eslint": "^9.24.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.1.0",
Expand All @@ -69,4 +68,4 @@
"vite": "^6.2.0"
},
"packageManager": "yarn@4.9.1+sha512.f95ce356460e05be48d66401c1ae64ef84d163dd689964962c6888a9810865e39097a5e9de748876c2e0bf89b232d583c33982773e9903ae7a76257270986538"
}
}
2 changes: 1 addition & 1 deletion src/api/core/OpenAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export type OpenAPIConfig = {
};

export const OpenAPI: OpenAPIConfig = {
BASE: import.meta.env.VITE_BACKEND_URL || 'none',
BASE: (import.meta.env.VITE_BACKEND_URL || "http://localhost:3000"),
VERSION: '1.0',
WITH_CREDENTIALS: true,
CREDENTIALS: 'include',
Expand Down
3 changes: 3 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ export type { CreateUserDto } from './models/CreateUserDto';
export type { FetchWorkSessionDto } from './models/FetchWorkSessionDto';
export type { KickWorkSessionDto } from './models/KickWorkSessionDto';
export type { LoginDto } from './models/LoginDto';
export type { NotificationTestDto } from './models/NotificationTestDto';
export type { PaginationMetaDto } from './models/PaginationMetaDto';
export type { ProjectResponseDto } from './models/ProjectResponseDto';
export type { ProjectWithRelationsResponseDto } from './models/ProjectWithRelationsResponseDto';
export type { RemoveCollaboratorDto } from './models/RemoveCollaboratorDto';
export type { UpdateProjectDto } from './models/UpdateProjectDto';
export type { UpdateUserDto } from './models/UpdateUserDto';
export type { UpdateUserProfileDto } from './models/UpdateUserProfileDto';
export type { UserBasicInfoDto } from './models/UserBasicInfoDto';
export type { UserListResponseDto } from './models/UserListResponseDto';
export type { UserProfileResponseDto } from './models/UserProfileResponseDto';
Expand All @@ -28,6 +30,7 @@ export type { UserRoleDto } from './models/UserRoleDto';
export type { UserSingleResponseDto } from './models/UserSingleResponseDto';

export { AuthService } from './services/AuthService';
export { NotificationsService } from './services/NotificationsService';
export { ProjectsService } from './services/ProjectsService';
export { UsersService } from './services/UsersService';
export { WorkSessionsService } from './services/WorkSessionsService';
1 change: 0 additions & 1 deletion src/api/models/AuthResponseDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,5 @@
/* eslint-disable */
export type AuthResponseDto = {
access_token: string;
refresh_token: string;
};

19 changes: 19 additions & 0 deletions src/api/models/NotificationTestDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type NotificationTestDto = {
/**
* The title of the test notification
*/
title: string;
/**
* The message content of the test notification
*/
message: string;
/**
* The type of the notification (friend request, message, etc.)
*/
type: string;
};

54 changes: 54 additions & 0 deletions src/api/services/NotificationsService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { NotificationTestDto } from '../models/NotificationTestDto';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class NotificationsService {
/**
* Send a test notification to the current user
* @param requestBody
* @returns any Notification created and sent
* @throws ApiError
*/
public static notificationsControllerSendTestNotification(
requestBody: NotificationTestDto,
): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'POST',
url: '/notifications/test',
body: requestBody,
mediaType: 'application/json',
});
}
/**
* set one notification as read
* @param id
* @returns any Notification marked as read
* @throws ApiError
*/
public static notificationsControllerMarkAsRead(
id: string,
): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'PATCH',
url: '/notifications/{id}/read',
path: {
'id': id,
},
});
}
/**
* set notifications as read
* @returns any All notifications as read
* @throws ApiError
*/
public static notificationsControllerMarkAllAsRead(): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'PATCH',
url: '/notifications/read-all',
});
}
}
3 changes: 3 additions & 0 deletions src/assets/inbox.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 0 additions & 2 deletions src/shared/authOverlay/AuthOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,6 @@ const AuthOverlay: FC<AuthOverlayProps> = ({ isOpen, setIsOpen, onClose }) => {
}
// FIXME: put the token to httpOnly cookie using the backend
LocalStorageManager.setToken(authResponse.access_token);
LocalStorageManager.setRefreshToken(authResponse.refresh_token);

const userRes = await UsersService.userControllerGetProfile();
LocalStorageManager.setUser({
id: String(userRes.id),
Expand Down
2 changes: 2 additions & 0 deletions src/shared/navbar/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import React, { useState } from "react";
import { useUser } from "@providers/UserProvider";
import { muiTheme } from "@theme/MUITheme";
import { Login } from "@shared/navbar/login/Login";
import { NotificationBox } from "@shared/navbar/notifications/NotificationBox";
import * as Urls from "@shared/route";
const Nav = styled("nav")(({ theme }) => ({
display: "grid",
Expand Down Expand Up @@ -54,6 +55,7 @@ const NavBar: React.FC = () => {

<Right>
<NavElem to="/friends">Friends</NavElem>
<NotificationBox />
{user ? <NavProfile /> : <Login forceShowAuthOverlay={forceShowAuthOverlay} setForceShowAuthOverlay={setForceShowAuthOverlay} />}
</Right>
</Nav >
Expand Down
128 changes: 128 additions & 0 deletions src/shared/navbar/notifications/NotificationBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { Badge, IconButton, styled } from "@mui/material";
import { JSX, useCallback, useEffect, useMemo, useRef, useState, type MouseEvent } from "react";
import InfoBoxIcon from "@assets/infoBox.svg?react";
import { useUser } from "@providers/UserProvider";
import { LocalStorageManager } from "@utils/LocalStorageManager";
import { NotificationMenu } from "./NotificationMenu";
import { NotificationItem } from "./types";
import { NotificationsService } from "@api/services/NotificationsService";

const NOTIFICATION_SOCKET_PATH = "/socket/notifications";
const MAX_NOTIFICATIONS = 50;

type NotificationWsMessage =
| { type: "notification"; payload: NotificationItem }
| { type: "notifications:init"; payload: NotificationItem[] };

const NotificationButton = styled(IconButton)(({ theme }) => ({
margin: theme.spacing(2),
color: theme.palette.text.primary,
}));

const InfoIcon = styled(InfoBoxIcon)(({ theme }) => ({
width: 24,
height: 24,
color: theme.palette.text.primary,
}));

const mergeNotification = (
previous: NotificationItem[],
notification: NotificationItem,
): NotificationItem[] => {
const withoutCurrent = previous.filter((item) => item.id !== notification.id);
return [notification, ...withoutCurrent].slice(0, MAX_NOTIFICATIONS);
};

export const NotificationBox = (): JSX.Element => {
const { user } = useUser();
const userId = user?.id;
const token = LocalStorageManager.getToken();
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
const [showMenu, setShowMenu] = useState(false);
const [anchorEl, setAnchorEl] = useState<HTMLElement | undefined>(undefined);
const socketRef = useRef<WebSocket | null>(null);
const backendUrl = (import.meta.env.VITE_BACKEND_URL || "http://localhost:3000").trim();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way for you to get that info from OpenAPI.BASE?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


const unreadCount = useMemo(
() => notifications.reduce((count, notification) => count + (notification.read ? 0 : 1), 0),
[notifications],
);

useEffect(() => {
if (!userId || !token || !backendUrl) {
if (socketRef.current) {
socketRef.current.close();
socketRef.current = null;
}
setNotifications([]);
return;
}

const wsBase = backendUrl.replace(/^http/i, "ws").replace(/\/$/, "");
const socketUrl = `${wsBase}${NOTIFICATION_SOCKET_PATH}?token=${encodeURIComponent(token)}`;
const socket = new WebSocket(socketUrl);

socketRef.current = socket;

socket.onmessage = (event: MessageEvent<string>) => {
try {
const message = JSON.parse(event.data) as NotificationWsMessage;

if (message.type === "notifications:init") {
setNotifications(message.payload.slice(0, MAX_NOTIFICATIONS));
return;
}

if (message.type === "notification") {
setNotifications((previous) => mergeNotification(previous, message.payload));
}
} catch {
// eslint
}
};

return () => {
socket.close();
socketRef.current = null;
};
}, [userId, token, backendUrl]);

const handleClick = useCallback((event: MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
setShowMenu((previous) => {
const next = !previous;

return next;
});
}, []);

const handleClose = useCallback(() => setShowMenu(false), []);

const handleMarkAsRead = useCallback((notificationId: string) => {
setNotifications((current) =>
current.map((notification) =>
notification.id === notificationId ? { ...notification, read: true } : notification,
),
);
NotificationsService.notificationsControllerMarkAsRead(notificationId);
}, []);

return (
<>
<NotificationButton onClick={handleClick} disabled={!userId}>
<Badge badgeContent={unreadCount} color="error">
<InfoIcon />
</Badge>
</NotificationButton>
{showMenu && (
<NotificationMenu
anchorEl={anchorEl}
open={showMenu}
onClose={handleClose}
notifications={notifications}
onMarkAsRead={handleMarkAsRead}
/>
)}
</>
);
};
Loading