From 55f1fe572f7eabb9bb3c6662005b634212bd7896 Mon Sep 17 00:00:00 2001 From: timlohse1104 Date: Mon, 9 Mar 2026 14:20:34 +0100 Subject: [PATCH 01/12] Remove chat application and add mobile optimizations - Remove chat application entirely from frontend and backend - Add mobile-first responsive improvements across all main pages - Fix GlobalMenu footer layout on phone (social buttons below text) - Redesign home page: icon-only navigation on phone, prominent JokeOfTheDay card - Redesign memorandum empty state with Carbon Tile cards and CTA buttons - FAB: primary variant, hidden in empty state, respects safe-area-inset - Add viewport-fit=cover for iPhone notch/home-indicator support - Fix duplicate text-shadow in uno-sort h1 - Reset button in uno-sort: icon-only on phone, icon+text on larger screens Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 35 ++ backend/apps/tilloh-dev/src/main.ts | 2 - backend/libs/chat/.eslintrc.json | 18 - backend/libs/chat/README.md | 7 - backend/libs/chat/jest.config.ts | 11 - backend/libs/chat/project.json | 24 -- backend/libs/chat/src/index.ts | 2 - .../libs/chat/src/lib/chat.controller.spec.ts | 123 ------ backend/libs/chat/src/lib/chat.controller.ts | 107 ------ .../libs/chat/src/lib/chat.gateway.spec.ts | 309 ---------------- backend/libs/chat/src/lib/chat.gateway.ts | 98 ----- backend/libs/chat/src/lib/chat.module.ts | 18 - .../chat/src/lib/chat.mongodb.service.spec.ts | 234 ------------ .../libs/chat/src/lib/chat.mongodb.service.ts | 126 ------- .../libs/chat/src/lib/chat.service.spec.ts | 168 --------- backend/libs/chat/src/lib/chat.service.ts | 87 ----- .../chat/src/lib/messages.service.spec.ts | 350 ------------------ backend/libs/chat/src/lib/messages.service.ts | 147 -------- .../libs/chat/src/lib/schema/chat.schema.ts | 54 --- backend/libs/chat/tsconfig.json | 22 -- backend/libs/chat/tsconfig.lib.json | 15 - backend/libs/chat/tsconfig.spec.json | 14 - backend/libs/shared/common/texts/src/index.ts | 1 - .../shared/common/texts/src/lib/chat-texts.ts | 22 -- backend/libs/shared/common/types/src/index.ts | 8 - .../shared/common/types/src/lib/chat.dto.ts | 88 ----- .../common/types/src/lib/chat.entity.ts | 45 --- .../types/src/lib/create-message.dto.ts | 7 - .../types/src/lib/identify-message.dto.ts | 4 - .../common/types/src/lib/join-room.dto.ts | 4 - .../common/types/src/lib/message.entity.ts | 5 - .../shared/common/types/src/lib/typing.dto.ts | 4 - .../types/src/lib/update-message.dto.ts | 10 - backend/libs/shared/util/src/index.ts | 1 - backend/libs/shared/util/src/lib/chat.mock.ts | 24 -- frontend/src/app.html | 2 +- frontend/src/lib/api/chats.api.ts | 51 --- .../src/lib/components/admin/Dashboard.svelte | 6 - .../lib/components/admin/DashboardCard.svelte | 6 + .../lib/components/home/JokeOfTheDay.svelte | 25 +- .../src/lib/components/home/SearchBar.svelte | 5 +- .../lib/components/memorandum/Folder.svelte | 2 +- .../components/memorandum/FolderArea.svelte | 22 +- .../src/lib/components/memorandum/Link.svelte | 7 +- .../lib/components/memorandum/Startup.svelte | 101 ++--- .../settings/OnlinePersistenceCheck.svelte | 1 + .../lib/components/shared/GlobalMenu.svelte | 13 +- .../src/lib/components/shared/Header.svelte | 6 + .../lib/components/shared/Navigation.svelte | 7 +- .../components/todo/GenericListOverlay.svelte | 5 + frontend/src/lib/components/todo/Todo.svelte | 8 + frontend/src/lib/config/applications.ts | 6 - frontend/src/lib/config/de.json | 9 +- frontend/src/lib/config/en.json | 9 +- frontend/src/lib/types/admin.dto.ts | 2 - frontend/src/lib/types/chat.ts | 14 - frontend/src/lib/types/chats.dto.ts | 13 - frontend/src/lib/types/toggle.dto.ts | 1 - frontend/src/lib/util/stores/store-chat.ts | 22 -- frontend/src/routes/+layout.svelte | 3 +- frontend/src/routes/+page.svelte | 28 +- frontend/src/routes/admin/+layout.svelte | 15 - frontend/src/routes/chat/+page.svelte | 24 -- frontend/src/routes/chat/+page.ts | 3 - frontend/src/routes/memorandum/+page.svelte | 8 + frontend/src/routes/settings/+page.svelte | 1 + frontend/src/routes/styles.css | 13 + frontend/src/routes/todo/+page.svelte | 2 +- frontend/src/routes/uno-sort/+page.svelte | 25 +- 69 files changed, 249 insertions(+), 2410 deletions(-) delete mode 100644 backend/libs/chat/.eslintrc.json delete mode 100644 backend/libs/chat/README.md delete mode 100644 backend/libs/chat/jest.config.ts delete mode 100644 backend/libs/chat/project.json delete mode 100644 backend/libs/chat/src/index.ts delete mode 100644 backend/libs/chat/src/lib/chat.controller.spec.ts delete mode 100644 backend/libs/chat/src/lib/chat.controller.ts delete mode 100644 backend/libs/chat/src/lib/chat.gateway.spec.ts delete mode 100644 backend/libs/chat/src/lib/chat.gateway.ts delete mode 100644 backend/libs/chat/src/lib/chat.module.ts delete mode 100644 backend/libs/chat/src/lib/chat.mongodb.service.spec.ts delete mode 100644 backend/libs/chat/src/lib/chat.mongodb.service.ts delete mode 100644 backend/libs/chat/src/lib/chat.service.spec.ts delete mode 100644 backend/libs/chat/src/lib/chat.service.ts delete mode 100644 backend/libs/chat/src/lib/messages.service.spec.ts delete mode 100644 backend/libs/chat/src/lib/messages.service.ts delete mode 100644 backend/libs/chat/src/lib/schema/chat.schema.ts delete mode 100644 backend/libs/chat/tsconfig.json delete mode 100644 backend/libs/chat/tsconfig.lib.json delete mode 100644 backend/libs/chat/tsconfig.spec.json delete mode 100644 backend/libs/shared/common/texts/src/lib/chat-texts.ts delete mode 100644 backend/libs/shared/common/types/src/lib/chat.dto.ts delete mode 100644 backend/libs/shared/common/types/src/lib/chat.entity.ts delete mode 100644 backend/libs/shared/common/types/src/lib/create-message.dto.ts delete mode 100644 backend/libs/shared/common/types/src/lib/identify-message.dto.ts delete mode 100644 backend/libs/shared/common/types/src/lib/join-room.dto.ts delete mode 100644 backend/libs/shared/common/types/src/lib/message.entity.ts delete mode 100644 backend/libs/shared/common/types/src/lib/typing.dto.ts delete mode 100644 backend/libs/shared/common/types/src/lib/update-message.dto.ts delete mode 100644 backend/libs/shared/util/src/lib/chat.mock.ts delete mode 100644 frontend/src/lib/api/chats.api.ts delete mode 100644 frontend/src/lib/types/chat.ts delete mode 100644 frontend/src/lib/types/chats.dto.ts delete mode 100644 frontend/src/lib/util/stores/store-chat.ts delete mode 100644 frontend/src/routes/chat/+page.svelte delete mode 100644 frontend/src/routes/chat/+page.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index fe1ff848..670bb85f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,41 @@ 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. + +### 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/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/memorandum/Folder.svelte b/frontend/src/lib/components/memorandum/Folder.svelte index 0d930976..7bc6773f 100644 --- a/frontend/src/lib/components/memorandum/Folder.svelte +++ b/frontend/src/lib/components/memorandum/Folder.svelte @@ -358,7 +358,7 @@ transition: opacity 0.2s; @media #{$phone}, #{$tablet} { - display: none; + opacity: 1; } } 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} + +

