refactor: migrate from types to dto

This commit is contained in:
Sonny
2025-12-10 05:12:14 +01:00
parent cd0a8a4803
commit be41962c89
51 changed files with 709 additions and 192 deletions

View File

@@ -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,
});

View File

@@ -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(),
});
}
}

View File

@@ -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),
});
}
}

View File

@@ -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(),
});
}
}

View File

@@ -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) {

View File

@@ -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));
}
}

View File

@@ -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(),
});
}

View File

@@ -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,
});
}
}

View File

@@ -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(),
});
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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(),
});
}
}

View File

@@ -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();
}

47
app/dtos/collection.ts Normal file
View File

@@ -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<Collection> {
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,
};
}
}

View File

@@ -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<Collection> {
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,
};
}
}

57
app/dtos/common_model.ts Normal file
View File

@@ -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<T extends AppBaseModel> {
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<T>,
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<T>,
TModel = any,
>(
this: StaticDto<TModel, TDto>,
paginator: TModel extends LucidRow
? ModelPaginatorContract<TModel>
: SimplePaginatorContract<TModel>,
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,
};
}
}

54
app/dtos/link.ts Normal file
View File

@@ -0,0 +1,54 @@
import { CommonModelDto } from '#dtos/common_model';
import Link from '#models/link';
export class LinkDto extends CommonModelDto<Link> {
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,
};
}
}

View File

@@ -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<Link> {
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,
};
}
}

View File

@@ -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<Collection> {
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,
};
}
}

View File

@@ -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<T>,
TModel = any,
> implements SimplePaginatorDtoContract<TDto>
{
declare data: TDto[];
declare meta: SimplePaginatorDtoMetaContract;
/**
* Constructs a new instance of the SimplePaginatorDto class.
*
* @param {SimplePaginatorContract<Model>|ModelPaginatorContract<Model>} paginator - The paginator object containing the data.
* @param {StaticDto<Model, Dto>} dto - The static DTO class used to map the data.
* @param {SimplePaginatorDtoMetaRange} [range] - Optional range for the paginator.
*/
constructor(
paginator: TModel extends LucidRow
? ModelPaginatorContract<TModel>
: SimplePaginatorContract<TModel>,
dto: StaticDto<TModel, TDto>,
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,
};
}
}

42
app/dtos/user.ts Normal file
View File

@@ -0,0 +1,42 @@
import { CommonModelDto } from '#dtos/common_model';
import User from '#models/user';
export class UserDto extends CommonModelDto<User> {
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,
};
}
}

27
app/dtos/user_auth.ts Normal file
View File

@@ -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<UserDto['serialize']> | undefined;
} {
return {
isAuthenticated: this.isAuthenticated,
isAdmin: this.isAdmin,
user: this.user?.serialize(),
};
}
}

View File

@@ -0,0 +1,58 @@
import { CommonModelDto } from '#dtos/common_model';
import User from '#models/user';
export class UserWithCountersDto extends CommonModelDto<User> {
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,
};
}
}

28
app/types/dto.ts Normal file
View File

@@ -0,0 +1,28 @@
export type StaticDto<Model, Dto> = { new (model: Model): Dto };
export interface SimplePaginatorDtoContract<Dto> {
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;
};

View File

@@ -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<typeof inertiaConfig> {}
}

View File

@@ -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) => (
<Tooltip label={dayjs(date).format(DATE_FORMAT).toString()}>
<Text>{dayjs(date).fromNow()}</Text>
</Tooltip>
);
const renderDateCell = (date: string | null) => {
if (!date) return '-';
return (
<Tooltip label={dayjs(date).format(DATE_FORMAT).toString()}>
<Text>{dayjs(date).fromNow()}</Text>
</Tooltip>
);
};
const rows = sortedData.map((user) => (
<Table.Tr key={user.id}>

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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) => (
<ExternalLinkStyled href={link.url} title={link.url}>
<Card className={styles.linkWrapper}>
<Group gap="xs" wrap="nowrap">

View File

@@ -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({

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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)';

View File

@@ -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;

View File

@@ -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[];

View File

@@ -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[];

View File

@@ -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<InertiaPage>().props.auth;
export const withAuth = <T extends object>(
Component: React.ComponentType<T & { auth: Auth }>
): React.ComponentType<Omit<T, 'auth'>> => {
return (props: Omit<T, 'auth'>) => {
const useAuth = (): UserAuth => usePage<PageProps>().props.auth as UserAuth;
const withAuth = <T extends object>(
Component: React.ComponentType<T & { auth: UserAuth }>
) => {
return (props: T) => {
const auth = useAuth();
return <Component {...(props as T)} auth={auth} />;
return <Component {...props} auth={auth} />;
};
};
export const withAuthRequired = <T extends object>(
Component: React.ComponentType<T & { auth: Auth }>
): React.ComponentType<Omit<T, 'auth'>> => {
return (props: Omit<T, 'auth'>) => {
const auth = useAuth();
if (!auth.isAuthenticated) {
return null;
}
return <Component {...(props as T)} auth={auth} />;
};
};
export { useAuth, withAuth };

View File

@@ -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'],

View File

@@ -1,4 +1,4 @@
import { Collection, CollectionWithLinks, Link } from '~/types/app';
import { Collection, CollectionWithLinks, Link } from '#shared/types/dto';
export const appendCollectionId = (
url: string,

View File

@@ -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<FormCollectionData>({
name: '',

View File

@@ -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<FormCollectionData>({

View File

@@ -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<FormCollectionData>({

View File

@@ -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;

View File

@@ -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,

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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<User, 'email'>;
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<Link, 'favorite'>;
export type PublicLinkWithCollection = Omit<Link, 'favorite'>;
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',

View File

@@ -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",

19
shared/types/dto.ts Normal file
View File

@@ -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<UserDto['serialize']>;
export type UserAuth = ReturnType<UserAuthDto['serialize']>;
export type UserWithCounters = ReturnType<UserWithCountersDto['serialize']>;
export type Collection = ReturnType<CollectionDto['serialize']>;
export type SharedCollection = ReturnType<SharedCollectionDto['serialize']>;
export type CollectionWithLinks = ReturnType<
CollectionWithLinksDto['serialize']
>;
export type Link = ReturnType<LinkDto['serialize']>;
export type LinkWithCollection = ReturnType<LinkWithCollectionDto['serialize']>;