diff --git a/CHANGELOG.md b/CHANGELOG.md index fe1ff848..280b9c30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- [frontend] Added `viewport-fit=cover` to enable iPhone safe-area support (Notch/Home-Indicator) across all pages. +- [memorandum] Empty state redesigned as Carbon Tile cards with title, description, and CTA buttons. +- [memorandum] FAB add-folder button is now `kind="primary"` and hidden in empty state. +- [memorandum] Folder and Link now show an `OverflowMenu` (kebab menu) on phone/tablet as alternative to right-click ContextMenu. Desktop keeps the existing ContextMenu unchanged. +- [todo] Edit button (pencil icon) added to Todo items on mobile as alternative to right-click ContextMenu. + +### Fixed + +- [memorandum] `FolderOverlay` modal now has `hasScrollingContent` so sliders and inputs are scrollable on mobile. +- [memorandum] Folder arrow buttons on phone now have `min-height: 44px` touch target (was too small). +- [memorandum] FolderOverlay color value fields wrap on `$phone` instead of overflowing. +- [admin] Replaced `width: 100vw` with `width: 100%` in `.admin_sections` and `.admin_list_item_content` to prevent horizontal overflow past padding. +- [admin] FAB (`#update_admin_info_button`) now uses `env(safe-area-inset-bottom)` so it doesn't overlap the iPhone home indicator. +- [food-scan] `ContentOutput` image preview and text output use `max-width: 100%` on phone (were too narrow at 33vw/50vw). +- [food-scan] `DebugInformation` stats table is wrapped in `overflow-x: auto` to prevent horizontal overflow on mobile. +- [home] `JokeOfTheDay` date font-size increased from 0.65rem (10.4px) to 0.75rem (12px) to meet minimum readability threshold. +- [todo] `TodoInput` inputs stack vertically on phone with `flex-wrap: wrap` (were too narrow at ~147px). +- [todo] `TodoList` view toggle buttons and header toggle button have `min-height/min-width: 44px` on phone. +- [todo] `GenericListOverlay` delete button has `min-height/min-width: 44px` on phone. +- [todo] `Todo.svelte` title and category inputs have `font-size: 16px` on phone to prevent iOS Safari auto-zoom. +- [todo] Tab autocomplete hint hidden on phone (`display: none`) since mobile keyboards lack a Tab key. +- [settings] `OnlinePersistenceCheck` edit connection form wraps on phone so TextInput stacks above the button. +- [todo] Empty state button group (Neue Liste / Liste importieren) wraps on phone with `flex-wrap: wrap`. +- [global] All `height: Xvh` declarations now use double-declaration `Xvh; Xdvh` pattern for iOS Safari dynamic toolbar compatibility (`+layout.svelte`, `uno-sort`, `Jokes.svelte`, `AboutInfo.svelte`, `ToggledApplicationInfo.svelte`). +- [global] Background gradient `background-size` uses `100dvh` fallback so it covers the full dynamic viewport on iOS. + +### Removed + +- [global] Removed Chat application entirely (frontend route, API, stores, types and backend module/library). +- [home] Removed web search bar (`SearchBar`) and Settings button from home page. + +### Changed + +- [home] Navigation tiles show icon-only on phone; `JokeOfTheDay` section is now prominently displayed with a Carbon Tile card. +- [global] `GlobalMenu` footer shows GitHub/Stadtwerk buttons below "Entwickelt von..." text on phone as social buttons. + +### Changed + +- [global] Replaced `height: 100vh` with `100dvh` (dynamic viewport height) in `.app` and settings page to fix iOS Browser-UI overlap. +- [global] Changed `main` width from `100vw` to `100%` to prevent horizontal scrollbar on mobile. +- [home] Fixed duplicate `@media #{$tablet}` bug in `SearchBar.svelte`; phone breakpoint now sets `margin-top: 2rem` and `width: 95vw` for a wider search bar. +- [global] `GlobalMenu` sidebar is now scrollable (`overflow-y: auto`) with padding to prevent footer overlap; footer respects safe-area-inset-bottom. +- [memorandum] Memorandum search bar uses `max-width: 100%` on phone (was 33vw ≈ 124px); menu line wraps on phone. +- [memorandum] Folder and link reorder buttons are always visible on phone/tablet (no longer hidden, only shown on hover). +- [memorandum] Link reorder buttons have a minimum touch-target of 44px height on phone. +- [todo] Edit container in `Todo.svelte` wraps on phone; title input takes full width. +- [memorandum] FAB add-folder button respects `safe-area-inset-bottom` on iPhone. +- [todo] FAB list-menu button respects `safe-area-inset-bottom` on iPhone. +- [global] Header `h2` uses `font-size: 1.2em` on phone for better fit. +- [global] Navigation touch-targets (`bx--content-switcher-btn`) have `min-height: 44px` on phone. +- [settings] `connection_card` uses `max-width: 100%` to prevent overflow on phone. +- [admin] Dashboard card description uses `font-size: 0.75rem` on phone (was unreadable 0.5rem/8px). +- [todo] Delete button in `GenericListOverlay` has minimum 32×32px touch-target. +- [home] Navigation margin-bottom reduced to `1rem` on phone/tablet (was 4rem/2rem). + ### Fixed - [frontend] Fixed `TypeError: Cannot read properties of undefined (reading 'de')` by replacing the non-reactive Proxy in `applications.ts` with a Svelte writable store initialized with proper default route values. All consumer components updated to use reactive store access (`$applicationRoutes`). diff --git a/backend/apps/tilloh-dev/src/main.ts b/backend/apps/tilloh-dev/src/main.ts index d116739d..4833ed1a 100644 --- a/backend/apps/tilloh-dev/src/main.ts +++ b/backend/apps/tilloh-dev/src/main.ts @@ -1,5 +1,4 @@ import { AdminModule } from '@backend/admin'; -import { ChatModule } from '@backend/chat'; import { JokesModule } from '@backend/jokes'; import { MemorandumModule } from '@backend/memorandum'; import { OcrModule } from '@backend/ocr'; @@ -71,7 +70,6 @@ import { EnvironmentVariables, validate } from './env.validation'; AdminModule, MemorandumModule, JokesModule, - ChatModule, OcrModule, TodoControllerModule, ], diff --git a/backend/libs/chat/.eslintrc.json b/backend/libs/chat/.eslintrc.json deleted file mode 100644 index 9d9c0db5..00000000 --- a/backend/libs/chat/.eslintrc.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], - "overrides": [ - { - "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": {} - }, - { - "files": ["*.ts", "*.tsx"], - "rules": {} - }, - { - "files": ["*.js", "*.jsx"], - "rules": {} - } - ] -} diff --git a/backend/libs/chat/README.md b/backend/libs/chat/README.md deleted file mode 100644 index 02644e73..00000000 --- a/backend/libs/chat/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# chat - -This library was generated with [Nx](https://nx.dev). - -## Running unit tests - -Run `nx test chat` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/backend/libs/chat/jest.config.ts b/backend/libs/chat/jest.config.ts deleted file mode 100644 index 0579a079..00000000 --- a/backend/libs/chat/jest.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable */ -export default { - displayName: 'chat', - preset: '../../jest.preset.js', - testEnvironment: 'node', - transform: { - '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], - }, - moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: '../../coverage/libs/chat', -}; diff --git a/backend/libs/chat/project.json b/backend/libs/chat/project.json deleted file mode 100644 index bdf06c65..00000000 --- a/backend/libs/chat/project.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "chat", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "libs/chat/src", - "projectType": "library", - "tags": [], - "targets": { - "lint": { - "executor": "@nx/eslint:lint", - "outputs": [ - "{options.outputFile}" - ] - }, - "test": { - "executor": "@nx/jest:jest", - "outputs": [ - "{workspaceRoot}/coverage/{projectRoot}" - ], - "options": { - "jestConfig": "libs/chat/jest.config.ts" - } - } - } -} \ No newline at end of file diff --git a/backend/libs/chat/src/index.ts b/backend/libs/chat/src/index.ts deleted file mode 100644 index 0187d4a1..00000000 --- a/backend/libs/chat/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './lib/chat.controller'; -export * from './lib/chat.module'; diff --git a/backend/libs/chat/src/lib/chat.controller.spec.ts b/backend/libs/chat/src/lib/chat.controller.spec.ts deleted file mode 100644 index 90878bbf..00000000 --- a/backend/libs/chat/src/lib/chat.controller.spec.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { - ChatEntityDto, - CreateChatInputDto, - GetChatInputDto, - RemoveChatInputDto, - UpdateChatInputDto, -} from '@backend/shared-types'; -import { mockChatEntityDto } from '@backend/util'; -import { Test, TestingModule } from '@nestjs/testing'; -import { ChatController } from './chat.controller'; -import { ChatService } from './chat.service'; - -describe('ChatController', () => { - let controller: ChatController; - let chatService: ChatService; - - beforeAll(() => { - jest.useFakeTimers(); - }); - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [ChatController], - providers: [ - { - provide: ChatService, - useValue: { - listChats: jest.fn(), - findChat: jest.fn(), - createChat: jest.fn(), - updateChat: jest.fn(), - removeChat: jest.fn(), - }, - }, - ], - }).compile(); - - controller = module.get(ChatController); - chatService = module.get(ChatService); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('Controller should be defined.', () => { - expect(controller).toBeDefined(); - }); - - describe('getChats', () => { - it('should return the output of getting chats', async () => { - // Arrange - const chatEntityDto: ChatEntityDto[] = [mockChatEntityDto()]; - jest.spyOn(chatService, 'listChats').mockResolvedValue(chatEntityDto); - - // Act and Assert - await expect(controller.getChats()).resolves.toEqual(chatEntityDto); - }); - }); - - describe('getChat', () => { - it('should return the output of getting a chat', async () => { - // Arrange - const getChatInputDto: GetChatInputDto = { id: '1' }; - const outputGetChat: ChatEntityDto = mockChatEntityDto(); - jest.spyOn(chatService, 'findChat').mockResolvedValue(outputGetChat); - - // Act and Assert - await expect(controller.getChat(getChatInputDto)).resolves.toEqual( - outputGetChat, - ); - }); - }); - - describe('createChat', () => { - it('should return the output of creating a chat', async () => { - // Arrange - const createChatInputDto: CreateChatInputDto = { name: 'test' }; - const outputCreateChat: ChatEntityDto = mockChatEntityDto({ - name: 'test', - }); - jest.spyOn(chatService, 'createChat').mockResolvedValue(outputCreateChat); - - // Act and Assert - await expect(controller.createChat(createChatInputDto)).resolves.toEqual( - outputCreateChat, - ); - }); - }); - - describe('updateChat', () => { - it('should return the output of updating a chat', async () => { - // Arrange - const updateChatInputParamDto: UpdateChatInputDto = { id: '1' }; - const updateChatInputBodyDto: ChatEntityDto = mockChatEntityDto({ - name: 'newName', - }); - const outpuUpdateChat: ChatEntityDto = mockChatEntityDto({ - name: 'newName', - }); - jest.spyOn(chatService, 'updateChat').mockResolvedValue(outpuUpdateChat); - - // Act and Assert - await expect( - controller.updateChat(updateChatInputParamDto, updateChatInputBodyDto), - ).resolves.toEqual(outpuUpdateChat); - }); - }); - - describe('deleteChat', () => { - it('should return the output of deleting a chat', async () => { - // Arrange - const removeChatInputDto: RemoveChatInputDto = { id: '1' }; - const outputDeleteChat: ChatEntityDto = mockChatEntityDto(); - jest.spyOn(chatService, 'removeChat').mockResolvedValue(outputDeleteChat); - - // Act and Assert - await expect(controller.deleteChat(removeChatInputDto)).resolves.toEqual( - outputDeleteChat, - ); - }); - }); -}); diff --git a/backend/libs/chat/src/lib/chat.controller.ts b/backend/libs/chat/src/lib/chat.controller.ts deleted file mode 100644 index 83e3d7cc..00000000 --- a/backend/libs/chat/src/lib/chat.controller.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { - ChatDto, - ChatEntityDto, - CreateChatInputDto, - CreateChatOutputDto, - GetChatInputDto, - GetChatOutputDto, - GetChatsOutputDto, - RemoveChatInputDto, - RemoveChatOutputDto, - UpdateChatInputDto, - UpdateChatOutputDto, -} from '@backend/shared-types'; -import { - Body, - Controller, - Delete, - Get, - Param, - Post, - Put, - Query, -} from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiBearerAuth, - ApiNotFoundResponse, - ApiOkResponse, - ApiTags, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; -import { ChatService } from './chat.service'; - -@ApiTags('chats') -@Controller('/chats') -export class ChatController { - constructor(private chatService: ChatService) {} - - @ApiBearerAuth() - @ApiOkResponse({ - description: 'List of all chats successfully returned.', - type: [GetChatsOutputDto], - }) - @ApiUnauthorizedResponse({ description: 'Unauthorized request.' }) - @ApiBadRequestResponse({ description: 'Bad or malformed request.' }) - @Get() - getChats(@Query() filter?: ChatDto) { - const filterQuery = filter || {}; - return this.chatService.listChats(filterQuery); - } - - @ApiBearerAuth() - @ApiOkResponse({ - description: 'Chat successfully returned.', - type: GetChatOutputDto, - }) - @ApiUnauthorizedResponse({ description: 'Unauthorized request.' }) - @ApiBadRequestResponse({ description: 'Bad or malformed request.' }) - @ApiNotFoundResponse({ description: 'Chat not found.' }) - @Get('/:id') - getChat(@Param() getChatInput: GetChatInputDto) { - const { id } = getChatInput; - return this.chatService.findChat(id); - } - - @ApiBearerAuth() - @ApiOkResponse({ - description: 'Chat successfully created.', - type: CreateChatOutputDto, - }) - @ApiUnauthorizedResponse({ description: 'Unauthorized request.' }) - @ApiBadRequestResponse({ description: 'Bad or malformed request.' }) - @Post() - createChat(@Body() createChatInputDto: CreateChatInputDto) { - const { name } = createChatInputDto; - return this.chatService.createChat(name); - } - - @ApiBearerAuth() - @ApiOkResponse({ - description: 'Chat successfully updated.', - type: UpdateChatOutputDto, - }) - @ApiUnauthorizedResponse({ description: 'Unauthorized request.' }) - @ApiBadRequestResponse({ description: 'Bad or malformed request.' }) - @ApiNotFoundResponse({ description: 'Chat not found.' }) - @Put('/:id') - updateChat( - @Param() updateChatInputDto: UpdateChatInputDto, - @Body() identifierDto: ChatEntityDto, - ) { - return this.chatService.updateChat(updateChatInputDto.id, identifierDto); - } - - @ApiBearerAuth() - @ApiOkResponse({ - description: 'Chat successfully deleted.', - type: RemoveChatOutputDto, - }) - @ApiUnauthorizedResponse({ description: 'Unauthorized request.' }) - @ApiBadRequestResponse({ description: 'Bad or malformed request.' }) - @ApiNotFoundResponse({ description: 'Chat not found.' }) - @Delete('/:id') - deleteChat(@Param() removeChatInputDto: RemoveChatInputDto) { - return this.chatService.removeChat(removeChatInputDto.id); - } -} diff --git a/backend/libs/chat/src/lib/chat.gateway.spec.ts b/backend/libs/chat/src/lib/chat.gateway.spec.ts deleted file mode 100644 index b9964eb1..00000000 --- a/backend/libs/chat/src/lib/chat.gateway.spec.ts +++ /dev/null @@ -1,309 +0,0 @@ -import { - CreateMessageDto, - IdentifyMessageDto, - JoinRoomDto, - MessageDto, - TypingDto, - UpdateMessageDto, -} from '@backend/shared-types'; -import { mockMessageDto } from '@backend/util'; -import { Test, TestingModule } from '@nestjs/testing'; -import { Socket } from 'socket.io'; -import { ChatGateway } from './chat.gateway'; -import { MessagesService } from './messages.service'; - -describe('ChatGateway', () => { - let gateway: ChatGateway; - let messagesService: MessagesService; - let mockServer: any; - - beforeAll(() => { - jest.useFakeTimers(); - }); - - beforeEach(async () => { - mockServer = { emit: jest.fn(), on: jest.fn() }; - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - { - provide: MessagesService, - useValue: { - createMessage: jest.fn(), - listMessages: jest.fn(), - findMessage: jest.fn(), - updateMessage: jest.fn(), - removeMessage: jest.fn(), - identify: jest.fn(), - getClientName: jest.fn(), - }, - }, - ChatGateway, - ], - }).compile(); - - gateway = module.get(ChatGateway); - messagesService = module.get(MessagesService); - gateway.server = mockServer; - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('Gateway should be defined.', () => { - expect(gateway).toBeDefined(); - }); - - describe('create', () => { - it('should return the created message', async () => { - // arrange - const createMessageDto: CreateMessageDto = { - chatId: 'chatId', - name: 'name', - text: 'text', - timestamp: new Date(), - }; - - const { name, text, timestamp } = createMessageDto; - const message = mockMessageDto({ - name, - text, - timestamp, - }); - - jest - .spyOn(messagesService, 'createMessage') - .mockResolvedValueOnce(message); - - // act & assert - await expect(gateway.create(createMessageDto)).resolves.toBe(message); - expect(mockServer.emit).toHaveBeenCalledWith('message', message); - }); - it('should throw an error if the message creation fails', async () => { - // arrange - const createMessageDto: CreateMessageDto = { - chatId: 'chatId', - name: 'name', - text: 'text', - timestamp: new Date(), - }; - - const createMessageErrorMsg = 'Failed to create message'; - jest - .spyOn(messagesService, 'createMessage') - .mockRejectedValueOnce(new Error(createMessageErrorMsg)); - - // act & assert - await expect(gateway.create(createMessageDto)).rejects.toThrow( - createMessageErrorMsg, - ); - expect(mockServer.emit).not.toHaveBeenCalled(); - }); - }); - - describe('findAll', () => { - it('should return all messages', async () => { - // arrange - const chatId = 'chatId'; - const messages = [mockMessageDto({}), mockMessageDto({})] as MessageDto[]; - jest - .spyOn(messagesService, 'listMessages') - .mockResolvedValueOnce(messages); - - // act & assert - await expect(gateway.findAll(chatId)).resolves.toEqual(messages); - }); - it('should throw an error if the message retrieval fails', async () => { - // arrange - const chatId = 'chatId'; - const listMessagesErrorMsg = 'Failed to list messages'; - jest - .spyOn(messagesService, 'listMessages') - .mockRejectedValueOnce(new Error(listMessagesErrorMsg)); - - // act & assert - await expect(gateway.findAll(chatId)).rejects.toThrow( - listMessagesErrorMsg, - ); - }); - }); - - describe('findOne', () => { - it('should return the message', async () => { - // arrange - const chatId = 'chatId'; - const id = 1; - const message = mockMessageDto({}); - jest.spyOn(messagesService, 'findMessage').mockResolvedValueOnce(message); - - // act & assert - await expect(gateway.findOne({ chatId, id })).resolves.toEqual(message); - }); - it('should throw an error if the message retrieval fails', async () => { - // arrange - const chatId = 'chatId'; - const id = 1; - const findMessageErrorMsg = 'Failed to find message'; - jest - .spyOn(messagesService, 'findMessage') - .mockRejectedValueOnce(new Error(findMessageErrorMsg)); - - // act & assert - await expect(gateway.findOne({ chatId, id })).rejects.toThrow( - findMessageErrorMsg, - ); - }); - }); - - describe('update', () => { - it('should return the updated message', async () => { - // arrange - const updateMessageDto: UpdateMessageDto = { - id: 1, - chatId: 'chatId', - name: 'name', - text: 'text', - timestamp: new Date(), - }; - - const { name, text, timestamp } = updateMessageDto; - const message = mockMessageDto({ - name, - text, - timestamp, - }); - - jest - .spyOn(messagesService, 'updateMessage') - .mockResolvedValueOnce(message); - - // act & assert - await expect(gateway.update(updateMessageDto)).resolves.toBe(message); - }); - it('should throw an error if the message update fails', async () => { - // arrange - const updateMessageDto: UpdateMessageDto = { - id: 1, - chatId: 'chatId', - name: 'name', - text: 'text', - timestamp: new Date(), - }; - - const updateMessageErrorMsg = 'Failed to update message'; - jest - .spyOn(messagesService, 'updateMessage') - .mockRejectedValueOnce(new Error(updateMessageErrorMsg)); - - // act & assert - await expect(gateway.update(updateMessageDto)).rejects.toThrow( - updateMessageErrorMsg, - ); - }); - }); - - describe('remove', () => { - it('should return the removed message', async () => { - // arrange - const identifyMessageDto: IdentifyMessageDto = { - chatId: 'chatId', - id: 1, - }; - const message = mockMessageDto({}); - jest - .spyOn(messagesService, 'removeMessage') - .mockResolvedValueOnce(message); - - // act & assert - await expect(gateway.remove(identifyMessageDto)).resolves.toEqual( - message, - ); - }); - it('should throw an error if the message removal fails', async () => { - // arrange - const identifyMessageDto: IdentifyMessageDto = { - chatId: 'chatId', - id: 1, - }; - const removeMessageErrorMsg = 'Failed to remove message'; - jest - .spyOn(messagesService, 'removeMessage') - .mockRejectedValueOnce(new Error(removeMessageErrorMsg)); - - // act & assert - await expect(gateway.remove(identifyMessageDto)).rejects.toThrow( - removeMessageErrorMsg, - ); - }); - }); - - describe('joinRoom', () => { - it('should return the identified client', async () => { - // arrange - const joinRoomDto: JoinRoomDto = { name: 'name', chatId: 'chatId' }; - const client: Partial = { id: 'id' }; - const clientName = 'name'; - jest - .spyOn(messagesService, 'identify') - .mockResolvedValueOnce([clientName]); - - // act & assert - await expect( - gateway.joinRoom(joinRoomDto, client as Socket), - ).resolves.toEqual([clientName]); - }); - it('should throw an error if the client identification fails', async () => { - // arrange - const joinRoomDto: JoinRoomDto = { name: 'name', chatId: 'chatId' }; - const client: Partial = { id: 'id' }; - const identifyErrorMsg = 'Failed to identify client'; - jest - .spyOn(messagesService, 'identify') - .mockRejectedValueOnce(new Error(identifyErrorMsg)); - - // act & assert - await expect( - gateway.joinRoom(joinRoomDto, client as Socket), - ).rejects.toThrow(identifyErrorMsg); - }); - }); - - describe('typing', () => { - it('should emit a typing event', async () => { - // arrange - const typingDto: TypingDto = { isTyping: true, chatId: 'chatId' }; - const emitSpy = jest.fn(); - const broadcast = { emit: emitSpy } as any; - const client: Partial = { - id: 'id', - broadcast, - }; - const name = 'name'; - jest.spyOn(messagesService, 'getClientName').mockResolvedValueOnce(name); - - // act - await gateway.typing(typingDto, client as Socket); - - // assert - expect(emitSpy).toHaveBeenCalledWith('typing', { - name, - isTyping: typingDto.isTyping, - }); - }); - it('should throw an error if the client name retrieval fails', async () => { - // arrange - const typingDto: TypingDto = { isTyping: true, chatId: 'chatId' }; - const client: Partial = { id: 'id' }; - const getClientNameErrorMsg = 'Failed to get client name'; - jest - .spyOn(messagesService, 'getClientName') - .mockRejectedValueOnce(new Error(getClientNameErrorMsg)); - - // act & assert - await expect(gateway.typing(typingDto, client as Socket)).rejects.toThrow( - getClientNameErrorMsg, - ); - }); - }); -}); diff --git a/backend/libs/chat/src/lib/chat.gateway.ts b/backend/libs/chat/src/lib/chat.gateway.ts deleted file mode 100644 index c6b03626..00000000 --- a/backend/libs/chat/src/lib/chat.gateway.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { - CreateMessageDto, - IdentifyMessageDto, - JoinRoomDto, - TypingDto, - UpdateMessageDto, -} from '@backend/shared-types'; -import { Logger } from '@nestjs/common'; -import { - ConnectedSocket, - MessageBody, - SubscribeMessage, - WebSocketGateway, - WebSocketServer, -} from '@nestjs/websockets'; -import { Server, Socket } from 'socket.io'; -import { MessagesService } from './messages.service'; - -@WebSocketGateway({ - cors: { - origin: '*', - }, -}) -export class ChatGateway { - private readonly logger = new Logger(ChatGateway.name); - - @WebSocketServer() - server: Server; - - constructor(private readonly messagesService: MessagesService) {} - - @SubscribeMessage('createMessage') - async create(@MessageBody() createMessageDto: CreateMessageDto) { - const { name, timestamp } = createMessageDto; - - this.logger.verbose( - `Creating a new message for ${name} with timestamp ${timestamp}.`, - ); - const message = await this.messagesService.createMessage(createMessageDto); - - this.logger.verbose( - `Emit the new message from ${name} to all connected clients.`, - ); - this.server.emit('message', message); - return message; - } - - @SubscribeMessage('findAllMessages') - async findAll(@MessageBody() chatId: string) { - this.logger.verbose(`Returning all messages from the database.`); - return await this.messagesService.listMessages(chatId); - } - - @SubscribeMessage('findOneMessage') - async findOne(@MessageBody() identifyMessageDto: IdentifyMessageDto) { - const { chatId, id } = identifyMessageDto; - return await this.messagesService.findMessage(chatId, id); - } - - @SubscribeMessage('updateMessage') - async update(@MessageBody() updateMessageDto: UpdateMessageDto) { - const { chatId, id } = updateMessageDto; - return await this.messagesService.updateMessage( - chatId, - id, - updateMessageDto, - ); - } - - @SubscribeMessage('removeMessage') - async remove(@MessageBody() identifyMessageDto: IdentifyMessageDto) { - const { chatId, id } = identifyMessageDto; - return await this.messagesService.removeMessage(chatId, id); - } - - @SubscribeMessage('join') - async joinRoom( - @MessageBody() joinRoomDto: JoinRoomDto, - @ConnectedSocket() client: Socket, - ) { - const { name, chatId } = joinRoomDto; - this.logger.verbose(`Client ${client.id} joined as ${name}.`); - return await this.messagesService.identify(chatId, name, client.id); - } - - @SubscribeMessage('typing') - async typing( - @MessageBody() typingDto: TypingDto, - @ConnectedSocket() client: Socket, - ) { - const { isTyping, chatId } = typingDto; - const name = await this.messagesService.getClientName(chatId, client.id); - this.logger.verbose( - `Emitting typing event from ${name} to all connected clients.`, - ); - client.broadcast.emit('typing', { name, isTyping }); - } -} diff --git a/backend/libs/chat/src/lib/chat.module.ts b/backend/libs/chat/src/lib/chat.module.ts deleted file mode 100644 index ee92c2d3..00000000 --- a/backend/libs/chat/src/lib/chat.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Module } from '@nestjs/common'; -import { MongooseModule } from '@nestjs/mongoose'; -import { ChatController } from './chat.controller'; -import { ChatGateway } from './chat.gateway'; -import { ChatMongoDbService } from './chat.mongodb.service'; -import { ChatService } from './chat.service'; -import { MessagesService } from './messages.service'; -import { Chat, ChatSchema } from './schema/chat.schema'; - -@Module({ - imports: [ - MongooseModule.forFeature([{ name: Chat.name, schema: ChatSchema }]), - ], - controllers: [ChatController], - providers: [ChatGateway, ChatMongoDbService, MessagesService, ChatService], - exports: [ChatGateway, ChatMongoDbService, MessagesService, ChatService], -}) -export class ChatModule {} diff --git a/backend/libs/chat/src/lib/chat.mongodb.service.spec.ts b/backend/libs/chat/src/lib/chat.mongodb.service.spec.ts deleted file mode 100644 index fb669fbc..00000000 --- a/backend/libs/chat/src/lib/chat.mongodb.service.spec.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { ChatEntityDto } from '@backend/shared-types'; -import { mockChatEntityDto } from '@backend/util'; -import { getModelToken } from '@nestjs/mongoose'; -import { Test, TestingModule } from '@nestjs/testing'; -import { Document, Model } from 'mongoose'; -import { ChatMongoDbService } from './chat.mongodb.service'; -import { Chat, ChatDocument } from './schema/chat.schema'; - -describe('ChatMongoDbService', () => { - let service: ChatMongoDbService; - let chatModel: Model; - - const chat1 = { _id: '1', name: 'chat1' }; - const chat2 = { _id: '2', name: 'chat2' }; - - const mockChatDocument = ( - mock: Partial, - ): Partial> => ({ - toObject: jest.fn().mockReturnValue(mockChatEntityDto(mock)), - }); - - beforeAll(() => { - jest.useFakeTimers(); - }); - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - ChatMongoDbService, - { - provide: getModelToken(Chat.name), - useValue: { - find: jest.fn(), - findOne: jest.fn(), - create: jest.fn(), - findOneAndUpdate: jest.fn(), - findOneAndDelete: jest.fn(), - }, - }, - ], - }).compile(); - - service = module.get(ChatMongoDbService); - chatModel = module.get>(getModelToken(Chat.name)); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('findAll', () => { - it('should find all chats', async () => { - // arrange - const expectedChats: Partial[] = [ - mockChatEntityDto(chat1), - mockChatEntityDto(chat2), - ]; - const result: Document[] = [ - mockChatDocument(chat1) as Document, - mockChatDocument(chat2) as Document, - ]; - jest.spyOn(chatModel, 'find').mockReturnValueOnce({ - exec: jest.fn().mockResolvedValueOnce(result), - } as never); - - // act & assert - expect(await service.findAll()).toEqual(expectedChats); - }); - it('should return empty array when no chats found', async () => { - // arrange - jest.spyOn(chatModel, 'find').mockReturnValueOnce({ - exec: jest.fn().mockResolvedValueOnce([]), - } as never); - - // act & assert - expect(await service.findAll()).toEqual([]); - }); - - it('should throw an error when finding all chats fails', async () => { - // arrange - const findAllChatsErrorMsg = 'Failed to find chats'; - jest.spyOn(chatModel, 'find').mockReturnValueOnce({ - exec: jest.fn().mockRejectedValueOnce(new Error(findAllChatsErrorMsg)), - } as never); - - // act & assert - await expect(service.findAll()).rejects.toThrow(findAllChatsErrorMsg); - }); - }); - - describe('findOne', () => { - it('should find a chat by id', async () => { - // arrange - const expectedChat = mockChatEntityDto(chat1); - const result = mockChatDocument(chat1) as Document; - jest.spyOn(chatModel, 'findOne').mockReturnValueOnce({ - exec: jest.fn().mockResolvedValueOnce(result), - } as never); - - // act & assert - expect(await service.findOne(chat1._id)).toEqual(expectedChat); - }); - it('should throw a not found exception when chat is not found', async () => { - // arrange - jest.spyOn(chatModel, 'findOne').mockReturnValueOnce({ - exec: jest.fn().mockResolvedValueOnce(null), - } as never); - - // act & assert - await expect(service.findOne(chat1._id)).rejects.toThrow( - 'Chat not found', - ); - }); - it('should throw an error when finding a chat by id fails', async () => { - // arrange - const chatId = '1'; - const findOneChatErrorMsg = 'Failed to find chat'; - jest.spyOn(chatModel, 'findOne').mockReturnValueOnce({ - exec: jest.fn().mockRejectedValueOnce(new Error(findOneChatErrorMsg)), - } as never); - - // act & assert - await expect(service.findOne(chatId)).rejects.toThrow( - findOneChatErrorMsg, - ); - }); - }); - - describe('create', () => { - it('should create a chat', async () => { - // arrange - const expectedChat = mockChatEntityDto(chat1); - const result = mockChatDocument(chat1) as Document; - jest.spyOn(chatModel, 'create').mockReturnValueOnce({ - save: jest.fn().mockResolvedValueOnce(result), - } as never); - - // act & assert - expect(await service.create(chat1.name)).toEqual(expectedChat); - }); - it('should throw an error when creating a chat fails', async () => { - // arrange - const createChatErrorMsg = 'Failed to create chat'; - jest.spyOn(chatModel, 'create').mockReturnValueOnce({ - save: jest.fn().mockRejectedValueOnce(new Error(createChatErrorMsg)), - } as never); - - // act & assert - await expect(service.create(chat1.name)).rejects.toThrow( - createChatErrorMsg, - ); - }); - }); - - describe('update', () => { - it('should update a chat', async () => { - // arrange - const expectedChat = mockChatEntityDto(chat1); - const result = mockChatDocument(chat1) as Document; - jest.spyOn(chatModel, 'findOneAndUpdate').mockReturnValueOnce({ - exec: jest.fn().mockResolvedValueOnce(result), - } as never); - - // act & assert - expect(await service.update(chat1._id, { name: chat1.name })).toEqual( - expectedChat, - ); - }); - it('should throw a not found exception when chat is not found', async () => { - // arrange - const chatId = '1'; - jest.spyOn(chatModel, 'findOneAndUpdate').mockReturnValueOnce({ - exec: jest.fn().mockResolvedValueOnce(null), - } as never); - - // act & assert - await expect(service.update(chatId, { name: 'chat1' })).rejects.toThrow( - 'Chat not found', - ); - }); - it('should throw an error when updating a chat fails', async () => { - // arrange - const chatId = '1'; - const updateChatErrorMsg = 'Failed to update chat'; - jest.spyOn(chatModel, 'findOneAndUpdate').mockReturnValueOnce({ - exec: jest.fn().mockRejectedValueOnce(new Error(updateChatErrorMsg)), - } as never); - - // act & assert - await expect(service.update(chatId, { name: 'chat1' })).rejects.toThrow( - updateChatErrorMsg, - ); - }); - }); - - describe('remove', () => { - it('should remove a chat', async () => { - // arrange - const expectedChat = mockChatEntityDto(chat1); - const result = mockChatDocument(chat1) as Document; - jest.spyOn(chatModel, 'findOneAndDelete').mockReturnValueOnce({ - exec: jest.fn().mockResolvedValueOnce(result), - } as never); - - // act & assert - expect(await service.remove(chat1._id)).toEqual(expectedChat); - }); - it('should throw a not found exception when chat is not found', async () => { - // arrange - jest.spyOn(chatModel, 'findOneAndDelete').mockReturnValueOnce({ - exec: jest.fn().mockResolvedValueOnce(null), - } as never); - - // act & assert - await expect(service.remove(chat1._id)).rejects.toThrow('Chat not found'); - }); - it('should throw an error when deleting a chat fails', async () => { - // arrange - const deleteChatErrorMsg = 'Failed to delete chat'; - jest.spyOn(chatModel, 'findOneAndDelete').mockReturnValueOnce({ - exec: jest.fn().mockRejectedValueOnce(new Error(deleteChatErrorMsg)), - } as never); - - // act & assert - await expect(service.remove(chat1._id)).rejects.toThrow( - deleteChatErrorMsg, - ); - }); - }); -}); diff --git a/backend/libs/chat/src/lib/chat.mongodb.service.ts b/backend/libs/chat/src/lib/chat.mongodb.service.ts deleted file mode 100644 index 18a28890..00000000 --- a/backend/libs/chat/src/lib/chat.mongodb.service.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { ChatTexts } from '@backend/shared-texts'; -import { ChatEntityDto } from '@backend/shared-types'; -import { Injectable, Logger, NotFoundException } from '@nestjs/common'; -import { InjectModel } from '@nestjs/mongoose'; -import { randomUUID } from 'crypto'; -import { Filter } from 'mongodb'; -import { Model } from 'mongoose'; -import { Chat, ChatDocument } from './schema/chat.schema'; - -@Injectable() -export class ChatMongoDbService { - private readonly logger = new Logger(ChatMongoDbService.name); - - constructor( - @InjectModel(Chat.name) - private chatModel: Model, - ) {} - - /** - * Fetches all chats from the mongodb collection 'chats'. - * - * @param filter Optional param to filter for specific chat results. - * @returns An array of chat objects. - */ - async findAll( - filter: Filter = {}, - ): Promise { - this.logger.debug({ input: { filter } }, ChatTexts.DB_ATTEMPT_FIND_ALL); - const chats = await this.chatModel.find(filter as any).exec(); - const chatEntities = chats.map((chat) => chat.toObject() as ChatEntityDto); - this.logger.debug( - { output: { chats } }, - `Found ${chats.length} chats in mongodb.`, - ); - return chatEntities; - } - - /** - * Fetches a chat by its id from the mongodb collection 'chats'. - * - * @param id The id of the chat. - * @returns A single chat object. - */ - async findOne(id: string): Promise { - this.logger.debug({ input: { id } }, ChatTexts.DB_ATTEMPT_FIND_ONE); - const chat = await this.chatModel.findOne({ _id: id }).exec(); - - if (!chat) { - throw new NotFoundException(ChatTexts.NOT_FOUND); - } - const chatEntity = chat.toObject() as ChatEntityDto; - this.logger.debug({ output: { chat: chatEntity } }, ChatTexts.DB_FOUND_ONE); - return chatEntity; - } - - /** - * Creates a chat in the mongodb collection 'chats'. - * - * @param name The name of the chat. - * @returns The created chat object. - */ - async create(name: string): Promise { - this.logger.debug({ input: {} }, ChatTexts.DB_ATTEMPT_CREATE); - const chat = ( - await this.chatModel.create({ - _id: randomUUID(), - name, - }) - ).save(); - const chatEntity = (await chat).toObject() as ChatEntityDto; - this.logger.debug( - { output: { chat: chatEntity } }, - ChatTexts.DB_CREATED_ONE, - ); - return chatEntity; - } - - /** - * Updates a chat by its id in the mongodb collection 'chats'. - * - * @param id The id of the chat. - * @param chatDto The chat information to be updated. - * @returns The updated chat object. - */ - async update( - id: string, - chatDto: Partial, - ): Promise { - this.logger.debug({ input: { id, chatDto } }, ChatTexts.DB_ATTEMPT_UPDATE); - const chat = await this.chatModel - .findOneAndUpdate( - { _id: id }, - { ...chatDto, updated: new Date() }, - { new: true }, - ) - .exec(); - - if (!chat) { - throw new NotFoundException(ChatTexts.NOT_FOUND); - } - const chatEntity = chat.toObject() as ChatEntityDto; - this.logger.debug( - { output: { chat: chatEntity } }, - ChatTexts.DB_UPDATED_ONE, - ); - return chatEntity; - } - - /** - * Removes a chat by its id from the mongodb collection 'chats'. - * - * @param id The id of the chat. - * @returns The deleted chat object. - */ - async remove(id: string): Promise { - this.logger.debug({ input: { id } }, ChatTexts.DB_ATTEMPT_DELETE); - const chat = await this.chatModel.findOneAndDelete({ _id: id }).exec(); - - if (!chat) { - throw new NotFoundException(ChatTexts.NOT_FOUND); - } - const chatEntity = chat.toObject() as ChatEntityDto; - this.logger.debug({ output: { chatEntity } }, ChatTexts.DB_DELETE_ONE); - return chatEntity; - } -} diff --git a/backend/libs/chat/src/lib/chat.service.spec.ts b/backend/libs/chat/src/lib/chat.service.spec.ts deleted file mode 100644 index ce21e861..00000000 --- a/backend/libs/chat/src/lib/chat.service.spec.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { mockChatEntityDto } from '@backend/util'; -import { Test, TestingModule } from '@nestjs/testing'; -import { ChatMongoDbService } from './chat.mongodb.service'; -import { ChatService } from './chat.service'; - -describe('ChatService', () => { - let service: ChatService; - let chatMongoDbServiceMock: ChatMongoDbService; - - beforeAll(() => { - jest.useFakeTimers(); - }); - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - { - provide: ChatMongoDbService, - useValue: { - create: jest.fn(), - findAll: jest.fn(), - findOne: jest.fn(), - update: jest.fn(), - remove: jest.fn(), - }, - }, - ChatService, - ], - }).compile(); - - service = module.get(ChatService); - chatMongoDbServiceMock = module.get(ChatMongoDbService); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should be defined.', () => { - expect(service).toBeDefined(); - }); - - describe('createChat', () => { - it('should create a chat.', async () => { - // arrange - const chatName = 'foo'; - const chat = mockChatEntityDto({ name: chatName }); - jest.spyOn(chatMongoDbServiceMock, 'create').mockResolvedValue(chat); - - // act - const result = await service.createChat(chatName); - - // assert - expect(result).toEqual(chat); - expect(chatMongoDbServiceMock.create).toHaveBeenCalledWith(chatName); - }); - it('should throw an error if chat creation fails.', async () => { - // arrange - jest - .spyOn(chatMongoDbServiceMock, 'create') - .mockRejectedValue(new Error()); - - // act & assert - await expect(service.createChat('chat')).rejects.toThrow(); - }); - }); - - describe('listChats', () => { - it('should list all chats.', async () => { - // arrange - const chat = mockChatEntityDto({}); - jest.spyOn(chatMongoDbServiceMock, 'findAll').mockResolvedValue([chat]); - - // act - const result = await service.listChats(); - - // assert - expect(result).toEqual([chat]); - expect(chatMongoDbServiceMock.findAll).toHaveBeenCalled(); - }); - it('should throw an error if chat listing fails.', async () => { - // arrange - jest - .spyOn(chatMongoDbServiceMock, 'findAll') - .mockRejectedValue(new Error()); - - // act & assert - await expect(service.listChats()).rejects.toThrow(); - }); - }); - - describe('findChat', () => { - it('should find a chat.', async () => { - // arrange - const chatId = 'foo'; - const chat = mockChatEntityDto({ _id: chatId }); - jest.spyOn(chatMongoDbServiceMock, 'findOne').mockResolvedValue(chat); - - // act - const result = await service.findChat(chatId); - - // assert - expect(result).toEqual(chat); - expect(chatMongoDbServiceMock.findOne).toHaveBeenCalledWith(chatId); - }); - it('should throw an error if chat finding fails.', async () => { - // arrange - jest - .spyOn(chatMongoDbServiceMock, 'findOne') - .mockRejectedValue(new Error()); - - // act & assert - await expect(service.findChat('chatId')).rejects.toThrow(); - }); - }); - - describe('updateChat', () => { - it('should update a chat.', async () => { - // arrange - const chatId = 'foo'; - const chat = mockChatEntityDto({ _id: chatId }); - jest.spyOn(chatMongoDbServiceMock, 'update').mockResolvedValue(chat); - - // act - const result = await service.updateChat(chatId, chat); - - // assert - expect(result).toEqual(chat); - expect(chatMongoDbServiceMock.update).toHaveBeenCalledWith(chatId, chat); - }); - it('should throw an error if chat updating fails.', async () => { - // arrange - jest - .spyOn(chatMongoDbServiceMock, 'update') - .mockRejectedValue(new Error()); - - // act & assert - await expect( - service.updateChat('chatId', mockChatEntityDto({})), - ).rejects.toThrow(); - }); - }); - - describe('removeChat', () => { - it('should remove a chat.', async () => { - // arrange - const chatId = 'foo'; - const chat = mockChatEntityDto({ _id: chatId }); - jest.spyOn(chatMongoDbServiceMock, 'remove').mockResolvedValue(chat); - - // act - const result = await service.removeChat(chatId); - - // assert - expect(result).toEqual(chat); - expect(chatMongoDbServiceMock.remove).toHaveBeenCalledWith(chatId); - }); - it('should throw an error if chat removal fails.', async () => { - // arrange - jest - .spyOn(chatMongoDbServiceMock, 'remove') - .mockRejectedValue(new Error()); - - // act & assert - await expect(service.removeChat('chatId')).rejects.toThrow(); - }); - }); -}); diff --git a/backend/libs/chat/src/lib/chat.service.ts b/backend/libs/chat/src/lib/chat.service.ts deleted file mode 100644 index 7a2fdd6f..00000000 --- a/backend/libs/chat/src/lib/chat.service.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { ChatTexts } from '@backend/shared-texts'; -import { ChatEntityDto } from '@backend/shared-types'; -import { Injectable, Logger } from '@nestjs/common'; -import { Filter } from 'mongodb'; -import { ChatMongoDbService } from './chat.mongodb.service'; -import { ChatDocument } from './schema/chat.schema'; - -@Injectable() -export class ChatService { - private readonly logger = new Logger(ChatService.name); - - constructor(private chatMongoDbService: ChatMongoDbService) {} - - /** - * Creates a chat. - * - * @param name The name of the chat. - * @returns The created chat. - */ - async createChat(name: string): Promise { - this.logger.verbose({ input: { name } }, ChatTexts.ATTEMPT_CREATE); - const newChat = await this.chatMongoDbService.create(name); - this.logger.verbose({ output: { newChat } }, ChatTexts.CREATED_ONE); - return newChat; - } - - /** - * Fetches all chats. - * - * @param filter Optional param to filter for specific chat results. - * @returns An array of chat objects. - */ - async listChats( - filter: Filter = {}, - ): Promise { - this.logger.verbose(ChatTexts.ATTEMPT_FIND_ALL); - const chats = await this.chatMongoDbService.findAll(filter); - this.logger.verbose(`Found ${chats.length} messages.`); - return chats; - } - - /** - * Fetches a chat by its id. - * - * @param id The id of the chat. - * @returns A single chat. - */ - async findChat(id: string): Promise { - this.logger.verbose({ input: { id } }, ChatTexts.ATTEMPT_FIND_ONE); - const chat = await this.chatMongoDbService.findOne(id); - this.logger.verbose({ output: { chat } }, ChatTexts.FOUND_ONE); - return chat; - } - - /** - * Updates a chat. - * - * @param id The id of the chat. - * @param updateChatDto The chat information to be updated. - * @returns The updated chat. - */ - async updateChat( - id: string, - updateChatDto: ChatEntityDto, - ): Promise { - this.logger.verbose( - { input: { id, updateMessageDto: updateChatDto } }, - ChatTexts.ATTEMPT_UPDATE, - ); - const chat = await this.chatMongoDbService.update(id, updateChatDto); - this.logger.verbose({ output: { chat } }, ChatTexts.UPDATED_ONE); - return chat; - } - - /** - * Deletes a chat. - * - * @param id The id of the chat. - * @returns The deleted chat. - */ - async removeChat(id: string): Promise { - this.logger.verbose({ input: { id } }, ChatTexts.ATTEMPT_DELETE); - const chat = await this.chatMongoDbService.remove(id); - this.logger.verbose({ output: { chat } }, ChatTexts.DELETE_ONE); - return chat; - } -} diff --git a/backend/libs/chat/src/lib/messages.service.spec.ts b/backend/libs/chat/src/lib/messages.service.spec.ts deleted file mode 100644 index 7910dc74..00000000 --- a/backend/libs/chat/src/lib/messages.service.spec.ts +++ /dev/null @@ -1,350 +0,0 @@ -import { UpdateMessageDto } from '@backend/shared-types'; -import { mockChatEntityDto, mockMessageDto } from '@backend/util'; -import { Test, TestingModule } from '@nestjs/testing'; -import { ChatMongoDbService } from './chat.mongodb.service'; -import { MessagesService } from './messages.service'; - -describe('MessagesService', () => { - let service: MessagesService; - let chatMongoDbServiceMock: ChatMongoDbService; - - beforeAll(() => { - jest.useFakeTimers(); - }); - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - { - provide: ChatMongoDbService, - useValue: { - create: jest.fn(), - findAll: jest.fn(), - findOne: jest.fn(), - update: jest.fn(), - remove: jest.fn(), - }, - }, - MessagesService, - ], - }).compile(); - - service = module.get(MessagesService); - chatMongoDbServiceMock = module.get(ChatMongoDbService); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should be defined.', () => { - expect(service).toBeDefined(); - }); - - describe('createMessage', () => { - it('should create a message', async () => { - // arrange - const chatId = 'foo'; - const messageDto = mockMessageDto({ text: 'bar' }); - const createMessageDto = { chatId, ...messageDto }; - const chat = mockChatEntityDto({ messages: [] }); - - jest.spyOn(chatMongoDbServiceMock, 'findOne').mockResolvedValue(chat); - - chat.messages.push(messageDto); - jest.spyOn(chatMongoDbServiceMock, 'update').mockResolvedValue(chat); - - // act - const result = await service.createMessage(createMessageDto); - - // assert - expect(result).toEqual(createMessageDto); - expect(chat.messages).toContain(messageDto); - }); - it('should throw an error if chat findOne fails', async () => { - // arrange - const chatId = 'foo'; - const createMessageDto = { chatId, ...mockMessageDto({ text: 'bar' }) }; - - jest - .spyOn(chatMongoDbServiceMock, 'findOne') - .mockRejectedValue(new Error()); - - // act - try { - await service.createMessage(createMessageDto); - } catch (error) { - // assert - expect(error).toBeInstanceOf(Error); - } - }); - it('should throw an error if chat update fails', async () => { - // arrange - const chatId = 'foo'; - const createMessageDto = { chatId, ...mockMessageDto({ text: 'bar' }) }; - const chat = mockChatEntityDto({ messages: [] }); - - jest.spyOn(chatMongoDbServiceMock, 'findOne').mockResolvedValue(chat); - jest - .spyOn(chatMongoDbServiceMock, 'update') - .mockRejectedValue(new Error()); - - // act - try { - await service.createMessage(createMessageDto); - } catch (error) { - // assert - expect(error).toBeInstanceOf(Error); - } - }); - }); - - describe('listMessages', () => { - it('should list messages', async () => { - // arrange - const chatId = 'foo'; - const chat = mockChatEntityDto({ messages: [mockMessageDto({})] }); - - jest.spyOn(chatMongoDbServiceMock, 'findOne').mockResolvedValue(chat); - - // act - const result = await service.listMessages(chatId); - - // assert - expect(result).toEqual(chat.messages); - }); - it('should throw an error if chat findOne fails', async () => { - // arrange - const chatId = 'foo'; - - jest - .spyOn(chatMongoDbServiceMock, 'findOne') - .mockRejectedValue(new Error()); - - // act & assert - await expect(service.listMessages(chatId)).rejects.toThrow(); - }); - }); - - describe('findMessage', () => { - it('should find a message', async () => { - // arrange - const chatId = 'foo'; - const messageId = 0; - const message = mockMessageDto({}); - - jest - .spyOn(chatMongoDbServiceMock, 'findOne') - .mockResolvedValue(mockChatEntityDto({ messages: [message] })); - - // act - const result = await service.findMessage(chatId, messageId); - - // assert - expect(result).toEqual(message); - }); - it('should throw an error if chat findOne fails', async () => { - // arrange - const chatId = 'foo'; - const messageId = 0; - - jest - .spyOn(chatMongoDbServiceMock, 'findOne') - .mockRejectedValue(new Error()); - - // act & assert - await expect(service.findMessage(chatId, messageId)).rejects.toThrow(); - }); - }); - - describe('updateMessage', () => { - it('should update a message', async () => { - // arrange - const chatId = 'foo'; - const messageId = 0; - const messageDto = mockMessageDto({ text: 'bar' }); - const updateMessageDto: UpdateMessageDto = { - id: messageId, - chatId, - ...messageDto, - }; - const chat = mockChatEntityDto({ messages: [mockMessageDto({})] }); - - jest.spyOn(chatMongoDbServiceMock, 'findOne').mockResolvedValue(chat); - - chat.messages[messageId] = messageDto; - jest.spyOn(chatMongoDbServiceMock, 'update').mockResolvedValue(chat); - - // act - const result = await service.updateMessage( - chatId, - messageId, - updateMessageDto, - ); - - // assert - expect(result).toEqual(updateMessageDto); - expect(chat.messages[messageId]).toEqual(updateMessageDto); - }); - it('should throw an error if chat findOne fails', async () => { - // arrange - const chatId = 'foo'; - const messageId = 0; - const messageDto = mockMessageDto({ text: 'bar' }); - const updateMessageDto = { id: messageId, chatId, ...messageDto }; - - jest - .spyOn(chatMongoDbServiceMock, 'findOne') - .mockRejectedValue(new Error()); - - // act & assert - await expect( - service.updateMessage(chatId, messageId, updateMessageDto), - ).rejects.toThrow(); - }); - it('should throw an error if chat update fails', async () => { - // arrange - const chatId = 'foo'; - const messageId = 0; - const messageDto = mockMessageDto({ text: 'bar' }); - const updateMessageDto = { id: messageId, chatId, ...messageDto }; - const chat = mockChatEntityDto({ messages: [mockMessageDto({})] }); - - jest.spyOn(chatMongoDbServiceMock, 'findOne').mockResolvedValue(chat); - jest - .spyOn(chatMongoDbServiceMock, 'update') - .mockRejectedValue(new Error()); - - // act & assert - await expect( - service.updateMessage(chatId, messageId, updateMessageDto), - ).rejects.toThrow(); - }); - }); - - describe('removeMessage', () => { - it('should remove a message', async () => { - // arrange - const chatId = 'foo'; - const messageId = 0; - const message = mockMessageDto({}); - - jest - .spyOn(chatMongoDbServiceMock, 'findOne') - .mockResolvedValue(mockChatEntityDto({ messages: [message] })); - - // act - const result = await service.removeMessage(chatId, messageId); - - // assert - expect(result).toEqual(message); - }); - it('should throw an error if chat findOne fails', async () => { - // arrange - const chatId = 'foo'; - const messageId = 0; - - jest - .spyOn(chatMongoDbServiceMock, 'findOne') - .mockRejectedValue(new Error()); - - // act & assert - await expect(service.removeMessage(chatId, messageId)).rejects.toThrow(); - }); - it('should throw an error if chat update fails', async () => { - // arrange - const chatId = 'foo'; - const messageId = 0; - const message = mockMessageDto({}); - - jest - .spyOn(chatMongoDbServiceMock, 'findOne') - .mockResolvedValue(mockChatEntityDto({ messages: [message] })); - jest - .spyOn(chatMongoDbServiceMock, 'update') - .mockRejectedValue(new Error()); - - // act & assert - await expect(service.removeMessage(chatId, messageId)).rejects.toThrow(); - }); - }); - - describe('identify', () => { - it('should identify a client', async () => { - // arrange - const chatId = 'foo'; - const name = 'bar'; - const clientId = 'baz'; - const chat = mockChatEntityDto({ clients: {} }); - - jest.spyOn(chatMongoDbServiceMock, 'findOne').mockResolvedValue(chat); - - chat.clients[clientId] = name; - jest.spyOn(chatMongoDbServiceMock, 'update').mockResolvedValue(chat); - - // act - const result = await service.identify(chatId, name, clientId); - - // assert - expect(result).toEqual(Object.values(chat.clients)); - }); - it('should throw an error if chat findOne fails', async () => { - // arrange - const chatId = 'foo'; - const name = 'bar'; - const clientId = 'baz'; - - jest - .spyOn(chatMongoDbServiceMock, 'findOne') - .mockRejectedValue(new Error()); - - // act & assert - await expect(service.identify(chatId, name, clientId)).rejects.toThrow(); - }); - it('should throw an error if chat update fails', async () => { - // arrange - const chatId = 'foo'; - const name = 'bar'; - const clientId = 'baz'; - const chat = mockChatEntityDto({ clients: {} }); - - jest.spyOn(chatMongoDbServiceMock, 'findOne').mockResolvedValue(chat); - jest - .spyOn(chatMongoDbServiceMock, 'update') - .mockRejectedValue(new Error()); - - // act & assert - await expect(service.identify(chatId, name, clientId)).rejects.toThrow(); - }); - }); - - describe('getClientName', () => { - it('should get a client name', async () => { - // arrange - const chatId = 'foo'; - const clientId = 'bar'; - const name = 'baz'; - const chat = mockChatEntityDto({ clients: { [clientId]: name } }); - - jest.spyOn(chatMongoDbServiceMock, 'findOne').mockResolvedValue(chat); - - // act - const result = await service.getClientName(chatId, clientId); - - // assert - expect(result).toEqual(name); - }); - it('should throw an error if chat findOne fails', async () => { - // arrange - const chatId = 'foo'; - const clientId = 'bar'; - - jest - .spyOn(chatMongoDbServiceMock, 'findOne') - .mockRejectedValue(new Error()); - - // act & assert - await expect(service.getClientName(chatId, clientId)).rejects.toThrow(); - }); - }); -}); diff --git a/backend/libs/chat/src/lib/messages.service.ts b/backend/libs/chat/src/lib/messages.service.ts deleted file mode 100644 index ea9f2b00..00000000 --- a/backend/libs/chat/src/lib/messages.service.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { - CreateMessageDto, - MessageDto, - UpdateMessageDto, -} from '@backend/shared-types'; -import { Injectable, Logger } from '@nestjs/common'; -import { ChatMongoDbService } from './chat.mongodb.service'; - -@Injectable() -export class MessagesService { - private readonly logger = new Logger(MessagesService.name); - - constructor(private chatMongoDbService: ChatMongoDbService) {} - - /** - * Creates a new message in a chat. - * - * @param chatId The id of the chat in which the message should be created. - * @param createMessageDto The message to be created. - * @returns The created message. - */ - async createMessage(createMessageDto: CreateMessageDto): Promise { - const { chatId } = createMessageDto; - this.logger.verbose( - { input: { chatId, createMessageDto } }, - `Adding new message to chat.`, - ); - const newMessage = { ...createMessageDto, timestamp: new Date() }; - const chat = await this.chatMongoDbService.findOne(chatId); - chat.messages.push(newMessage); - const updatedChat = await this.chatMongoDbService.update(chatId, chat); - const updatedMessage = - updatedChat.messages[updatedChat.messages.length - 1]; - this.logger.verbose( - { output: { updatedMessage } }, - `Added new message to chat.`, - ); - return updatedMessage; - } - - /** - * Fetches all messages in a chat. - * - * @param chatId The id of the chat. - * @returns An array of message objects. - */ - async listMessages(chatId: string) { - this.logger.verbose({ input: { chatId } }, `Retrieving all messages.`); - const chat = await this.chatMongoDbService.findOne(chatId); - const { messages } = chat; - this.logger.verbose(`Found ${messages.length} messages.`); - return messages; - } - - /** - * Fetches a message by its id in a chat. - * - * @param chatId The id of the chat. - * @param id The id of the message. - * @returns A single message. - */ - async findMessage(chatId: string, id: number) { - this.logger.verbose({ input: { id } }, `Retrieving message.`); - const message = (await this.chatMongoDbService.findOne(chatId))?.messages[ - id - ]; - this.logger.verbose({ output: { message } }, `Found message.`); - return message; - } - - /** - * Updates a message in a chat. - * - * @param chatId The id of the chat. - * @param id The id of the message. - * @param updateMessageDto The message information to be updated. - * @returns The updated message. - */ - async updateMessage( - chatId: string, - id: number, - updateMessageDto: UpdateMessageDto, - ) { - this.logger.verbose({ input: { id } }, `Updating message .`); - const chat = await this.chatMongoDbService.findOne(chatId); - chat.messages[id] = { ...chat.messages[id], ...updateMessageDto }; - const message = chat.messages[id]; - await this.chatMongoDbService.update(chatId, chat); - this.logger.verbose({ output: { message } }, `Updated message.`); - return message; - } - - /** - * Deletes a message in a chat. - * - * @param chatId The id of the chat. - * @param id The id of the message. - * @returns The deleted message. - */ - async removeMessage(chatId: string, id: number) { - this.logger.verbose({ input: { id } }, `Removing message.`); - const chat = await this.chatMongoDbService.findOne(chatId); - const message = chat.messages.splice(id, 1)[0]; - await this.chatMongoDbService.update(chatId, chat); - this.logger.verbose({ output: { message } }, `Removed message.`); - return message; - } - - /** - * Identifies a client in a chat. - * - * @param chatId The id of the chat. - * @param name The name of the client. - * @param clientId The id of the client. - * @returns An array of client names. - */ - async identify(chatId: string, name: string, clientId: string) { - this.logger.verbose( - { input: { name, clientId } }, - `Identifing client in chat.`, - ); - const chat = await this.chatMongoDbService.findOne(chatId); - chat.clients[clientId] = name; - const client = Object.values(chat.clients); - await this.chatMongoDbService.update(chatId, chat); - this.logger.verbose({ output: { client } }, `Identified client.`); - return client; - } - - /** - * Fetches the name of a client in a chat. - * - * @param chatId The id of the chat. - * @param clientId The id of the client. - * @returns A single client name. - */ - async getClientName(chatId: string, clientId: string) { - this.logger.verbose( - { input: { clientId } }, - `Retrieving client from chat.`, - ); - const chat = await this.chatMongoDbService.findOne(chatId); - const client = chat.clients[clientId]; - this.logger.verbose({ output: { client } }, `Found client.`); - return client; - } -} diff --git a/backend/libs/chat/src/lib/schema/chat.schema.ts b/backend/libs/chat/src/lib/schema/chat.schema.ts deleted file mode 100644 index b031aae9..00000000 --- a/backend/libs/chat/src/lib/schema/chat.schema.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; -import { Document } from 'mongoose'; - -export type ChatDocument = Chat & Document; - -export class ChatMessage { - @Prop({ required: true }) - name: string; - - @Prop({ required: true }) - text: string; - - @Prop({ - type: Date, - required: true, - default: () => new Date(Date.now() + 5000), - }) - timestamp: Date; -} - -@Schema({ _id: false, collection: 'chats' }) -export class Chat { - @Prop({ required: true }) - _id: string; - - @Prop({ required: true }) - name: string; - - @Prop({ type: [ChatMessage], required: true, default: [] }) - messages: ChatMessage[]; - - @Prop({ type: Map, of: String, default: {} }) - clients: Record; - - @Prop({ type: Date, required: true, default: new Date() }) - created: Date; - - @Prop({ type: Date, required: true, default: new Date() }) - updated: Date; - - @Prop({ required: true }) - emoji: string; - - @Prop({ required: true }) - owner: string; - - @Prop({ required: true }) - securityQuestion: string; - - @Prop({ required: true }) - securityAnswer: string; -} - -export const ChatSchema = SchemaFactory.createForClass(Chat); diff --git a/backend/libs/chat/tsconfig.json b/backend/libs/chat/tsconfig.json deleted file mode 100644 index f5b85657..00000000 --- a/backend/libs/chat/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "module": "commonjs", - "forceConsistentCasingInFileNames": true, - "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true - }, - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" - } - ] -} diff --git a/backend/libs/chat/tsconfig.lib.json b/backend/libs/chat/tsconfig.lib.json deleted file mode 100644 index 89261624..00000000 --- a/backend/libs/chat/tsconfig.lib.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "declaration": true, - "types": ["node"], - "target": "es2021", - "strictNullChecks": true, - "strictBindCallApply": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src/**/*.ts"], - "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] -} diff --git a/backend/libs/chat/tsconfig.spec.json b/backend/libs/chat/tsconfig.spec.json deleted file mode 100644 index 9b2a121d..00000000 --- a/backend/libs/chat/tsconfig.spec.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "module": "commonjs", - "types": ["jest", "node"] - }, - "include": [ - "jest.config.ts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.d.ts" - ] -} diff --git a/backend/libs/shared/common/texts/src/index.ts b/backend/libs/shared/common/texts/src/index.ts index 4e5f0be5..d6734ea5 100644 --- a/backend/libs/shared/common/texts/src/index.ts +++ b/backend/libs/shared/common/texts/src/index.ts @@ -1,4 +1,3 @@ -export * from './lib/chat-texts'; export * from './lib/identifiers-texts'; export * from './lib/jokes-texts'; export * from './lib/keystore-texts'; diff --git a/backend/libs/shared/common/texts/src/lib/chat-texts.ts b/backend/libs/shared/common/texts/src/lib/chat-texts.ts deleted file mode 100644 index 9b72e886..00000000 --- a/backend/libs/shared/common/texts/src/lib/chat-texts.ts +++ /dev/null @@ -1,22 +0,0 @@ -export class ChatTexts { - static readonly DB_ATTEMPT_FIND_ALL = - 'Attempt to list all chats from mongodb.'; - static readonly DB_ATTEMPT_FIND_ONE = 'Attempt to get a chat from mongodb.'; - static readonly DB_ATTEMPT_CREATE = 'Attempt to create a chat in mongodb.'; - static readonly DB_ATTEMPT_UPDATE = 'Attempt to update a chat in mongodb.'; - static readonly DB_ATTEMPT_DELETE = 'Attempt to delete a chat from mongodb.'; - static readonly DB_FOUND_ONE = 'Found chat in mongodb.'; - static readonly DB_CREATED_ONE = 'Created chat in mongodb.'; - static readonly DB_UPDATED_ONE = 'Updated chat in mongodb.'; - static readonly DB_DELETE_ONE = 'Deleted chat from mongodb.'; - static readonly ATTEMPT_FIND_ALL = 'Attempt to list all chats.'; - static readonly ATTEMPT_FIND_ONE = 'Attempt to get a chat.'; - static readonly ATTEMPT_CREATE = 'Attempt to create a chat.'; - static readonly ATTEMPT_UPDATE = 'Attempt to update a chat.'; - static readonly ATTEMPT_DELETE = 'Attempt to delete a chat.'; - static readonly FOUND_ONE = 'Found chat.'; - static readonly CREATED_ONE = 'Created chat.'; - static readonly UPDATED_ONE = 'Updated chat.'; - static readonly DELETE_ONE = 'Deleted chat.'; - static readonly NOT_FOUND = 'Chat not found.'; -} diff --git a/backend/libs/shared/common/types/src/index.ts b/backend/libs/shared/common/types/src/index.ts index 2be85a5f..bbdedc24 100644 --- a/backend/libs/shared/common/types/src/index.ts +++ b/backend/libs/shared/common/types/src/index.ts @@ -1,14 +1,6 @@ export * from './lib/admin.dto'; -export * from './lib/chat.dto'; -export * from './lib/chat.entity'; -export * from './lib/create-message.dto'; export * from './lib/identifiers.dto'; -export * from './lib/identify-message.dto'; -export * from './lib/join-room.dto'; export * from './lib/jokes.dto'; export * from './lib/keystore.dto'; -export * from './lib/message.entity'; export * from './lib/ocr-space.dto'; -export * from './lib/typing.dto'; -export * from './lib/update-message.dto'; export * from './lib/todo.dto'; diff --git a/backend/libs/shared/common/types/src/lib/chat.dto.ts b/backend/libs/shared/common/types/src/lib/chat.dto.ts deleted file mode 100644 index e191c2e7..00000000 --- a/backend/libs/shared/common/types/src/lib/chat.dto.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsOptional } from 'class-validator'; - -export class MessageDto { - @ApiProperty({ description: 'Message author name' }) - @IsNotEmpty() - name: string; - - @ApiProperty({ description: 'Message text' }) - @IsNotEmpty() - text: string; - - @ApiProperty({ description: 'Message timestamp' }) - @IsNotEmpty() - timestamp: Date; -} - -export class ChatDto { - @ApiProperty({ description: 'Chat ID', required: false }) - @IsNotEmpty() - _id: string; - - @ApiProperty({ description: 'Chat name', required: false }) - @IsNotEmpty() - name: string; - - @ApiProperty({ - type: [MessageDto], - description: 'Chat name', - required: false, - }) - @IsNotEmpty() - messages: MessageDto[]; - - @ApiProperty({ description: 'Chat creation date', required: false }) - @IsOptional() - created: Date; - - @ApiProperty({ description: 'Chat update date', required: false }) - @IsOptional() - updated: Date; - - @ApiProperty({ description: 'Chat emoji', required: false }) - @IsOptional() - emoji: string; - - @ApiProperty({ description: 'Chat owner id', required: false }) - @IsNotEmpty() - owner: string; - - @ApiProperty({ description: 'Chat security question', required: false }) - @IsNotEmpty() - securityQuestion: string; - - @ApiProperty({ description: 'Chat security answer', required: false }) - @IsNotEmpty() - securityAnswer: string; -} - -export class GetChatsOutputDto extends ChatDto {} - -export class GetChatInputDto { - @ApiProperty({ description: 'Chat ID' }) - @IsNotEmpty() - id: string; -} -export class GetChatOutputDto extends ChatDto {} - -export class CreateChatInputDto { - @ApiProperty({ description: 'Chat name' }) - @IsNotEmpty() - name: string; -} -export class CreateChatOutputDto extends ChatDto {} - -export class UpdateChatInputDto { - @ApiProperty({ description: 'Chat ID' }) - @IsNotEmpty() - id: string; -} -export class UpdateChatOutputDto extends ChatDto {} - -export class RemoveChatInputDto { - @ApiProperty({ description: 'Chat ID' }) - @IsNotEmpty() - id: string; -} -export class RemoveChatOutputDto extends ChatDto {} diff --git a/backend/libs/shared/common/types/src/lib/chat.entity.ts b/backend/libs/shared/common/types/src/lib/chat.entity.ts deleted file mode 100644 index dd34014d..00000000 --- a/backend/libs/shared/common/types/src/lib/chat.entity.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsOptional } from 'class-validator'; -import { Message } from './message.entity'; - -export class ChatEntityDto { - @ApiProperty({ description: 'Chat id' }) - @IsOptional() - _id: string; - - @ApiProperty({ description: 'Chat name' }) - @IsOptional() - name: string; - - @ApiProperty({ description: 'Chat messages' }) - @IsOptional() - messages: Message[]; - - @ApiProperty({ description: 'Chat clients' }) - @IsOptional() - clients: Record; - - @ApiProperty({ description: 'Chat creation date' }) - @IsOptional() - created: Date; - - @ApiProperty({ description: 'Chat update date' }) - @IsOptional() - updated: Date; - - @ApiProperty({ description: 'Chat emoji' }) - @IsOptional() - emoji: string; - - @ApiProperty({ description: 'Chat owner' }) - @IsOptional() - owner: string; - - @ApiProperty({ description: 'Chat security question' }) - @IsOptional() - securityQuestion: string; - - @ApiProperty({ description: 'Chat security answer' }) - @IsOptional() - securityAnswer: string; -} diff --git a/backend/libs/shared/common/types/src/lib/create-message.dto.ts b/backend/libs/shared/common/types/src/lib/create-message.dto.ts deleted file mode 100644 index 47a6e233..00000000 --- a/backend/libs/shared/common/types/src/lib/create-message.dto.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IsNotEmpty } from 'class-validator'; -import { Message } from './message.entity'; - -export class CreateMessageDto extends Message { - @IsNotEmpty() - chatId: string; -} diff --git a/backend/libs/shared/common/types/src/lib/identify-message.dto.ts b/backend/libs/shared/common/types/src/lib/identify-message.dto.ts deleted file mode 100644 index 218f0a96..00000000 --- a/backend/libs/shared/common/types/src/lib/identify-message.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -export class IdentifyMessageDto { - chatId: string; - id: number; -} diff --git a/backend/libs/shared/common/types/src/lib/join-room.dto.ts b/backend/libs/shared/common/types/src/lib/join-room.dto.ts deleted file mode 100644 index c7375d4e..00000000 --- a/backend/libs/shared/common/types/src/lib/join-room.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -export class JoinRoomDto { - name: string; - chatId: string; -} diff --git a/backend/libs/shared/common/types/src/lib/message.entity.ts b/backend/libs/shared/common/types/src/lib/message.entity.ts deleted file mode 100644 index d886ee63..00000000 --- a/backend/libs/shared/common/types/src/lib/message.entity.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class Message { - name: string; - text: string; - timestamp: Date; -} diff --git a/backend/libs/shared/common/types/src/lib/typing.dto.ts b/backend/libs/shared/common/types/src/lib/typing.dto.ts deleted file mode 100644 index 8e4c378e..00000000 --- a/backend/libs/shared/common/types/src/lib/typing.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -export class TypingDto { - isTyping: boolean; - chatId: string; -} diff --git a/backend/libs/shared/common/types/src/lib/update-message.dto.ts b/backend/libs/shared/common/types/src/lib/update-message.dto.ts deleted file mode 100644 index 3bdd435d..00000000 --- a/backend/libs/shared/common/types/src/lib/update-message.dto.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { PartialType } from '@nestjs/mapped-types'; -import { IsNotEmpty } from 'class-validator'; -import { CreateMessageDto } from './create-message.dto'; - -export class UpdateMessageDto extends PartialType(CreateMessageDto) { - id: number; - - @IsNotEmpty() - override chatId: string; -} diff --git a/backend/libs/shared/util/src/index.ts b/backend/libs/shared/util/src/index.ts index 91ad008c..93573764 100644 --- a/backend/libs/shared/util/src/index.ts +++ b/backend/libs/shared/util/src/index.ts @@ -1,5 +1,4 @@ export * from './lib/admin-auth.guard'; -export * from './lib/chat.mock'; export * from './lib/global-exception.filter'; export * from './lib/http-response.mock'; export * from './lib/identifier.mock'; diff --git a/backend/libs/shared/util/src/lib/chat.mock.ts b/backend/libs/shared/util/src/lib/chat.mock.ts deleted file mode 100644 index 4561d910..00000000 --- a/backend/libs/shared/util/src/lib/chat.mock.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ChatEntityDto, MessageDto } from '@backend/shared-types'; - -export const mockChatEntityDto = ( - mock?: Partial, -): ChatEntityDto => { - return { - _id: mock?._id || '1', - name: mock?.name || 'name', - messages: mock?.messages || [], - clients: mock?.clients || {}, - created: mock?.created || new Date(), - updated: mock?.updated || new Date(), - emoji: mock?.emoji || 'emoji', - owner: mock?.owner || 'owner', - securityQuestion: mock?.securityQuestion || 'securityQuestion', - securityAnswer: mock?.securityAnswer || 'securityAnswer', - }; -}; - -export const mockMessageDto = (mock: Partial): MessageDto => ({ - name: mock.name || 'mockName', - text: mock.text || 'mockText', - timestamp: mock.timestamp || new Date(), -}); diff --git a/frontend/src/app.html b/frontend/src/app.html index 94615396..fa30c338 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -2,7 +2,7 @@ - + => { - return await fetch(`${getApiURL()}/chats`, { - method: 'GET', - headers: createHeaders(token), - }).then((res) => res.json()); -}; - -export const createChat = async ( - token: string, - name: string, -): Promise => { - return await fetch(`${getApiURL()}/chats`, { - method: 'POST', - headers: createHeaders(token), - body: JSON.stringify({ name }), - }).then((res) => res.json()); -}; - -export const getChat = async (token: string, id: string): Promise => { - return await fetch(`${getApiURL()}/chats/${id}`, { - method: 'GET', - headers: createHeaders(token), - }).then((res) => res.json()); -}; - -export const updateChat = async ( - token: string, - id: string, - name: string, -): Promise => { - return await fetch(`${getApiURL()}/chats/${id}`, { - method: 'PUT', - headers: createHeaders(token), - body: JSON.stringify({ name }), - }).then((res) => res.json()); -}; - -export const deleteChat = async ( - token: string, - id: string, -): Promise => { - return await fetch(`${getApiURL()}/chats/${id}`, { - method: 'DELETE', - headers: createHeaders(token), - }).then((res) => res.json()); -}; diff --git a/frontend/src/lib/components/about/AboutInfo.svelte b/frontend/src/lib/components/about/AboutInfo.svelte index 4b4c403e..77e50013 100644 --- a/frontend/src/lib/components/about/AboutInfo.svelte +++ b/frontend/src/lib/components/about/AboutInfo.svelte @@ -83,6 +83,7 @@ console.log(f.reduce((a, c) => a + (c.size <= 100000 ? c.size : 0), 0)); max-width: 80vw; margin: 0 auto; height: 88vh; + height: 88dvh; overflow-y: auto; overflow-x: hidden; } diff --git a/frontend/src/lib/components/admin/Dashboard.svelte b/frontend/src/lib/components/admin/Dashboard.svelte index 8e0c6423..cf9587f6 100644 --- a/frontend/src/lib/components/admin/Dashboard.svelte +++ b/frontend/src/lib/components/admin/Dashboard.svelte @@ -12,7 +12,6 @@ presetFolderAmount: 0, presetLinksAmount: 0, jokesAmount: 0, - chatsAmount: 0, apiIsHealthy: false, jokesIsHealthy: false, mongoIsHealthy: false, @@ -85,11 +84,6 @@ description={$t('page.admin.dashboard.duplicateJokesDescription')} amount={metrics.duplicateJokesAmount} /> - {:else} diff --git a/frontend/src/lib/components/admin/DashboardCard.svelte b/frontend/src/lib/components/admin/DashboardCard.svelte index 4da28e0a..f173f19a 100644 --- a/frontend/src/lib/components/admin/DashboardCard.svelte +++ b/frontend/src/lib/components/admin/DashboardCard.svelte @@ -26,6 +26,8 @@ diff --git a/frontend/src/lib/components/home/SearchBar.svelte b/frontend/src/lib/components/home/SearchBar.svelte index a8e2281b..2911ea03 100644 --- a/frontend/src/lib/components/home/SearchBar.svelte +++ b/frontend/src/lib/components/home/SearchBar.svelte @@ -46,8 +46,9 @@ margin-top: 2rem; } - @media #{$tablet} { - margin-top: 8rem; + @media #{$phone} { + margin-top: 2rem; + width: 95vw; } } :global(.global_search_form) { diff --git a/frontend/src/lib/components/jokes/Jokes.svelte b/frontend/src/lib/components/jokes/Jokes.svelte index 60005916..b1f1f2b0 100644 --- a/frontend/src/lib/components/jokes/Jokes.svelte +++ b/frontend/src/lib/components/jokes/Jokes.svelte @@ -141,6 +141,7 @@ max-width: 90vw; margin: 0 auto 3rem auto; height: 85vh; + height: 85dvh; overflow-y: auto; overflow-x: hidden; diff --git a/frontend/src/lib/components/memorandum/Folder.svelte b/frontend/src/lib/components/memorandum/Folder.svelte index 0d930976..b2646bde 100644 --- a/frontend/src/lib/components/memorandum/Folder.svelte +++ b/frontend/src/lib/components/memorandum/Folder.svelte @@ -11,6 +11,8 @@ import ContextMenu from 'carbon-components-svelte/src/ContextMenu/ContextMenu.svelte'; import ContextMenuDivider from 'carbon-components-svelte/src/ContextMenu/ContextMenuDivider.svelte'; import ContextMenuOption from 'carbon-components-svelte/src/ContextMenu/ContextMenuOption.svelte'; + import OverflowMenu from 'carbon-components-svelte/src/OverflowMenu/OverflowMenu.svelte'; + import OverflowMenuItem from 'carbon-components-svelte/src/OverflowMenu/OverflowMenuItem.svelte'; import Add from 'carbon-icons-svelte/lib/Add.svelte'; import ChevronUp from 'carbon-icons-svelte/lib/ChevronUp.svelte'; import ChevronDown from 'carbon-icons-svelte/lib/ChevronDown.svelte'; @@ -188,6 +190,53 @@ > +
e.stopPropagation()} role="presentation"> + + + + + + {#if $localPresetStore.Folders.length > 1} + {#if currentFolderIndex > 0} + + + {/if} + {#if currentFolderIndex < $localPresetStore.Folders.length - 1} + + + {/if} + {/if} + onDeleteFolder(folderId)} + /> + +
@@ -358,7 +407,15 @@ transition: opacity 0.2s; @media #{$phone}, #{$tablet} { - display: none; + opacity: 1; + } + } + + .mobile_overflow { + display: none; + + @media #{$phone}, #{$tablet} { + display: block; } } @@ -378,6 +435,10 @@ border-radius: 4px; transition: background-color 0.2s; + @media #{$phone}, #{$tablet} { + display: none; + } + &:hover:not(:disabled) { background-color: var(--white30); } diff --git a/frontend/src/lib/components/memorandum/FolderArea.svelte b/frontend/src/lib/components/memorandum/FolderArea.svelte index 0fd08cbf..19d6407b 100644 --- a/frontend/src/lib/components/memorandum/FolderArea.svelte +++ b/frontend/src/lib/components/memorandum/FolderArea.svelte @@ -103,6 +103,17 @@ {:else} {/if} + + {#if value.Folders.length > 0} + +
e.stopPropagation()} role="presentation"> + + window.open(linkUrl, '_blank')} + /> + + onEditLink(linkId, linkName, linkUrl)} + /> + + {#if currentFolder && currentFolder.links.length > 1} + {#if currentLinkIndex > 0} + + + {/if} + {#if currentLinkIndex < currentFolder.links.length - 1} + + + {/if} + {/if} + onDeleteLink(linkId)} + /> + +
@@ -242,7 +291,15 @@ transition: opacity 0.2s; @media #{$phone}, #{$tablet} { - display: none; + opacity: 1; + } + } + + .mobile_overflow { + display: none; + + @media #{$phone}, #{$tablet} { + display: block; } } @@ -262,6 +319,10 @@ border-radius: 4px; transition: background-color 0.2s; + @media #{$phone}, #{$tablet} { + display: none; + } + &:hover:not(:disabled) { background-color: var(--white30); } diff --git a/frontend/src/lib/components/memorandum/Startup.svelte b/frontend/src/lib/components/memorandum/Startup.svelte index ee8e9eae..dfa1ac9e 100644 --- a/frontend/src/lib/components/memorandum/Startup.svelte +++ b/frontend/src/lib/components/memorandum/Startup.svelte @@ -1,32 +1,38 @@ - {#if $initialized}
- +

{$t('page.memorandum.startup.createEmptyFolderDescription')}

+ + - +

{$t('page.memorandum.startup.loadPresetDescription')}

+ +
{:else}
Locale initializing...
@@ -37,51 +43,62 @@ section { display: flex; - align-items: center; + gap: 1.5rem; + padding: 2rem; + justify-content: center; + align-items: stretch; + + @media #{$tablet} { + gap: 1rem; + padding: 1.5rem; + } @media #{$phone} { flex-direction: column; + align-items: center; + padding: 1rem; } } - button { + :global(.startup_card) { display: flex; flex-direction: column; align-items: center; - justify-content: center; - cursor: pointer; - margin: 0; - flex-grow: 2; - padding: var(--default_padding); - border: none; - background-color: var(--trans); - color: white; - font-size: 30px; - display: inline-block; - transition: all 0.3s ease 0s; + gap: 0.75rem; + width: 300px; + text-align: center; + padding: 2rem !important; - &:hover { - background-color: var(--darkgrey80); + @media #{$tablet} { + width: 240px; } - img { - width: 350px; + @media #{$phone} { + width: 100%; + max-width: 400px; } + } - @media #{$tablet} { - p { - font-size: 22px; - } + img { + width: 80px; + opacity: 0.85; - img { - width: 200px; - } + @media #{$tablet} { + width: 60px; } + } - @media #{$phone} { - img { - width: 100px; - } - } + h3 { + margin: 0; + font-size: 1.1rem; + font-weight: 600; + } + + p { + margin: 0; + font-size: 0.875rem; + opacity: 0.75; + line-height: 1.5; + flex: 1; } diff --git a/frontend/src/lib/components/settings/OnlinePersistenceCheck.svelte b/frontend/src/lib/components/settings/OnlinePersistenceCheck.svelte index 8067e7d1..4912e0a8 100644 --- a/frontend/src/lib/components/settings/OnlinePersistenceCheck.svelte +++ b/frontend/src/lib/components/settings/OnlinePersistenceCheck.svelte @@ -295,6 +295,7 @@ :global(.connection_card) { width: 500px; + max-width: 100%; } .online_persistence_info_text { @@ -306,9 +307,19 @@ display: flex; flex-direction: row; justify-content: space-around; - align-items: center; + align-items: flex-end; margin-top: 1rem; gap: 1rem; + + @media only screen and (max-width: 767px) { + flex-wrap: wrap; + } + } + + .button_group :global(.bx--form-item) { + @media only screen and (max-width: 767px) { + flex: 1 1 100%; + } } :global(.modal_notification) { diff --git a/frontend/src/lib/components/shared/GlobalMenu.svelte b/frontend/src/lib/components/shared/GlobalMenu.svelte index 0a4b57d4..0ff6ad43 100644 --- a/frontend/src/lib/components/shared/GlobalMenu.svelte +++ b/frontend/src/lib/components/shared/GlobalMenu.svelte @@ -125,6 +125,8 @@ margin: 0; margin-left: auto; transform: translateX(100%); + overflow-y: auto; + padding-bottom: calc(6rem + env(safe-area-inset-bottom, 0px)); --duration: 500ms; transition: transform var(--duration), @@ -216,7 +218,7 @@ footer { position: fixed; - bottom: 2rem; + bottom: calc(2rem + env(safe-area-inset-bottom, 0px)); left: 0; width: 100%; text-align: center; @@ -227,11 +229,20 @@ @media #{$phone} { font-size: 0.8rem; + flex-wrap: wrap; + gap: 0.25rem; } p { margin: 0 2rem 0 2rem; + + @media #{$phone} { + margin: 0; + width: 100%; + order: -1; + } } + img { width: 2em; height: 2em; diff --git a/frontend/src/lib/components/shared/Header.svelte b/frontend/src/lib/components/shared/Header.svelte index f3de0e43..006c65be 100644 --- a/frontend/src/lib/components/shared/Header.svelte +++ b/frontend/src/lib/components/shared/Header.svelte @@ -61,6 +61,8 @@ diff --git a/frontend/src/lib/components/todo/TodoInput.svelte b/frontend/src/lib/components/todo/TodoInput.svelte index 9c0030df..c893a3b0 100644 --- a/frontend/src/lib/components/todo/TodoInput.svelte +++ b/frontend/src/lib/components/todo/TodoInput.svelte @@ -161,17 +161,29 @@ display: flex; gap: 0.5rem; align-items: flex-end; + + @media #{$phone} { + flex-wrap: wrap; + } } .title-input-wrapper { flex: 1; min-width: 0; + + @media #{$phone} { + flex: 1 1 100%; + } } .category-input-wrapper { position: relative; flex: 1; min-width: 0; + + @media #{$phone} { + flex: 1 1 100%; + } } .autocomplete-hint { @@ -188,6 +200,10 @@ white-space: nowrap; z-index: 10; pointer-events: none; + + @media #{$phone} { + display: none; + } } .autocomplete-hint kbd { diff --git a/frontend/src/lib/components/todo/TodoList.svelte b/frontend/src/lib/components/todo/TodoList.svelte index 5f9bec0f..b4848d6f 100644 --- a/frontend/src/lib/components/todo/TodoList.svelte +++ b/frontend/src/lib/components/todo/TodoList.svelte @@ -675,6 +675,8 @@ display: flex; align-items: center; justify-content: center; + min-height: 44px; + min-width: 44px; } } @@ -701,6 +703,11 @@ background-color 0.2s ease, color 0.2s ease; + @media #{$phone} { + min-height: 44px; + min-width: 44px; + } + &:hover { background: rgba(255, 255, 255, 0.08); color: rgba(255, 255, 255, 0.8); diff --git a/frontend/src/lib/config/applications.ts b/frontend/src/lib/config/applications.ts index 299965c8..e1f5405e 100644 --- a/frontend/src/lib/config/applications.ts +++ b/frontend/src/lib/config/applications.ts @@ -3,7 +3,6 @@ import { TogglesEnum } from '$lib/types/toggle.dto'; import { getToggleValue } from '$lib/util/toggle'; import Bookmark from 'carbon-icons-svelte/lib/Bookmark.svelte'; import Bullhorn from 'carbon-icons-svelte/lib/Bullhorn.svelte'; -import Chat from 'carbon-icons-svelte/lib/Chat.svelte'; import Cognitive from 'carbon-icons-svelte/lib/Cognitive.svelte'; import FingerprintRecognition from 'carbon-icons-svelte/lib/FingerprintRecognition.svelte'; import GameConsole from 'carbon-icons-svelte/lib/GameConsole.svelte'; @@ -22,7 +21,6 @@ export type ApplicationRoutes = { jokes: Route; todo: Route; 'food-scan': Route; - chat: Route; 'catch-em-all': Route; 'uno-sort': Route; about: Route; @@ -34,7 +32,6 @@ const loadApplications = async (): Promise => { jokesToggle, todoToggle, foodScanToggle, - chatToggle, catchEmAllToggle, unoSortToggle, aboutToggle, @@ -43,7 +40,6 @@ const loadApplications = async (): Promise => { getToggleValue(TogglesEnum.jokes), getToggleValue(TogglesEnum.todo), getToggleValue(TogglesEnum.foodScan), - getToggleValue(TogglesEnum.chat), getToggleValue(TogglesEnum.catchEmAll), getToggleValue(TogglesEnum.unoSort), getToggleValue(TogglesEnum.about), @@ -55,7 +51,6 @@ const loadApplications = async (): Promise => { jokes: { id: 'jokes', name: { en: 'Jokes', de: 'Witze' }, path: '/jokes', icon: Cognitive, toggle: jokesToggle }, todo: { id: 'todo', name: { en: 'Lists', de: 'Listen' }, path: '/todo', icon: ListBoxes, toggle: todoToggle }, 'food-scan': { id: 'food-scan', name: { en: 'Food Scan', de: 'Essen-Scan' }, path: '/food-scan', icon: Restaurant, toggle: foodScanToggle }, - chat: { id: 'chat', name: { en: 'Chat', de: 'Klönschnack' }, path: '/chat', icon: Chat, toggle: chatToggle }, 'catch-em-all': { id: 'catch-em-all', name: { en: 'Catch-em-all', de: 'Catch-em-all' }, path: '/catch-em-all', icon: GameConsole, toggle: catchEmAllToggle }, 'uno-sort': { id: 'uno-sort', name: { en: 'Uno sort', de: 'Uno Sortierung' }, path: '/uno-sort', icon: GameConsole, toggle: unoSortToggle }, about: { id: 'about', name: { en: 'About me', de: 'Über mich' }, path: '/about', icon: Identification, toggle: aboutToggle }, @@ -68,7 +63,6 @@ const defaultApplicationRoutes: ApplicationRoutes = { jokes: { id: 'jokes', name: { en: 'Jokes', de: 'Witze' }, path: '/jokes', icon: Cognitive, toggle: false }, todo: { id: 'todo', name: { en: 'Lists', de: 'Listen' }, path: '/todo', icon: ListBoxes, toggle: false }, 'food-scan': { id: 'food-scan', name: { en: 'Food Scan', de: 'Essen-Scan' }, path: '/food-scan', icon: Restaurant, toggle: false }, - chat: { id: 'chat', name: { en: 'Chat', de: 'Klönschnack' }, path: '/chat', icon: Chat, toggle: false }, 'catch-em-all': { id: 'catch-em-all', name: { en: 'Catch-em-all', de: 'Catch-em-all' }, path: '/catch-em-all', icon: GameConsole, toggle: false }, 'uno-sort': { id: 'uno-sort', name: { en: 'Uno sort', de: 'Uno Sortierung' }, path: '/uno-sort', icon: GameConsole, toggle: false }, about: { id: 'about', name: { en: 'About me', de: 'Über mich' }, path: '/about', icon: Identification, toggle: false }, diff --git a/frontend/src/lib/config/de.json b/frontend/src/lib/config/de.json index e43c5edd..556d9dda 100644 --- a/frontend/src/lib/config/de.json +++ b/frontend/src/lib/config/de.json @@ -21,6 +21,8 @@ "page.memorandum.folder.editTitle": "Ordner bearbeiten", "page.memorandum.folder.nameOfFolderHelptext": "Name des Ordners", "page.memorandum.folder.setBackgroundColorQuestion": "Hintergrundfarbe des Ordners ändern?", + "page.memorandum.folder.colorPreviewLabel": "Farbvorschau", + "page.memorandum.folder.colorPreviewDescription": "Die Farbe aktualisiert sich live, sobald du die Regler anpasst.", "page.memorandum.folder.currentValues": "Aktuelle Werte", "page.memorandum.folder.newValues": "Neue Werte", "page.memorandum.folder.copyColorValues": "Farbwerte kopieren.", @@ -63,10 +65,14 @@ "page.memorandum.presetOverlay.validJson": "Gültiges JSON", "page.memorandum.presetOverlay.changesApplied": "Änderungen erfolgreich übernommen!", "page.memorandum.presetOverlay.editPlaceholder": "Bearbeiten Sie hier Ihre Voreinstellung...", - "page.memorandum.startup.createEmptyFolder": "Erstelle einen leeren Ordner", + "page.memorandum.startup.createEmptyFolder": "Leerer Ordner", + "page.memorandum.startup.createEmptyFolderDescription": "Starte mit einer leeren Sammlung und füge deine eigenen Links und Ordner hinzu.", "page.memorandum.startup.createEmptyFolderAlt": "Klicke um einen leeren Ordner zu erstellen", - "page.memorandum.startup.loadPreset": "Lade eine Voreinstellung", + "page.memorandum.startup.createEmptyFolderCta": "Ordner erstellen", + "page.memorandum.startup.loadPreset": "Voreinstellung laden", + "page.memorandum.startup.loadPresetDescription": "Lade eine vorgefertigte Sammlung mit nützlichen Links als Startpunkt.", "page.memorandum.startup.loadPresetAlt": "Klicke um einen Ordner mit vorgefertigten Links zu erstellen", + "page.memorandum.startup.loadPresetCta": "Preset laden", "page.memorandum.searchPlaceholder": "Durchsuche Links", "page.settings.identifiers.title": "Deine Informationen", "page.settings.identifiers.description1": "Hier findest du deine persönliche ID. Um auf deine Zwischenstände von anderen Geräten zugreifen zu können, musst du diese ID kopieren und auf dem anderen Gerät eintragen.", @@ -238,7 +244,6 @@ "page.admin.dashboard.duplicateLinksDescription": "Links mit gleichem Namen", "page.admin.dashboard.jokesDescription": "Anzahl Witze in DB", "page.admin.dashboard.duplicateJokesDescription": "Witze mit gleichem Inhalt", - "page.admin.dashboard.chatsDescription": "Anzahl von Chats in DB", "page.admin.toggles.title": "Schalter", "page.admin.toggles.newToggle": "Neuer Schalter", "page.admin.toggles.addToggle": "Schalter hinzufügen", diff --git a/frontend/src/lib/config/en.json b/frontend/src/lib/config/en.json index ef7af189..18a3e81f 100644 --- a/frontend/src/lib/config/en.json +++ b/frontend/src/lib/config/en.json @@ -21,6 +21,8 @@ "page.memorandum.folder.editTitle": "Edit folder", "page.memorandum.folder.nameOfFolderHelptext": "Name of folder", "page.memorandum.folder.setBackgroundColorQuestion": "Want to change the folder background color?", + "page.memorandum.folder.colorPreviewLabel": "Color preview", + "page.memorandum.folder.colorPreviewDescription": "The color updates live as you adjust the sliders.", "page.memorandum.folder.currentValues": "Current values", "page.memorandum.folder.newValues": "New values", "page.memorandum.folder.copyColorValues": "Copy color values.", @@ -63,10 +65,14 @@ "page.memorandum.presetOverlay.validJson": "Valid JSON", "page.memorandum.presetOverlay.changesApplied": "Changes applied successfully!", "page.memorandum.presetOverlay.editPlaceholder": "Edit your preset configuration here...", - "page.memorandum.startup.createEmptyFolder": "Create an empty folder", + "page.memorandum.startup.createEmptyFolder": "Empty folder", + "page.memorandum.startup.createEmptyFolderDescription": "Start with an empty collection and add your own links and folders.", "page.memorandum.startup.createEmptyFolderAlt": "Click to create an empty folder", - "page.memorandum.startup.loadPreset": "Load a preset", + "page.memorandum.startup.createEmptyFolderCta": "Create folder", + "page.memorandum.startup.loadPreset": "Load preset", + "page.memorandum.startup.loadPresetDescription": "Load a prefabricated collection of useful links as a starting point.", "page.memorandum.startup.loadPresetAlt": "Click to create a folder with prefabricated links", + "page.memorandum.startup.loadPresetCta": "Load preset", "page.memorandum.searchPlaceholder": "Search links", "page.settings.identifiers.title": "Your information", "page.settings.identifiers.description1": "Here you will find your personal ID. To be able to access your interim results from other devices, you must copy this ID and enter it on the other device.", @@ -238,7 +244,6 @@ "page.admin.dashboard.duplicateLinksDescription": "Links with same content", "page.admin.dashboard.jokesDescription": "Displayed daily jokes", "page.admin.dashboard.duplicateJokesDescription": "Jokes with same content", - "page.admin.dashboard.chatsDescription": "Saved chats", "page.admin.toggles.title": "Toggles", "page.admin.toggles.newToggle": "New toggle", "page.admin.toggles.addToggle": "Add toggle", diff --git a/frontend/src/lib/types/admin.dto.ts b/frontend/src/lib/types/admin.dto.ts index af9b9344..365693c5 100644 --- a/frontend/src/lib/types/admin.dto.ts +++ b/frontend/src/lib/types/admin.dto.ts @@ -6,7 +6,6 @@ export enum ActivityTypeDto { IDENTIFIER = 'identifier', PRESET = 'link-preset', JOKE = 'joke', - CHAT = 'chat', } export class ActivityDto { @@ -23,7 +22,6 @@ export class AdminDashboardPropsDto { presetFolderAmount: number; presetLinksAmount: number; jokesAmount: number; - chatsAmount: number; apiIsHealthy: boolean; jokesIsHealthy: boolean; mongoIsHealthy: boolean; diff --git a/frontend/src/lib/types/chat.ts b/frontend/src/lib/types/chat.ts deleted file mode 100644 index ad1fe850..00000000 --- a/frontend/src/lib/types/chat.ts +++ /dev/null @@ -1,14 +0,0 @@ -export type Chat = { - title: string; - done?: boolean; - owner?: string; - securityQuestion?: string; - securityAnswer?: string; -}; - -export type ChatList = { - name: string; - emoji: string; - chats: Chat[]; - history: string[]; -}; diff --git a/frontend/src/lib/types/chats.dto.ts b/frontend/src/lib/types/chats.dto.ts deleted file mode 100644 index 307479ec..00000000 --- a/frontend/src/lib/types/chats.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -export class MessageDto { - name: string; - text: string; - timestamp: string; -} - -export class ChatDto { - _id: string; - name: string; - messages: MessageDto[]; - created: string; - updated: string; -} diff --git a/frontend/src/lib/types/toggle.dto.ts b/frontend/src/lib/types/toggle.dto.ts index c4bc010a..08cfb66f 100644 --- a/frontend/src/lib/types/toggle.dto.ts +++ b/frontend/src/lib/types/toggle.dto.ts @@ -4,7 +4,6 @@ export enum TogglesEnum { todo = 'TOGGLE_NAV_TODO', foodScan = 'TOGGLE_NAV_FOOD_SCAN', jokes = 'TOGGLE_NAV_JOKES', - chat = 'TOGGLE_NAV_CHAT', catchEmAll = 'TOGGLE_NAV_CATCH_EM_ALL', unoSort = 'TOGGLE_NAV_UNO_SORT', about = 'TOGGLE_NAV_ABOUT', diff --git a/frontend/src/lib/util/stores/store-chat.ts b/frontend/src/lib/util/stores/store-chat.ts deleted file mode 100644 index 3d2ef6c0..00000000 --- a/frontend/src/lib/util/stores/store-chat.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { browser } from '$app/environment'; -import type { ChatList } from '$lib/types/chat'; -import { writable } from 'svelte/store'; - -const localStorageKey = 'chats'; -const defaultValue: ChatList[] = []; - -const getInitialValue = () => { - if (browser) { - return JSON.parse(localStorage.getItem(localStorageKey)) || defaultValue; - } else { - return defaultValue; - } -}; - -export const chatStore = writable(getInitialValue()); - -if (browser) { - chatStore.subscribe((value: ChatList[]) => - localStorage.setItem(localStorageKey, JSON.stringify(value)), - ); -} diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 237dfe44..48113548 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -62,13 +62,14 @@ display: flex; flex-direction: column; height: 100vh; + height: 100dvh; } main { display: flex; flex-direction: column; padding: 1rem; - width: 100vw; + width: 100%; overflow: auto; flex: 1; margin: 0 auto; @@ -101,6 +102,7 @@ align-items: center; justify-content: center; height: 80vh; + height: 80dvh; } .confetti_container { diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index bba70cde..320a6dec 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,19 +1,13 @@ - - - {chatRoute.name[locale]} - - - - - - diff --git a/frontend/src/routes/chat/+page.ts b/frontend/src/routes/chat/+page.ts deleted file mode 100644 index a72419a6..00000000 --- a/frontend/src/routes/chat/+page.ts +++ /dev/null @@ -1,3 +0,0 @@ -// since there's no dynamic data here, we can prerender -// it so that it gets served as a static asset in production -export const prerender = true; diff --git a/frontend/src/routes/memorandum/+page.svelte b/frontend/src/routes/memorandum/+page.svelte index 1b2660fe..5d043090 100644 --- a/frontend/src/routes/memorandum/+page.svelte +++ b/frontend/src/routes/memorandum/+page.svelte @@ -152,6 +152,10 @@ justify-content: start; padding-left: calc(var(--default_padding) / 2); gap: calc(var(--default_padding) / 2); + + @media #{$phone} { + flex-wrap: wrap; + } } .order_buttons { @@ -166,6 +170,10 @@ :global(.memorandum_search_bar) { max-width: 33vw; + + @media #{$phone} { + max-width: 100%; + } } .info_buttons { diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index d0cdc3bf..41ee2daa 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -42,6 +42,7 @@ section { margin: 0; height: 88vh; + height: 88dvh; overflow-y: auto; overflow-x: hidden; } diff --git a/frontend/src/routes/styles.css b/frontend/src/routes/styles.css index 7ce05ded..daaf2bd6 100644 --- a/frontend/src/routes/styles.css +++ b/frontend/src/routes/styles.css @@ -26,6 +26,7 @@ body { .background_gradient_dark { background-size: 100vw 100vh !important; + background-size: 100vw 100dvh !important; background-image: linear-gradient( 180deg, var(--color_bg_0) 0%, @@ -35,6 +36,7 @@ body { } .background_gradient_light { background-size: 100vw 100vh !important; + background-size: 100vw 100dvh !important; background-image: linear-gradient( 180deg, var(--color_bg_light_0) 0%, @@ -179,6 +181,7 @@ button:focus:not(:focus-visible) { row-gap: 0.25rem; align-items: stretch; justify-content: center; + height: auto; } .bx--content-switcher-btn { max-width: 225px; @@ -187,6 +190,18 @@ button:focus:not(:focus-visible) { .bx--content-switcher-btn::before { display: none; } +@media only screen and (max-width: 767px) { + .bx--content-switcher-btn { + flex: 0 0 44px !important; + width: 44px !important; + min-width: unset !important; + min-height: 44px; + padding: 0 !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + } +} /* Carbon Accordion Overrides - Admin Lists*/ .admin_list_item_headline { @@ -202,7 +217,7 @@ button:focus:not(:focus-visible) { justify-content: space-between; padding-left: calc(var(--default_padding) / 2); padding-right: calc(var(--default_padding) / 2); - width: calc(100vw - var(--default_padding) * 2); + width: 100%; } .admin_list_button_group { display: flex; diff --git a/frontend/src/routes/todo/+page.svelte b/frontend/src/routes/todo/+page.svelte index 96389178..c80c9c7a 100644 --- a/frontend/src/routes/todo/+page.svelte +++ b/frontend/src/routes/todo/+page.svelte @@ -384,6 +384,11 @@ gap: 1rem; justify-content: center; align-items: center; + + @media only screen and (max-width: 767px) { + flex-wrap: wrap; + gap: 0.5rem; + } } .app_content { @@ -403,7 +408,7 @@ :global(#list_menu_button) { position: fixed; right: 1.5rem; - bottom: 1.5rem; + bottom: calc(1.5rem + env(safe-area-inset-bottom, 0px)); z-index: 100; } diff --git a/frontend/src/routes/uno-sort/+page.svelte b/frontend/src/routes/uno-sort/+page.svelte index 146ad656..6986363c 100644 --- a/frontend/src/routes/uno-sort/+page.svelte +++ b/frontend/src/routes/uno-sort/+page.svelte @@ -7,6 +7,7 @@ import { initialized, setLocale, t } from '$lib/util/translations'; import Button from 'carbon-components-svelte/src/Button/Button.svelte'; import NumberInput from 'carbon-components-svelte/src/NumberInput/NumberInput.svelte'; + import Reset from 'carbon-icons-svelte/lib/Reset.svelte'; import { onMount } from 'svelte'; // 4. STATE @@ -79,8 +80,8 @@ - @@ -156,7 +157,6 @@ color: yellow; font-size: 6em; transition: 0.5s; - text-shadow: 0 0 15px black; transform-origin: bottom; transform: rotate(-25deg); text-shadow: @@ -271,15 +271,18 @@ overflow-y: auto; display: flex; height: 53vh; + height: 53dvh; gap: 2rem; @media #{$tablet} { height: 45vh; + height: 45dvh; } @media #{$phone} { flex-direction: column; height: 40vh; + height: 40dvh; } } @@ -287,4 +290,23 @@ font-size: 0.5em; font-weight: normal; } + + @media #{$phone} { + :global(.reset_button) { + padding: 0.875rem !important; + min-width: unset !important; + width: 3rem !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + } + + :global(.reset_button .reset_label) { + display: none; + } + + :global(.reset_button svg) { + margin: 0 !important; + } + }