From be41962c89ea04e8682b2461512e3693d909587b Mon Sep 17 00:00:00 2001 From: Sonny Date: Wed, 10 Dec 2025 05:12:14 +0100 Subject: [PATCH] refactor: migrate from types to dto --- app/controllers/admin/admin_controller.ts | 22 +------ .../create_collection_controller.ts | 5 +- .../collections/get_collections_controller.ts | 3 +- .../api/links/create_link_controller.ts | 3 +- .../api/links/delete_link_controller.ts | 5 +- .../links/get_favorite_links_controller.ts | 3 +- .../delete_collection_controller.ts | 3 +- .../show_collections_controller.ts | 10 ++- .../update_collection_controller.ts | 3 +- .../links/create_link_controller.ts | 5 +- .../links/delete_link_controller.ts | 5 +- .../links/update_link_controller.ts | 7 +- .../shared_collections_controller.ts | 5 +- .../user/display_preferences_controller.ts | 1 - app/dtos/collection.ts | 47 ++++++++++++++ app/dtos/collection_with_links.ts | 53 +++++++++++++++ app/dtos/common_model.ts | 57 ++++++++++++++++ app/dtos/link.ts | 54 +++++++++++++++ app/dtos/link_with_collection.ts | 60 +++++++++++++++++ app/dtos/shared_collection.ts | 58 +++++++++++++++++ app/dtos/simple_paginator.ts | 65 +++++++++++++++++++ app/dtos/user.ts | 42 ++++++++++++ app/dtos/user_auth.ts | 27 ++++++++ app/dtos/user_with_counters.ts | 58 +++++++++++++++++ app/types/dto.ts | 28 ++++++++ config/inertia.ts | 22 ++++--- .../components/admin/users/users_table.tsx | 27 ++++---- inertia/components/admin/users/utils.ts | 16 ++--- inertia/components/common/user_badge_role.tsx | 4 +- .../collection/item/collection_item.tsx | 2 +- .../dashboard/favorite/item/favorite_item.tsx | 8 ++- .../dashboard/link/item/link_controls.tsx | 4 +- .../dashboard/link/item/link_item.tsx | 4 +- inertia/components/form/form_link.tsx | 2 +- inertia/constants/index.ts | 1 - .../collections/use_active_collection.tsx | 2 +- inertia/hooks/collections/use_collections.tsx | 2 +- .../hooks/collections/use_favorite_links.tsx | 2 +- inertia/hooks/use_auth.tsx | 28 +++----- inertia/lib/favorite.ts | 2 +- inertia/lib/navigation.ts | 2 +- inertia/pages/collections/create.tsx | 8 ++- inertia/pages/collections/delete.tsx | 10 +-- inertia/pages/collections/edit.tsx | 10 +-- inertia/pages/links/create.tsx | 10 +-- inertia/pages/links/delete.tsx | 8 ++- inertia/pages/links/edit.tsx | 13 ++-- inertia/pages/shared.tsx | 4 +- inertia/types/app.ts | 61 ----------------- package.json | 1 + shared/types/dto.ts | 19 ++++++ 51 files changed, 709 insertions(+), 192 deletions(-) create mode 100644 app/dtos/collection.ts create mode 100644 app/dtos/collection_with_links.ts create mode 100644 app/dtos/common_model.ts create mode 100644 app/dtos/link.ts create mode 100644 app/dtos/link_with_collection.ts create mode 100644 app/dtos/shared_collection.ts create mode 100644 app/dtos/simple_paginator.ts create mode 100644 app/dtos/user.ts create mode 100644 app/dtos/user_auth.ts create mode 100644 app/dtos/user_with_counters.ts create mode 100644 app/types/dto.ts create mode 100644 shared/types/dto.ts diff --git a/app/controllers/admin/admin_controller.ts b/app/controllers/admin/admin_controller.ts index 9cd193f..2add7de 100644 --- a/app/controllers/admin/admin_controller.ts +++ b/app/controllers/admin/admin_controller.ts @@ -1,28 +1,10 @@ import AuthController from '#controllers/auth/auth_controller'; import LinksController from '#controllers/links/delete_link_controller'; -import User from '#models/user'; +import { UserWithCountersDto } from '#dtos/user_with_counters'; import { CollectionService } from '#services/collections/collection_service'; import { inject } from '@adonisjs/core'; import { HttpContext } from '@adonisjs/core/http'; -class UserWithRelationCountDto { - constructor(private user: User) {} - - toJson = () => ({ - id: this.user.id, - email: this.user.email, - fullname: this.user.name, - avatarUrl: this.user.avatarUrl, - isAdmin: this.user.isAdmin, - createdAt: this.user.createdAt.toString(), - updatedAt: this.user.updatedAt.toString(), - lastSeenAt: - this.user.lastSeenAt?.toString() ?? this.user.updatedAt.toString(), - linksCount: Number(this.user.$extras.totalLinks), - collectionsCount: Number(this.user.$extras.totalCollections), - }); -} - @inject() export default class AdminController { constructor( @@ -38,7 +20,7 @@ export default class AdminController { await this.collectionService.getTotalCollectionsCount(); return inertia.render('admin/dashboard', { - users: users.map((user) => new UserWithRelationCountDto(user).toJson()), + users: UserWithCountersDto.fromArray(users), totalLinks: linksCount, totalCollections: collectionsCount, }); diff --git a/app/controllers/api/collections/create_collection_controller.ts b/app/controllers/api/collections/create_collection_controller.ts index 78aec1a..a7d1591 100644 --- a/app/controllers/api/collections/create_collection_controller.ts +++ b/app/controllers/api/collections/create_collection_controller.ts @@ -1,3 +1,4 @@ +import { CollectionWithLinksDto } from '#dtos/collection_with_links'; import { CollectionService } from '#services/collections/collection_service'; import { createCollectionValidator } from '#validators/collections/create_collection_validator'; import { inject } from '@adonisjs/core'; @@ -8,17 +9,15 @@ export default class CreateCollectionController { constructor(private collectionService: CollectionService) {} async execute({ request, response }: HttpContext) { - console.log('avant'); const payload = await request.validateUsing(createCollectionValidator); const collection = await this.collectionService.createCollection({ name: payload.name, description: payload.description, visibility: payload.visibility, }); - console.log('après', collection); return response.json({ message: 'Collection created successfully', - collection: collection.serialize(), + collection: new CollectionWithLinksDto(collection).serialize(), }); } } diff --git a/app/controllers/api/collections/get_collections_controller.ts b/app/controllers/api/collections/get_collections_controller.ts index dea091e..b1294ec 100644 --- a/app/controllers/api/collections/get_collections_controller.ts +++ b/app/controllers/api/collections/get_collections_controller.ts @@ -1,3 +1,4 @@ +import { CollectionWithLinksDto } from '#dtos/collection_with_links'; import { CollectionService } from '#services/collections/collection_service'; import { inject } from '@adonisjs/core'; import { HttpContext } from '@adonisjs/core/http'; @@ -10,7 +11,7 @@ export default class GetCollectionsController { const collections = await this.collectionService.getCollectionsForAuthenticatedUser(); return response.json({ - collections: collections.map((collection) => collection.serialize()), + collections: CollectionWithLinksDto.fromArray(collections), }); } } diff --git a/app/controllers/api/links/create_link_controller.ts b/app/controllers/api/links/create_link_controller.ts index 5b0c554..1f3d01d 100644 --- a/app/controllers/api/links/create_link_controller.ts +++ b/app/controllers/api/links/create_link_controller.ts @@ -1,3 +1,4 @@ +import { LinkDto } from '#dtos/link'; import { LinkService } from '#services/links/link_service'; import { createLinkValidator } from '#validators/links/create_link_validator'; import { inject } from '@adonisjs/core'; @@ -17,7 +18,7 @@ export default class CreateLinkController { }); return response.json({ message: 'Link created successfully', - link: link.serialize(), + link: new LinkDto(link).serialize(), }); } } diff --git a/app/controllers/api/links/delete_link_controller.ts b/app/controllers/api/links/delete_link_controller.ts index 41597dd..b5ccfa6 100644 --- a/app/controllers/api/links/delete_link_controller.ts +++ b/app/controllers/api/links/delete_link_controller.ts @@ -1,3 +1,4 @@ +import { LinkWithCollectionDto } from '#dtos/link_with_collection'; import { CollectionService } from '#services/collections/collection_service'; import { LinkService } from '#services/links/link_service'; import { deleteLinkValidator } from '#validators/links/delete_link_validator'; @@ -20,7 +21,9 @@ export default class DeleteLinkController { const link = await this.linkService.getLinkById(linkId, auth.user!.id); await link.load('collection'); - return inertia.render('links/delete', { link }); + return inertia.render('links/delete', { + link: new LinkWithCollectionDto(link).serialize(), + }); } async execute({ request, auth }: HttpContext) { diff --git a/app/controllers/api/links/get_favorite_links_controller.ts b/app/controllers/api/links/get_favorite_links_controller.ts index 45a7101..69befde 100644 --- a/app/controllers/api/links/get_favorite_links_controller.ts +++ b/app/controllers/api/links/get_favorite_links_controller.ts @@ -1,3 +1,4 @@ +import { LinkDto } from '#dtos/link'; import { LinkService } from '#services/links/link_service'; import { inject } from '@adonisjs/core'; import { HttpContext } from '@adonisjs/core/http'; @@ -8,6 +9,6 @@ export default class GetFavoriteLinksController { public async execute({ response }: HttpContext) { const links = await this.linkService.getFavoriteLinksForAuthenticatedUser(); - return response.json(links); + return response.json(LinkDto.fromArray(links)); } } diff --git a/app/controllers/collections/delete_collection_controller.ts b/app/controllers/collections/delete_collection_controller.ts index 6a49a93..8b10e9f 100644 --- a/app/controllers/collections/delete_collection_controller.ts +++ b/app/controllers/collections/delete_collection_controller.ts @@ -1,3 +1,4 @@ +import { CollectionDto } from '#dtos/collection'; import { CollectionService } from '#services/collections/collection_service'; import { deleteCollectionValidator } from '#validators/collections/delete_collection_validator'; import { inject } from '@adonisjs/core'; @@ -14,7 +15,7 @@ export default class DeleteCollectionController { const collection = await this.collectionService.getCollectionById(collectionId); return inertia.render('collections/delete', { - collection, + collection: new CollectionDto(collection).serialize(), }); } diff --git a/app/controllers/collections/show_collections_controller.ts b/app/controllers/collections/show_collections_controller.ts index c3df473..251b55f 100644 --- a/app/controllers/collections/show_collections_controller.ts +++ b/app/controllers/collections/show_collections_controller.ts @@ -1,3 +1,5 @@ +import { CollectionDto } from '#dtos/collection'; +import { LinkDto } from '#dtos/link'; import { CollectionService } from '#services/collections/collection_service'; import { LinkService } from '#services/links/link_service'; import { inject } from '@adonisjs/core'; @@ -28,9 +30,11 @@ export default class ShowCollectionsController { } return inertia.render('dashboard', { - collections: collections.map((collection) => collection.serialize()), - favoriteLinks: favoriteLinks.map((link) => link.serialize()), - activeCollection: activeCollection?.serialize(), + collections: CollectionDto.fromArray(collections), + favoriteLinks: LinkDto.fromArray(favoriteLinks), + activeCollection: activeCollection + ? new CollectionDto(activeCollection).serialize() + : null, }); } } diff --git a/app/controllers/collections/update_collection_controller.ts b/app/controllers/collections/update_collection_controller.ts index bca1a4d..b0383eb 100644 --- a/app/controllers/collections/update_collection_controller.ts +++ b/app/controllers/collections/update_collection_controller.ts @@ -1,3 +1,4 @@ +import { CollectionDto } from '#dtos/collection'; import { CollectionService } from '#services/collections/collection_service'; import { updateCollectionValidator } from '#validators/collections/update_collection_validator'; import { inject } from '@adonisjs/core'; @@ -14,7 +15,7 @@ export default class UpdateCollectionController { const collection = await this.collectionService.getCollectionById(collectionId); return inertia.render('collections/edit', { - collection: collection.serialize(), + collection: new CollectionDto(collection).serialize(), }); } diff --git a/app/controllers/links/create_link_controller.ts b/app/controllers/links/create_link_controller.ts index e64f36d..4a8198f 100644 --- a/app/controllers/links/create_link_controller.ts +++ b/app/controllers/links/create_link_controller.ts @@ -1,3 +1,4 @@ +import { CollectionDto } from '#dtos/collection'; import { CollectionService } from '#services/collections/collection_service'; import { LinkService } from '#services/links/link_service'; import { createLinkValidator } from '#validators/links/create_link_validator'; @@ -14,7 +15,9 @@ export default class CreateLinkController { async render({ inertia }: HttpContext) { const collections = await this.collectionsService.getCollectionsForAuthenticatedUser(); - return inertia.render('links/create', { collections }); + return inertia.render('links/create', { + collections: CollectionDto.fromArray(collections), + }); } async execute({ request }: HttpContext) { diff --git a/app/controllers/links/delete_link_controller.ts b/app/controllers/links/delete_link_controller.ts index 41597dd..b5ccfa6 100644 --- a/app/controllers/links/delete_link_controller.ts +++ b/app/controllers/links/delete_link_controller.ts @@ -1,3 +1,4 @@ +import { LinkWithCollectionDto } from '#dtos/link_with_collection'; import { CollectionService } from '#services/collections/collection_service'; import { LinkService } from '#services/links/link_service'; import { deleteLinkValidator } from '#validators/links/delete_link_validator'; @@ -20,7 +21,9 @@ export default class DeleteLinkController { const link = await this.linkService.getLinkById(linkId, auth.user!.id); await link.load('collection'); - return inertia.render('links/delete', { link }); + return inertia.render('links/delete', { + link: new LinkWithCollectionDto(link).serialize(), + }); } async execute({ request, auth }: HttpContext) { diff --git a/app/controllers/links/update_link_controller.ts b/app/controllers/links/update_link_controller.ts index 02730f1..ee5a4f0 100644 --- a/app/controllers/links/update_link_controller.ts +++ b/app/controllers/links/update_link_controller.ts @@ -1,3 +1,5 @@ +import { CollectionDto } from '#dtos/collection'; +import { LinkDto } from '#dtos/link'; import { CollectionService } from '#services/collections/collection_service'; import { LinkService } from '#services/links/link_service'; import { updateLinkValidator } from '#validators/links/update_link_validator'; @@ -21,7 +23,10 @@ export default class UpdateLinkController { await this.collectionsService.getCollectionsForAuthenticatedUser(); const link = await this.linkService.getLinkById(linkId, auth.user!.id); - return inertia.render('links/edit', { collections, link }); + return inertia.render('links/edit', { + collections: CollectionDto.fromArray(collections), + link: new LinkDto(link).serialize(), + }); } async execute({ request }: HttpContext) { diff --git a/app/controllers/shared_collections/shared_collections_controller.ts b/app/controllers/shared_collections/shared_collections_controller.ts index 1ec112d..23f9b58 100644 --- a/app/controllers/shared_collections/shared_collections_controller.ts +++ b/app/controllers/shared_collections/shared_collections_controller.ts @@ -1,3 +1,4 @@ +import { SharedCollectionDto } from '#dtos/shared_collection'; import { CollectionService } from '#services/collections/collection_service'; import { getSharedCollectionValidator } from '#validators/shared_collections/shared_collection'; import { inject } from '@adonisjs/core'; @@ -14,6 +15,8 @@ export default class SharedCollectionsController { const activeCollection = await this.collectionService.getPublicCollectionById(params.id); - return inertia.render('shared', { activeCollection }); + return inertia.render('shared', { + activeCollection: new SharedCollectionDto(activeCollection).serialize(), + }); } } diff --git a/app/controllers/user/display_preferences_controller.ts b/app/controllers/user/display_preferences_controller.ts index 8235f79..f9579e0 100644 --- a/app/controllers/user/display_preferences_controller.ts +++ b/app/controllers/user/display_preferences_controller.ts @@ -19,7 +19,6 @@ export default class DisplayPreferencesController { getDisplayPreferences().collectionListDisplay, }; auth.user!.displayPreferences = mergedPrefs; - console.log(auth.user!.displayPreferences); await auth.user!.save(); return response.redirect().withQs().back(); } diff --git a/app/dtos/collection.ts b/app/dtos/collection.ts new file mode 100644 index 0000000..ffcb62a --- /dev/null +++ b/app/dtos/collection.ts @@ -0,0 +1,47 @@ +import { CommonModelDto } from '#dtos/common_model'; +import { Visibility } from '#enums/collections/visibility'; +import Collection from '#models/collection'; + +export class CollectionDto extends CommonModelDto { + declare id: number; + declare name: string; + declare description: string | null; + declare visibility: Visibility; + declare authorId: number; + declare createdAt: string | null; + declare updatedAt: string | null; + + constructor(collection?: Collection) { + if (!collection) return; + super(collection); + + this.id = collection.id; + this.name = collection.name; + this.description = collection.description; + this.visibility = collection.visibility; + this.authorId = collection.authorId; + this.createdAt = collection.createdAt?.toISO(); + this.updatedAt = collection.updatedAt?.toISO(); + } + + serialize(): { + id: number; + name: string; + description: string | null; + visibility: Visibility; + authorId: number; + createdAt: string | null; + updatedAt: string | null; + } { + return { + ...super.serialize(), + id: this.id, + name: this.name, + description: this.description, + visibility: this.visibility, + authorId: this.authorId, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + }; + } +} diff --git a/app/dtos/collection_with_links.ts b/app/dtos/collection_with_links.ts new file mode 100644 index 0000000..c90ce54 --- /dev/null +++ b/app/dtos/collection_with_links.ts @@ -0,0 +1,53 @@ +import { CommonModelDto } from '#dtos/common_model'; +import { LinkDto } from '#dtos/link'; +import { Visibility } from '#enums/collections/visibility'; +import Collection from '#models/collection'; +import { Link } from '#shared/types/dto'; + +export class CollectionWithLinksDto extends CommonModelDto { + declare id: number; + declare name: string; + declare description: string | null; + declare visibility: Visibility; + declare authorId: number; + declare links: LinkDto[]; + declare createdAt: string | null; + declare updatedAt: string | null; + + constructor(collection?: Collection) { + if (!collection) return; + super(collection); + + this.id = collection.id; + this.name = collection.name; + this.description = collection.description; + this.visibility = collection.visibility; + this.authorId = collection.authorId; + this.links = LinkDto.fromArray(collection.links); + this.createdAt = collection.createdAt?.toISO(); + this.updatedAt = collection.updatedAt?.toISO(); + } + + serialize(): { + id: number; + name: string; + description: string | null; + visibility: Visibility; + authorId: number; + links: Link[]; + createdAt: string | null; + updatedAt: string | null; + } { + return { + ...super.serialize(), + id: this.id, + name: this.name, + description: this.description, + visibility: this.visibility, + authorId: this.authorId, + links: this.links.map((link) => link.serialize()), + createdAt: this.createdAt, + updatedAt: this.updatedAt, + }; + } +} diff --git a/app/dtos/common_model.ts b/app/dtos/common_model.ts new file mode 100644 index 0000000..beee65a --- /dev/null +++ b/app/dtos/common_model.ts @@ -0,0 +1,57 @@ +import SimplePaginatorDto from '#dtos/simple_paginator'; +import AppBaseModel from '#models/app_base_model'; +import { SimplePaginatorDtoMetaRange, StaticDto } from '#types/dto'; +import { LucidRow, ModelPaginatorContract } from '@adonisjs/lucid/types/model'; +import { SimplePaginatorContract } from '@adonisjs/lucid/types/querybuilder'; + +export abstract class CommonModelDto { + declare id: number; + declare createdAt: string | null; + declare updatedAt: string | null; + + constructor(model?: T) { + if (!model) return; + this.id = model.id; + this.createdAt = model.createdAt?.toISO(); + this.updatedAt = model.updatedAt?.toISO(); + } + + static fromArray< + T extends AppBaseModel, + TDto extends CommonModelDto, + TModel = any, + >( + this: new (model: TModel, ...args: any[]) => TDto, + models: TModel[], + ...args: any[] + ): TDto[] { + if (!Array.isArray(models)) return []; + return models.map((model) => new this(model, ...args)); + } + + static fromPaginator< + T extends AppBaseModel, + TDto extends CommonModelDto, + TModel = any, + >( + this: StaticDto, + paginator: TModel extends LucidRow + ? ModelPaginatorContract + : SimplePaginatorContract, + range?: SimplePaginatorDtoMetaRange + ) { + return new SimplePaginatorDto(paginator, this, range); + } + + serialize(): { + id: number; + createdAt: string | null; + updatedAt: string | null; + } { + return { + id: this.id, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + }; + } +} diff --git a/app/dtos/link.ts b/app/dtos/link.ts new file mode 100644 index 0000000..da87e50 --- /dev/null +++ b/app/dtos/link.ts @@ -0,0 +1,54 @@ +import { CommonModelDto } from '#dtos/common_model'; +import Link from '#models/link'; + +export class LinkDto extends CommonModelDto { + declare id: number; + declare name: string; + declare description: string | null; + declare url: string; + declare favorite: boolean; + declare collectionId: number; + declare authorId: number; + declare createdAt: string | null; + declare updatedAt: string | null; + + constructor(link?: Link) { + if (!link) return; + super(link); + + this.id = link.id; + this.name = link.name; + this.description = link.description; + this.url = link.url; + this.favorite = link.favorite; + this.collectionId = link.collectionId; + this.authorId = link.authorId; + this.createdAt = link.createdAt?.toISO(); + this.updatedAt = link.updatedAt?.toISO(); + } + + serialize(): { + id: number; + name: string; + description: string | null; + url: string; + favorite: boolean; + collectionId: number; + authorId: number; + createdAt: string | null; + updatedAt: string | null; + } { + return { + ...super.serialize(), + id: this.id, + name: this.name, + description: this.description, + url: this.url, + favorite: this.favorite, + collectionId: this.collectionId, + authorId: this.authorId, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + }; + } +} diff --git a/app/dtos/link_with_collection.ts b/app/dtos/link_with_collection.ts new file mode 100644 index 0000000..7d8f576 --- /dev/null +++ b/app/dtos/link_with_collection.ts @@ -0,0 +1,60 @@ +import { CollectionDto } from '#dtos/collection'; +import { CommonModelDto } from '#dtos/common_model'; +import Link from '#models/link'; +import { Collection } from '#shared/types/dto'; + +export class LinkWithCollectionDto extends CommonModelDto { + declare id: number; + declare name: string; + declare description: string | null; + declare url: string; + declare favorite: boolean; + declare collectionId: number; + declare collection: CollectionDto; + declare authorId: number; + declare createdAt: string | null; + declare updatedAt: string | null; + + constructor(link?: Link) { + if (!link) return; + super(link); + + this.id = link.id; + this.name = link.name; + this.description = link.description; + this.url = link.url; + this.favorite = link.favorite; + this.collectionId = link.collectionId; + this.collection = new CollectionDto(link.collection); + this.authorId = link.authorId; + this.createdAt = link.createdAt?.toISO(); + this.updatedAt = link.updatedAt?.toISO(); + } + + serialize(): { + id: number; + name: string; + description: string | null; + url: string; + favorite: boolean; + collectionId: number; + collection: Collection; + authorId: number; + createdAt: string | null; + updatedAt: string | null; + } { + return { + ...super.serialize(), + id: this.id, + name: this.name, + description: this.description, + url: this.url, + favorite: this.favorite, + collectionId: this.collectionId, + collection: this.collection.serialize(), + authorId: this.authorId, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + }; + } +} diff --git a/app/dtos/shared_collection.ts b/app/dtos/shared_collection.ts new file mode 100644 index 0000000..585dca7 --- /dev/null +++ b/app/dtos/shared_collection.ts @@ -0,0 +1,58 @@ +import { CommonModelDto } from '#dtos/common_model'; +import { LinkDto } from '#dtos/link'; +import { UserDto } from '#dtos/user'; +import { Visibility } from '#enums/collections/visibility'; +import Collection from '#models/collection'; +import { Link, User } from '#shared/types/dto'; + +export class SharedCollectionDto extends CommonModelDto { + declare id: number; + declare name: string; + declare description: string | null; + declare visibility: Visibility; + declare links: LinkDto[]; + declare authorId: number; + declare author: UserDto; + declare createdAt: string | null; + declare updatedAt: string | null; + + constructor(collection?: Collection) { + if (!collection) return; + super(collection); + + this.id = collection.id; + this.name = collection.name; + this.description = collection.description; + this.visibility = collection.visibility; + this.links = LinkDto.fromArray(collection.links); + this.authorId = collection.authorId; + this.author = new UserDto(collection.author); + this.createdAt = collection.createdAt?.toISO(); + this.updatedAt = collection.updatedAt?.toISO(); + } + + serialize(): { + id: number; + name: string; + description: string | null; + visibility: Visibility; + links: Link[]; + authorId: number; + author: User; + createdAt: string | null; + updatedAt: string | null; + } { + return { + ...super.serialize(), + id: this.id, + name: this.name, + description: this.description, + visibility: this.visibility, + links: this.links.map((link) => link.serialize()), + authorId: this.authorId, + author: this.author.serialize(), + createdAt: this.createdAt, + updatedAt: this.updatedAt, + }; + } +} diff --git a/app/dtos/simple_paginator.ts b/app/dtos/simple_paginator.ts new file mode 100644 index 0000000..ca25ef4 --- /dev/null +++ b/app/dtos/simple_paginator.ts @@ -0,0 +1,65 @@ +// Source : https://github.com/adocasts/package-dto/blob/main/src/paginator/simple_paginator_dto.ts + +import { CommonModelDto } from '#dtos/common_model'; +import AppBaseModel from '#models/app_base_model'; +import { + SimplePaginatorDtoContract, + SimplePaginatorDtoMetaContract, + SimplePaginatorDtoMetaRange, + StaticDto, +} from '#types/dto'; +import { LucidRow, ModelPaginatorContract } from '@adonisjs/lucid/types/model'; +import { SimplePaginatorContract } from '@adonisjs/lucid/types/querybuilder'; + +export default class SimplePaginatorDto< + T extends AppBaseModel, + TDto extends CommonModelDto, + TModel = any, +> implements SimplePaginatorDtoContract +{ + declare data: TDto[]; + declare meta: SimplePaginatorDtoMetaContract; + + /** + * Constructs a new instance of the SimplePaginatorDto class. + * + * @param {SimplePaginatorContract|ModelPaginatorContract} paginator - The paginator object containing the data. + * @param {StaticDto} dto - The static DTO class used to map the data. + * @param {SimplePaginatorDtoMetaRange} [range] - Optional range for the paginator. + */ + constructor( + paginator: TModel extends LucidRow + ? ModelPaginatorContract + : SimplePaginatorContract, + dto: StaticDto, + range?: SimplePaginatorDtoMetaRange + ) { + this.data = paginator.all().map((row) => new dto(row)); + + this.meta = { + total: paginator.total, + perPage: paginator.perPage, + currentPage: paginator.currentPage, + lastPage: paginator.lastPage, + firstPage: paginator.firstPage, + firstPageUrl: paginator.getUrl(1), + lastPageUrl: paginator.getUrl(paginator.lastPage), + nextPageUrl: paginator.getNextPageUrl(), + previousPageUrl: paginator.getPreviousPageUrl(), + }; + + if (range?.start || range?.end) { + const start = range?.start || paginator.firstPage; + const end = range?.end || paginator.lastPage; + + this.meta.pagesInRange = paginator.getUrlsForRange(start, end); + } + } + + serialize() { + return { + data: this.data.map((item) => item.serialize()), + meta: this.meta, + }; + } +} diff --git a/app/dtos/user.ts b/app/dtos/user.ts new file mode 100644 index 0000000..66fa7c6 --- /dev/null +++ b/app/dtos/user.ts @@ -0,0 +1,42 @@ +import { CommonModelDto } from '#dtos/common_model'; +import User from '#models/user'; + +export class UserDto extends CommonModelDto { + declare id: number; + declare fullname: string; + declare avatarUrl: string; + declare isAdmin: boolean; + declare createdAt: string | null; + declare updatedAt: string | null; + + constructor(user?: User) { + if (!user) return; + super(user); + + this.id = user.id; + this.fullname = user.fullname; + this.avatarUrl = user.avatarUrl; + this.isAdmin = user.isAdmin; + this.createdAt = user.createdAt.toISO(); + this.updatedAt = user.updatedAt.toISO(); + } + + serialize(): { + id: number; + fullname: string; + avatarUrl: string; + isAdmin: boolean; + createdAt: string | null; + updatedAt: string | null; + } { + return { + ...super.serialize(), + id: this.id, + fullname: this.fullname, + avatarUrl: this.avatarUrl, + isAdmin: this.isAdmin, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + }; + } +} diff --git a/app/dtos/user_auth.ts b/app/dtos/user_auth.ts new file mode 100644 index 0000000..8cba2cb --- /dev/null +++ b/app/dtos/user_auth.ts @@ -0,0 +1,27 @@ +import { UserDto } from '#dtos/user'; +import User from '#models/user'; + +export class UserAuthDto { + declare isAuthenticated: boolean; + declare isAdmin: boolean; + declare user?: UserDto; + + constructor(user: User | undefined) { + if (!user) return; + this.isAuthenticated = !!user; + this.isAdmin = user?.isAdmin; + this.user = user && new UserDto(user); + } + + serialize(): { + isAuthenticated: boolean; + isAdmin: boolean; + user: ReturnType | undefined; + } { + return { + isAuthenticated: this.isAuthenticated, + isAdmin: this.isAdmin, + user: this.user?.serialize(), + }; + } +} diff --git a/app/dtos/user_with_counters.ts b/app/dtos/user_with_counters.ts new file mode 100644 index 0000000..dc99ec6 --- /dev/null +++ b/app/dtos/user_with_counters.ts @@ -0,0 +1,58 @@ +import { CommonModelDto } from '#dtos/common_model'; +import User from '#models/user'; + +export class UserWithCountersDto extends CommonModelDto { + declare id: number; + declare email: string; + declare fullname: string; + declare avatarUrl: string; + declare isAdmin: boolean; + declare linksCount: number; + declare collectionsCount: number; + declare lastSeenAt: string | null; + declare createdAt: string | null; + declare updatedAt: string | null; + + constructor(user?: User) { + if (!user) return; + super(user); + + this.id = user.id; + this.email = user.email; + this.fullname = user.fullname; + this.avatarUrl = user.avatarUrl; + this.isAdmin = user.isAdmin; + this.linksCount = Number(user.$extras.totalLinks); + this.collectionsCount = Number(user.$extras.totalCollections); + this.lastSeenAt = user.lastSeenAt?.toString() ?? user.updatedAt.toString(); + this.createdAt = user.createdAt?.toISO(); + this.updatedAt = user.updatedAt?.toISO(); + } + + serialize(): { + id: number; + email: string; + fullname: string; + avatarUrl: string; + isAdmin: boolean; + linksCount: number; + collectionsCount: number; + lastSeenAt: string | null; + createdAt: string | null; + updatedAt: string | null; + } { + return { + ...super.serialize(), + id: this.id, + email: this.email, + fullname: this.fullname, + avatarUrl: this.avatarUrl, + isAdmin: this.isAdmin, + linksCount: this.linksCount, + collectionsCount: this.collectionsCount, + lastSeenAt: this.lastSeenAt, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + }; + } +} diff --git a/app/types/dto.ts b/app/types/dto.ts new file mode 100644 index 0000000..a54f77c --- /dev/null +++ b/app/types/dto.ts @@ -0,0 +1,28 @@ +export type StaticDto = { new (model: Model): Dto }; + +export interface SimplePaginatorDtoContract { + data: Dto[]; + meta: SimplePaginatorDtoMetaContract; +} + +export interface SimplePaginatorDtoMetaContract { + total: number; + perPage: number; + currentPage: number; + lastPage: number; + firstPage: number; + firstPageUrl: string; + lastPageUrl: string; + nextPageUrl: string | null; + previousPageUrl: string | null; + pagesInRange?: { + url: string; + page: number; + isActive: boolean; + }[]; +} + +export type SimplePaginatorDtoMetaRange = { + start: number; + end: number; +}; diff --git a/config/inertia.ts b/config/inertia.ts index 2cf1557..5846b21 100644 --- a/config/inertia.ts +++ b/config/inertia.ts @@ -1,10 +1,12 @@ import { isSSREnableForPage } from '#config/ssr'; import { DEFAULT_USER_THEME, KEY_USER_THEME } from '#constants/user/theme'; +import { UserAuthDto } from '#dtos/user_auth'; import env from '#start/env'; import logger from '@adonisjs/core/services/logger'; import { defineConfig } from '@adonisjs/inertia'; +import type { InferSharedProps } from '@adonisjs/inertia/types'; -export default defineConfig({ +const inertiaConfig = defineConfig({ /** * Path to the Edge view that will be used as the root view for Inertia responses */ @@ -19,13 +21,11 @@ export default defineConfig({ user: (ctx) => ({ theme: ctx.session?.get(KEY_USER_THEME, DEFAULT_USER_THEME), }), - auth: async (ctx) => { - await ctx.auth?.check(); - return { - user: ctx.auth?.user || null, - isAuthenticated: ctx.auth?.isAuthenticated || false, - }; - }, + auth: async (ctx) => + ctx.inertia.always(async () => { + await ctx.auth?.check(); + return new UserAuthDto(ctx.auth?.user).serialize(); + }), appUrl: env.get('APP_URL'), }, @@ -42,3 +42,9 @@ export default defineConfig({ }, }, }); + +export default inertiaConfig; + +declare module '@adonisjs/inertia/types' { + export interface SharedProps extends InferSharedProps {} +} diff --git a/inertia/components/admin/users/users_table.tsx b/inertia/components/admin/users/users_table.tsx index 79dc976..c3daaaf 100644 --- a/inertia/components/admin/users/users_table.tsx +++ b/inertia/components/admin/users/users_table.tsx @@ -1,3 +1,4 @@ +import { UserWithCounters } from '#shared/types/dto'; import { ScrollArea, Table, @@ -15,23 +16,16 @@ import { Th } from '~/components/admin/users/th'; import { sortData } from '~/components/admin/users/utils'; import { UserBadgeRole } from '~/components/common/user_badge_role'; import { DATE_FORMAT } from '~/constants'; -import { User } from '~/types/app'; dayjs.extend(relativeTime); -export type UserWithCounts = User & { - linksCount: number; - collectionsCount: number; -}; -export type UsersWithCounts = UserWithCounts[]; - -export type Columns = keyof UserWithCounts; +export type Columns = keyof UserWithCounters; const DEFAULT_SORT_BY: Columns = 'lastSeenAt'; const DEFAULT_SORT_DIRECTION = true; export interface UsersTableProps { - users: UsersWithCounts; + users: UserWithCounters[]; totalCollections: number; totalLinks: number; } @@ -56,7 +50,7 @@ export function UsersTable({ }) ); - const setSorting = (field: keyof UserWithCounts) => { + const setSorting = (field: keyof UserWithCounters) => { const reversed = field === sortBy ? !reverseSortDirection : false; setReverseSortDirection(reversed); setSortBy(field); @@ -75,11 +69,14 @@ export function UsersTable({ ); }; - const renderDateCell = (date: string) => ( - - {dayjs(date).fromNow()} - - ); + const renderDateCell = (date: string | null) => { + if (!date) return '-'; + return ( + + {dayjs(date).fromNow()} + + ); + }; const rows = sortedData.map((user) => ( diff --git a/inertia/components/admin/users/utils.ts b/inertia/components/admin/users/utils.ts index 095a4e0..cb3c985 100644 --- a/inertia/components/admin/users/utils.ts +++ b/inertia/components/admin/users/utils.ts @@ -1,22 +1,19 @@ -import { - UsersWithCounts, - UserWithCounts, -} from '~/components/admin/users/users_table'; +import { UserWithCounters } from '#shared/types/dto'; -export function filterData(data: UsersWithCounts, search: string) { +export function filterData(data: UserWithCounters[], search: string) { const query = search.toLowerCase().trim(); return data.filter((item) => - ['email', 'name', 'nickName', 'fullname'].some((key) => { - const value = item[key as keyof UserWithCounts]; + ['email', 'fullname'].some((key) => { + const value = item[key as keyof UserWithCounters]; return typeof value === 'string' && value.toLowerCase().includes(query); }) ); } export function sortData( - data: UsersWithCounts, + data: UserWithCounters[], payload: { - sortBy: keyof UserWithCounts | null; + sortBy: keyof UserWithCounters | null; reversed: boolean; search: string; } @@ -29,6 +26,7 @@ export function sortData( return filterData( [...data].sort((a, b) => { + if (!a[sortBy] || !b[sortBy]) return 0; if (payload.reversed) { return b[sortBy] > a[sortBy] ? 1 : -1; } diff --git a/inertia/components/common/user_badge_role.tsx b/inertia/components/common/user_badge_role.tsx index 4d34e8c..f803a93 100644 --- a/inertia/components/common/user_badge_role.tsx +++ b/inertia/components/common/user_badge_role.tsx @@ -1,9 +1,9 @@ +import { UserWithCounters } from '#shared/types/dto'; import { Badge } from '@mantine/core'; import { useTranslation } from 'react-i18next'; -import type { PublicUser, User } from '~/types/app'; interface UserBadgeRoleProps { - user: User | PublicUser; + user: UserWithCounters; } export function UserBadgeRole({ user }: UserBadgeRoleProps) { diff --git a/inertia/components/dashboard/collection/item/collection_item.tsx b/inertia/components/dashboard/collection/item/collection_item.tsx index a07a3c2..ebcf0e8 100644 --- a/inertia/components/dashboard/collection/item/collection_item.tsx +++ b/inertia/components/dashboard/collection/item/collection_item.tsx @@ -1,3 +1,4 @@ +import { CollectionWithLinks } from '#shared/types/dto'; import { Link } from '@inertiajs/react'; import { route } from '@izzyjs/route/client'; import { Text } from '@mantine/core'; @@ -5,7 +6,6 @@ import { useEffect, useRef } from 'react'; import { AiFillFolderOpen, AiOutlineFolder } from 'react-icons/ai'; import { useActiveCollection } from '~/hooks/collections/use_active_collection'; import { appendCollectionId } from '~/lib/navigation'; -import { CollectionWithLinks } from '~/types/app'; import classes from './collection_item.module.css'; interface CollectionItemProps { diff --git a/inertia/components/dashboard/favorite/item/favorite_item.tsx b/inertia/components/dashboard/favorite/item/favorite_item.tsx index 207ea1d..6bfedfe 100644 --- a/inertia/components/dashboard/favorite/item/favorite_item.tsx +++ b/inertia/components/dashboard/favorite/item/favorite_item.tsx @@ -1,11 +1,15 @@ +import { LinkWithCollection } from '#shared/types/dto'; import { Card, Flex, Group, Text } from '@mantine/core'; import { ExternalLinkStyled } from '~/components/common/external_link_styled'; import LinkFavicon from '~/components/dashboard/link/item/favicon/link_favicon'; import LinkControls from '~/components/dashboard/link/item/link_controls'; -import { LinkWithCollection } from '~/types/app'; import styles from './favorite_item.module.css'; -export const FavoriteItem = ({ link }: { link: LinkWithCollection }) => ( +interface FavoriteItemProps { + link: LinkWithCollection; +} + +export const FavoriteItem = ({ link }: FavoriteItemProps) => ( diff --git a/inertia/components/dashboard/link/item/link_controls.tsx b/inertia/components/dashboard/link/item/link_controls.tsx index 924f40c..77c2ee2 100644 --- a/inertia/components/dashboard/link/item/link_controls.tsx +++ b/inertia/components/dashboard/link/item/link_controls.tsx @@ -1,3 +1,4 @@ +import { Link } from '#shared/types/dto'; import { Link as InertiaLink, router } from '@inertiajs/react'; import { route } from '@izzyjs/route/client'; import { ActionIcon, Menu } from '@mantine/core'; @@ -10,10 +11,9 @@ import { IoTrashOutline } from 'react-icons/io5'; import { MdFavorite, MdFavoriteBorder } from 'react-icons/md'; import { onFavorite } from '~/lib/favorite'; import { appendCollectionId, appendLinkId } from '~/lib/navigation'; -import { Link, PublicLink } from '~/types/app'; interface LinksControlsProps { - link: Link | PublicLink; + link: Link; showGoToCollection?: boolean; } export default function LinkControls({ diff --git a/inertia/components/dashboard/link/item/link_item.tsx b/inertia/components/dashboard/link/item/link_item.tsx index 64991e9..5e3eb45 100644 --- a/inertia/components/dashboard/link/item/link_item.tsx +++ b/inertia/components/dashboard/link/item/link_item.tsx @@ -1,14 +1,14 @@ +import { Link } from '#shared/types/dto'; import { Card, Flex, Group, Text } from '@mantine/core'; import { AiFillStar } from 'react-icons/ai'; import { ExternalLinkStyled } from '~/components/common/external_link_styled'; import LinkFavicon from '~/components/dashboard/link/item/favicon/link_favicon'; import LinkControls from '~/components/dashboard/link/item/link_controls'; import type { LinkListProps } from '~/components/dashboard/link/list/link_list'; -import { Link, PublicLink } from '~/types/app'; import styles from './link.module.css'; interface LinkItemProps extends LinkListProps { - link: Link | PublicLink; + link: Link; } export function LinkItem({ link, hideMenu: hideMenu = false }: LinkItemProps) { diff --git a/inertia/components/form/form_link.tsx b/inertia/components/form/form_link.tsx index 7b7a588..d04eac4 100644 --- a/inertia/components/form/form_link.tsx +++ b/inertia/components/form/form_link.tsx @@ -1,10 +1,10 @@ +import { Collection } from '#shared/types/dto'; import { Checkbox, Select, TextInput } from '@mantine/core'; import { FormEvent } from 'react'; import { useTranslation } from 'react-i18next'; import BackToDashboard from '~/components/common/navigation/back_to_dashboard'; import useSearchParam from '~/hooks/use_search_param'; import { FormLayout, FormLayoutProps } from '~/layouts/form_layout'; -import { Collection } from '~/types/app'; export type FormLinkData = { name: string; diff --git a/inertia/constants/index.ts b/inertia/constants/index.ts index da392b4..d2b4a65 100644 --- a/inertia/constants/index.ts +++ b/inertia/constants/index.ts @@ -1,3 +1,2 @@ export const LS_LANG_KEY = 'language'; -export const GOOGLE_SEARCH_URL = 'https://google.com/search?q='; export const DATE_FORMAT = 'DD MMM YYYY (HH:mm)'; diff --git a/inertia/hooks/collections/use_active_collection.tsx b/inertia/hooks/collections/use_active_collection.tsx index 3e5ef95..c5b09e1 100644 --- a/inertia/hooks/collections/use_active_collection.tsx +++ b/inertia/hooks/collections/use_active_collection.tsx @@ -1,6 +1,6 @@ +import { CollectionWithLinks } from '#shared/types/dto'; import { PageProps } from '@adonisjs/inertia/types'; import { usePage } from '@inertiajs/react'; -import { CollectionWithLinks } from '~/types/app'; interface UseActiveCollectionProps { activeCollection?: CollectionWithLinks; diff --git a/inertia/hooks/collections/use_collections.tsx b/inertia/hooks/collections/use_collections.tsx index 6bc987c..9c5efd0 100644 --- a/inertia/hooks/collections/use_collections.tsx +++ b/inertia/hooks/collections/use_collections.tsx @@ -1,6 +1,6 @@ +import { CollectionWithLinks } from '#shared/types/dto'; import { PageProps } from '@adonisjs/inertia/types'; import { usePage } from '@inertiajs/react'; -import { CollectionWithLinks } from '~/types/app'; interface UseCollectionsProps { collections: CollectionWithLinks[]; diff --git a/inertia/hooks/collections/use_favorite_links.tsx b/inertia/hooks/collections/use_favorite_links.tsx index fec9a96..8e991b0 100644 --- a/inertia/hooks/collections/use_favorite_links.tsx +++ b/inertia/hooks/collections/use_favorite_links.tsx @@ -1,6 +1,6 @@ +import { LinkWithCollection } from '#shared/types/dto'; import { PageProps } from '@adonisjs/inertia/types'; import { usePage } from '@inertiajs/react'; -import { LinkWithCollection } from '~/types/app'; interface UseFavoriteLinksProps { favoriteLinks: LinkWithCollection[]; diff --git a/inertia/hooks/use_auth.tsx b/inertia/hooks/use_auth.tsx index cb8b107..b6c38f7 100644 --- a/inertia/hooks/use_auth.tsx +++ b/inertia/hooks/use_auth.tsx @@ -1,25 +1,15 @@ +import { UserAuth } from '#shared/types/dto'; +import { PageProps } from '@adonisjs/inertia/types'; import { usePage } from '@inertiajs/react'; -import type { Auth, InertiaPage } from '~/types/inertia'; -export const useAuth = () => usePage().props.auth; - -export const withAuth = ( - Component: React.ComponentType -): React.ComponentType> => { - return (props: Omit) => { +const useAuth = (): UserAuth => usePage().props.auth as UserAuth; +const withAuth = ( + Component: React.ComponentType +) => { + return (props: T) => { const auth = useAuth(); - return ; + return ; }; }; -export const withAuthRequired = ( - Component: React.ComponentType -): React.ComponentType> => { - return (props: Omit) => { - const auth = useAuth(); - if (!auth.isAuthenticated) { - return null; - } - return ; - }; -}; +export { useAuth, withAuth }; diff --git a/inertia/lib/favorite.ts b/inertia/lib/favorite.ts index a72e27a..a5b3abb 100644 --- a/inertia/lib/favorite.ts +++ b/inertia/lib/favorite.ts @@ -1,6 +1,6 @@ +import { Link } from '#shared/types/dto'; import { route } from '@izzyjs/route/client'; import { makeRequest } from '~/lib/request'; -import { Link } from '~/types/app'; export const onFavorite = ( linkId: Link['id'], diff --git a/inertia/lib/navigation.ts b/inertia/lib/navigation.ts index 7df0aa6..781c754 100644 --- a/inertia/lib/navigation.ts +++ b/inertia/lib/navigation.ts @@ -1,4 +1,4 @@ -import { Collection, CollectionWithLinks, Link } from '~/types/app'; +import { Collection, CollectionWithLinks, Link } from '#shared/types/dto'; export const appendCollectionId = ( url: string, diff --git a/inertia/pages/collections/create.tsx b/inertia/pages/collections/create.tsx index 3acbf1a..7ef8ce9 100644 --- a/inertia/pages/collections/create.tsx +++ b/inertia/pages/collections/create.tsx @@ -8,11 +8,13 @@ import { } from '~/components/form/form_collection'; import { Visibility } from '~/types/app'; +interface CreateCollectionPageProps { + disableHomeLink: boolean; +} + export default function CreateCollectionPage({ disableHomeLink, -}: { - disableHomeLink: boolean; -}) { +}: CreateCollectionPageProps) { const { t } = useTranslation('common'); const { data, setData, submit, processing } = useForm({ name: '', diff --git a/inertia/pages/collections/delete.tsx b/inertia/pages/collections/delete.tsx index 40f8c0e..91e2704 100644 --- a/inertia/pages/collections/delete.tsx +++ b/inertia/pages/collections/delete.tsx @@ -1,3 +1,4 @@ +import { Collection } from '#shared/types/dto'; import { useForm } from '@inertiajs/react'; import { route } from '@izzyjs/route/client'; import { useTranslation } from 'react-i18next'; @@ -5,13 +6,14 @@ import { FormCollection, FormCollectionData, } from '~/components/form/form_collection'; -import { Collection } from '~/types/app'; + +interface DeleteCollectionPageProps { + collection: Collection; +} export default function DeleteCollectionPage({ collection, -}: { - collection: Collection; -}) { +}: DeleteCollectionPageProps) { const { t } = useTranslation('common'); const { data, setData, submit, processing, errors } = useForm({ diff --git a/inertia/pages/collections/edit.tsx b/inertia/pages/collections/edit.tsx index eb66c2f..1355032 100644 --- a/inertia/pages/collections/edit.tsx +++ b/inertia/pages/collections/edit.tsx @@ -1,3 +1,4 @@ +import { Collection } from '#shared/types/dto'; import { useForm } from '@inertiajs/react'; import { route } from '@izzyjs/route/client'; import { useMemo } from 'react'; @@ -6,13 +7,14 @@ import { FormCollection, FormCollectionData, } from '~/components/form/form_collection'; -import { Collection } from '~/types/app'; + +interface EditCollectionPageProps { + collection: Collection; +} export default function EditCollectionPage({ collection, -}: { - collection: Collection; -}) { +}: EditCollectionPageProps) { const { t } = useTranslation('common'); const { data, setData, submit, processing, errors } = useForm({ diff --git a/inertia/pages/links/create.tsx b/inertia/pages/links/create.tsx index 6dcb261..10091b3 100644 --- a/inertia/pages/links/create.tsx +++ b/inertia/pages/links/create.tsx @@ -1,3 +1,4 @@ +import { Collection } from '#shared/types/dto'; import { useForm } from '@inertiajs/react'; import { route } from '@izzyjs/route/client'; import { useMemo } from 'react'; @@ -5,13 +6,12 @@ import { useTranslation } from 'react-i18next'; import { FormLink } from '~/components/form/form_link'; import useSearchParam from '~/hooks/use_search_param'; import { isValidHttpUrl } from '~/lib/navigation'; -import { Collection } from '~/types/app'; -export default function CreateLinkPage({ - collections, -}: { +interface CreateLinkPageProps { collections: Collection[]; -}) { +} + +export default function CreateLinkPage({ collections }: CreateLinkPageProps) { const { t } = useTranslation('common'); const collectionId = Number(useSearchParam('collectionId')) ?? collections[0].id; diff --git a/inertia/pages/links/delete.tsx b/inertia/pages/links/delete.tsx index 38e1335..6d0326a 100644 --- a/inertia/pages/links/delete.tsx +++ b/inertia/pages/links/delete.tsx @@ -1,10 +1,14 @@ +import { LinkWithCollection } from '#shared/types/dto'; import { useForm } from '@inertiajs/react'; import { route } from '@izzyjs/route/client'; import { useTranslation } from 'react-i18next'; import { FormLink } from '~/components/form/form_link'; -import { LinkWithCollection } from '~/types/app'; -export default function DeleteLinkPage({ link }: { link: LinkWithCollection }) { +interface DeleteLinkPageProps { + link: LinkWithCollection; +} + +export default function DeleteLinkPage({ link }: DeleteLinkPageProps) { const { t } = useTranslation('common'); const { data, setData, submit, processing } = useForm({ name: link.name, diff --git a/inertia/pages/links/edit.tsx b/inertia/pages/links/edit.tsx index db65348..c3458f9 100644 --- a/inertia/pages/links/edit.tsx +++ b/inertia/pages/links/edit.tsx @@ -1,18 +1,17 @@ +import { Collection, LinkWithCollection } from '#shared/types/dto'; import { useForm } from '@inertiajs/react'; import { route } from '@izzyjs/route/client'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { FormLink } from '~/components/form/form_link'; import { isValidHttpUrl } from '~/lib/navigation'; -import { Collection, Link } from '~/types/app'; -export default function EditLinkPage({ - collections, - link, -}: { +interface EditLinkPageProps { collections: Collection[]; - link: Link; -}) { + link: LinkWithCollection; +} + +export default function EditLinkPage({ collections, link }: EditLinkPageProps) { const { t } = useTranslation('common'); const { data, setData, submit, processing } = useForm({ name: link.name, diff --git a/inertia/pages/shared.tsx b/inertia/pages/shared.tsx index ad8c48b..12f54fc 100644 --- a/inertia/pages/shared.tsx +++ b/inertia/pages/shared.tsx @@ -1,10 +1,10 @@ +import { SharedCollection } from '#shared/types/dto'; import { Flex, Text } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { LinkList } from '~/components/dashboard/link/list/link_list'; -import type { CollectionWithLinks, PublicUser } from '~/types/app'; interface SharedPageProps { - activeCollection: CollectionWithLinks & { author: PublicUser }; + activeCollection: SharedCollection; } export default function SharedPage({ activeCollection }: SharedPageProps) { diff --git a/inertia/types/app.ts b/inertia/types/app.ts index 470e85c..bea25a8 100644 --- a/inertia/types/app.ts +++ b/inertia/types/app.ts @@ -1,64 +1,3 @@ -import { DisplayPreferences } from '#shared/types/index'; - -type CommonBase = { - id: number; - createdAt: string; - updatedAt: string; -}; - -export type User = CommonBase & { - email: string; - fullname: string; - avatarUrl: string; - isAdmin: boolean; - lastSeenAt: string; - displayPreferences: DisplayPreferences; -}; - -export type PublicUser = Omit; - -export type Users = User[]; - -export type UserWithCollections = User & { - collections: Collection[]; -}; - -export type UserWithRelationCount = CommonBase & { - email: string; - fullname: string; - avatarUrl: string; - isAdmin: string; - linksCount: number; - collectionsCount: number; - lastSeenAt: string; -}; - -export type Link = CommonBase & { - name: string; - description: string | null; - url: string; - favorite: boolean; - collectionId: number; -}; - -export type LinkWithCollection = Link & { - collection: Collection; -}; - -export type PublicLink = Omit; -export type PublicLinkWithCollection = Omit; - -export type Collection = CommonBase & { - name: string; - description: string | null; - visibility: Visibility; - authorId: number; -}; - -export type CollectionWithLinks = Collection & { - links: Link[]; -}; - export enum Visibility { PUBLIC = 'PUBLIC', PRIVATE = 'PRIVATE', diff --git a/package.json b/package.json index 23eecbe..4fefa37 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "#adonis/api": "./.adonisjs/api.ts", "#config/*": "./config/*.js", "#controllers/*": "./app/controllers/*.js", + "#dtos/*": "./app/dtos/*.js", "#models/*": "./app/models/*.js", "#routes/*": "./app/routes/*.js", "#services/*": "./app/services/*.js", diff --git a/shared/types/dto.ts b/shared/types/dto.ts new file mode 100644 index 0000000..6a30841 --- /dev/null +++ b/shared/types/dto.ts @@ -0,0 +1,19 @@ +import { CollectionDto } from '#dtos/collection'; +import { CollectionWithLinksDto } from '#dtos/collection_with_links'; +import { LinkDto } from '#dtos/link'; +import { LinkWithCollectionDto } from '#dtos/link_with_collection'; +import { SharedCollectionDto } from '#dtos/shared_collection'; +import { UserDto } from '#dtos/user'; +import { UserAuthDto } from '#dtos/user_auth'; +import { UserWithCountersDto } from '#dtos/user_with_counters'; + +export type User = ReturnType; +export type UserAuth = ReturnType; +export type UserWithCounters = ReturnType; +export type Collection = ReturnType; +export type SharedCollection = ReturnType; +export type CollectionWithLinks = ReturnType< + CollectionWithLinksDto['serialize'] +>; +export type Link = ReturnType; +export type LinkWithCollection = ReturnType;