{$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..b54880d4 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 { 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/routes/admin/+layout.svelte b/frontend/src/routes/admin/+layout.svelte index 42588e3f..5de70d65 100644 --- a/frontend/src/routes/admin/+layout.svelte +++ b/frontend/src/routes/admin/+layout.svelte @@ -1,7 +1,6 @@ - - - {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..271641dd 100644 --- a/frontend/src/routes/styles.css +++ b/frontend/src/routes/styles.css @@ -179,6 +179,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 +188,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 { diff --git a/frontend/src/routes/todo/+page.svelte b/frontend/src/routes/todo/+page.svelte index 96389178..269ccc6e 100644 --- a/frontend/src/routes/todo/+page.svelte +++ b/frontend/src/routes/todo/+page.svelte @@ -403,7 +403,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..62df2705 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: @@ -287,4 +287,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; + } + } From f6d5c99880cdd93640859312cc0b7beb64e67550 Mon Sep 17 00:00:00 2001 From: timlohse1104 Date: Mon, 9 Mar 2026 14:35:41 +0100 Subject: [PATCH 02/12] Add OverflowMenu alternative for ContextMenu on touch devices Folder.svelte and Link.svelte: Add mobile overflow menu (OverflowMenu) visible on phone/tablet with all context menu actions. Desktop keeps right-click ContextMenu unchanged. Also add min-height: 44px for arrow_button on $phone. Todo.svelte: Add mobile-only edit button (pencil icon) visible on $phone as alternative to right-click ContextMenu. Add min-height: 44px for save/cancel buttons on mobile. Co-Authored-By: Claude Sonnet 4.6 --- .../lib/components/memorandum/Folder.svelte | 62 +++++++++++++++++++ .../src/lib/components/memorandum/Link.svelte | 57 +++++++++++++++++ frontend/src/lib/components/todo/Todo.svelte | 33 ++++++++++ 3 files changed, 152 insertions(+) diff --git a/frontend/src/lib/components/memorandum/Folder.svelte b/frontend/src/lib/components/memorandum/Folder.svelte index 7bc6773f..a8396152 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)} + /> + +
@@ -362,6 +411,14 @@ } } + .mobile_overflow { + display: none; + + @media #{$phone}, #{$tablet} { + display: block; + } + } + .box_header:hover .folder_controls { opacity: 1; } @@ -378,6 +435,11 @@ border-radius: 4px; transition: background-color 0.2s; + @media #{$phone} { + min-height: 44px; + padding: 0.5rem; + } + &:hover:not(:disabled) { background-color: var(--white30); } diff --git a/frontend/src/lib/components/memorandum/Link.svelte b/frontend/src/lib/components/memorandum/Link.svelte index d975c648..21d6fc36 100644 --- a/frontend/src/lib/components/memorandum/Link.svelte +++ b/frontend/src/lib/components/memorandum/Link.svelte @@ -5,6 +5,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 ChevronUp from 'carbon-icons-svelte/lib/ChevronUp.svelte'; import ChevronDown from 'carbon-icons-svelte/lib/ChevronDown.svelte'; import ArrowUp from 'carbon-icons-svelte/lib/ArrowUp.svelte'; @@ -121,6 +123,53 @@ > +
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)} + /> + +
@@ -246,6 +295,14 @@ } } + .mobile_overflow { + display: none; + + @media #{$phone}, #{$tablet} { + display: block; + } + } + section:hover .link_controls { opacity: 1; } diff --git a/frontend/src/lib/components/todo/Todo.svelte b/frontend/src/lib/components/todo/Todo.svelte index 1fbe442a..6f2f7103 100644 --- a/frontend/src/lib/components/todo/Todo.svelte +++ b/frontend/src/lib/components/todo/Todo.svelte @@ -201,6 +201,11 @@ tabindex="-1" style="display: contents;" > + {#if !isRenaming} + + {/if}