diff --git a/app/controllers/collections_controller.ts b/app/controllers/collections_controller.ts index 27464aa..17725ee 100644 --- a/app/controllers/collections_controller.ts +++ b/app/controllers/collections_controller.ts @@ -120,6 +120,7 @@ export default class CollectionsController { async getCollectionsByAuthorId(authorId: User['id']) { return await Collection.query() .where('author_id', authorId) + .orderBy('created_at') .preload('links'); } diff --git a/app/controllers/users_controller.ts b/app/controllers/users_controller.ts index b2ed31d..04faef4 100644 --- a/app/controllers/users_controller.ts +++ b/app/controllers/users_controller.ts @@ -49,6 +49,7 @@ export default class UsersController { nickName, avatarUrl, token, + providerType: 'google', } ); diff --git a/app/middleware/log_request.ts b/app/middleware/log_request.ts index f109cf4..8371949 100644 --- a/app/middleware/log_request.ts +++ b/app/middleware/log_request.ts @@ -10,7 +10,7 @@ export default class LogRequest { !request.url().startsWith('/@react-refresh') && !request.url().includes('.ts') ) { - logger.info(`-> ${request.method()}: ${request.url()}`); + logger.info(`[${request.method()}]: ${request.url()}`); } await next(); } diff --git a/app/models/app_base_model.ts b/app/models/app_base_model.ts index 7a0f458..5152dfa 100644 --- a/app/models/app_base_model.ts +++ b/app/models/app_base_model.ts @@ -1,29 +1,27 @@ -import { BaseModel, CamelCaseNamingStrategy, beforeCreate, column } from '@adonisjs/lucid/orm'; +import { + BaseModel, + CamelCaseNamingStrategy, + column, +} from '@adonisjs/lucid/orm'; import { DateTime } from 'luxon'; -import { v4 as uuidv4 } from 'uuid'; export default class AppBaseModel extends BaseModel { static namingStrategy = new CamelCaseNamingStrategy(); static selfAssignPrimaryKey = true; @column({ isPrimary: true }) - declare id: string; // UUID + declare id: number; @column.dateTime({ autoCreate: true, - serializeAs: 'createdAt', + serializeAs: 'created_at', }) - declare createdAt: DateTime; + declare created_at: DateTime; @column.dateTime({ autoCreate: true, autoUpdate: true, - serializeAs: 'updatedAt', + serializeAs: 'updated_at', }) - declare updatedAt: DateTime; - - @beforeCreate() - static assignUuid(item: any) { - item.id = uuidv4(); - } + declare updated_at: DateTime; } diff --git a/app/models/collection.ts b/app/models/collection.ts index 87df0b4..ebaaa8f 100644 --- a/app/models/collection.ts +++ b/app/models/collection.ts @@ -16,10 +16,10 @@ export default class Collection extends AppBaseModel { declare visibility: Visibility; @column() - declare nextId: string; + declare next_id: number; @column() - declare authorId: string; + declare authorId: number; @belongsTo(() => User, { foreignKey: 'authorId' }) declare author: BelongsTo; diff --git a/app/models/link.ts b/app/models/link.ts index ed0646c..0c13584 100644 --- a/app/models/link.ts +++ b/app/models/link.ts @@ -9,7 +9,7 @@ export default class Link extends AppBaseModel { declare name: string; @column() - declare description: string; + declare description: string | null; @column() declare url: string; @@ -18,13 +18,13 @@ export default class Link extends AppBaseModel { declare favorite: boolean; @column() - declare collectionId: string; + declare collectionId: number; @belongsTo(() => Collection, { foreignKey: 'collectionId' }) declare collection: BelongsTo; @column() - declare authorId: string; + declare authorId: number; @belongsTo(() => User, { foreignKey: 'authorId' }) declare author: BelongsTo; diff --git a/app/models/user.ts b/app/models/user.ts index a683997..a666869 100644 --- a/app/models/user.ts +++ b/app/models/user.ts @@ -1,7 +1,7 @@ import Collection from '#models/collection'; import Link from '#models/link'; import type { GoogleToken } from '@adonisjs/ally/types'; -import { column, manyToMany } from '@adonisjs/lucid/orm'; +import { column, computed, manyToMany } from '@adonisjs/lucid/orm'; import type { ManyToMany } from '@adonisjs/lucid/types/relations'; import AppBaseModel from './app_base_model.js'; @@ -12,20 +12,23 @@ export default class User extends AppBaseModel { @column() declare name: string; - @column({ serializeAs: 'nickName' }) + @column() declare nickName: string; // public username - @column({ serializeAs: 'avatarUrl' }) + @column() declare avatarUrl: string; @column() declare isAdmin: boolean; @column({ serializeAs: null }) - declare token: GoogleToken; + declare token?: GoogleToken; @column({ serializeAs: null }) - declare providerId: string; + declare providerId: number; + + @column({ serializeAs: null }) + declare providerType: 'google'; @manyToMany(() => Collection, { relatedKey: 'authorId', @@ -36,4 +39,9 @@ export default class User extends AppBaseModel { relatedKey: 'authorId', }) declare links: ManyToMany; + + @computed() + get fullname() { + return this.nickName || this.name; + } } diff --git a/app/validators/collection.ts b/app/validators/collection.ts index 3e16626..54516b9 100644 --- a/app/validators/collection.ts +++ b/app/validators/collection.ts @@ -2,7 +2,7 @@ import vine, { SimpleMessagesProvider } from '@vinejs/vine'; import { Visibility } from '../enums/visibility.js'; const params = vine.object({ - id: vine.string().trim(), + id: vine.number(), }); export const createCollectionValidator = vine.compile( @@ -10,7 +10,7 @@ export const createCollectionValidator = vine.compile( name: vine.string().trim().minLength(1).maxLength(254), description: vine.string().trim().maxLength(254).nullable(), visibility: vine.enum(Visibility), - nextId: vine.string().optional(), + nextId: vine.number(), }) ); @@ -19,7 +19,7 @@ export const updateCollectionValidator = vine.compile( name: vine.string().trim().minLength(1).maxLength(254), description: vine.string().trim().maxLength(254).nullable(), visibility: vine.enum(Visibility), - nextId: vine.string().optional(), + nextId: vine.number(), params, }) diff --git a/app/validators/link.ts b/app/validators/link.ts index f4225f6..f247377 100644 --- a/app/validators/link.ts +++ b/app/validators/link.ts @@ -1,7 +1,7 @@ import vine from '@vinejs/vine'; const params = vine.object({ - id: vine.string().trim(), + id: vine.number(), }); export const createLinkValidator = vine.compile( @@ -10,7 +10,7 @@ export const createLinkValidator = vine.compile( description: vine.string().trim().maxLength(300).optional(), url: vine.string().trim(), favorite: vine.boolean(), - collectionId: vine.string().trim(), + collectionId: vine.number(), }) ); @@ -20,7 +20,7 @@ export const updateLinkValidator = vine.compile( description: vine.string().trim().maxLength(300).optional(), url: vine.string().trim(), favorite: vine.boolean(), - collectionId: vine.string().trim(), + collectionId: vine.number(), params, }) @@ -37,7 +37,7 @@ export const updateLinkFavoriteStatusValidator = vine.compile( favorite: vine.boolean(), params: vine.object({ - id: vine.string().trim(), + id: vine.number(), }), }) ); diff --git a/app/validators/search.ts b/app/validators/search.ts new file mode 100644 index 0000000..503dc2d --- /dev/null +++ b/app/validators/search.ts @@ -0,0 +1,9 @@ +import vine from '@vinejs/vine'; + +export const searchValidator = vine.compile( + vine.object({ + searchTerm: vine.string().trim().minLength(1).maxLength(254), + links: vine.boolean(), + collections: vine.boolean(), + }) +); diff --git a/database/default_table_fields.ts b/database/default_table_fields.ts new file mode 100644 index 0000000..1c901c1 --- /dev/null +++ b/database/default_table_fields.ts @@ -0,0 +1,8 @@ +import { Knex } from 'knex'; + +export function defaultTableFields(table: Knex.CreateTableBuilder) { + table.increments('id', { primaryKey: true }).first().unique().notNullable(); + + table.timestamp('created_at').notNullable(); + table.timestamp('updated_at').nullable(); +} diff --git a/database/migrations/1714218548323_create_users_table.ts b/database/migrations/1714218548323_create_users_table.ts index f40f97e..87068c7 100644 --- a/database/migrations/1714218548323_create_users_table.ts +++ b/database/migrations/1714218548323_create_users_table.ts @@ -1,27 +1,26 @@ +import { defaultTableFields } from '#database/default_table_fields'; import { BaseSchema } from '@adonisjs/lucid/schema'; -export default class extends BaseSchema { - protected tableName = 'users'; +export default class CreateUsersTable extends BaseSchema { + static tableName = 'users'; async up() { - this.schema.createTable(this.tableName, (table) => { - table.uuid('id').primary().unique().notNullable(); - + this.schema.createTableIfNotExists(CreateUsersTable.tableName, (table) => { table.string('email', 254).notNullable().unique(); table.string('name', 254).notNullable(); - table.string('nick_name', 254).notNullable(); + table.string('nick_name', 254).nullable(); table.text('avatar_url').notNullable(); table.boolean('is_admin').defaultTo(0).notNullable(); - table.json('token').notNullable(); + table.json('token').nullable(); table.string('provider_id').notNullable(); + table.enum('provider_type', ['google']).notNullable().defaultTo('google'); - table.timestamp('created_at').notNullable(); - table.timestamp('updated_at').nullable(); + defaultTableFields(table); }); } async down() { - this.schema.dropTable(this.tableName); + this.schema.dropTable(CreateUsersTable.tableName); } } diff --git a/database/migrations/1714253983443_create_collections_table.ts b/database/migrations/1714253983443_create_collections_table.ts index 6cba8d2..26eef62 100644 --- a/database/migrations/1714253983443_create_collections_table.ts +++ b/database/migrations/1714253983443_create_collections_table.ts @@ -1,40 +1,44 @@ +import { defaultTableFields } from '#database/default_table_fields'; import { BaseSchema } from '@adonisjs/lucid/schema'; import { Visibility } from '../../app/enums/visibility.js'; -export default class extends BaseSchema { - protected tableName = 'collections'; +export default class CreateCollectionTable extends BaseSchema { + static tableName = 'collections'; private visibilityEnumName = 'collection_visibility'; async up() { this.schema.raw(`DROP TYPE IF EXISTS ${this.visibilityEnumName}`); - this.schema.createTable(this.tableName, (table) => { - table.uuid('id').primary().unique().notNullable(); + this.schema.createTableIfNotExists( + CreateCollectionTable.tableName, + (table) => { + table.string('name', 254).notNullable(); + table.string('description', 254).nullable(); + table + .enum('visibility', Object.values(Visibility), { + useNative: true, + enumName: this.visibilityEnumName, + existingType: false, + }) + .nullable() + .defaultTo(Visibility.PRIVATE); + table + .integer('next_id') + .references('id') + .inTable('collections') + .defaultTo(null); + table + .integer('author_id') + .references('id') + .inTable('users') + .onDelete('CASCADE'); - table.string('name', 254).notNullable(); - table.string('description', 254); - table - .uuid('next_id') - .references('id') - .inTable('collections') - .defaultTo(null); - table - .uuid('author_id') - .references('id') - .inTable('users') - .onDelete('CASCADE'); - table.enum('visibility', Object.values(Visibility), { - useNative: true, - enumName: this.visibilityEnumName, - existingType: false, - }); - - table.timestamp('created_at'); - table.timestamp('updated_at'); - }); + defaultTableFields(table); + } + ); } async down() { this.schema.raw(`DROP TYPE IF EXISTS ${this.visibilityEnumName}`); - this.schema.dropTable(this.tableName); + this.schema.dropTable(CreateCollectionTable.tableName); } } diff --git a/database/migrations/1714254076754_create_links_table.ts b/database/migrations/1714254076754_create_links_table.ts index fb8daaa..f65a26d 100644 --- a/database/migrations/1714254076754_create_links_table.ts +++ b/database/migrations/1714254076754_create_links_table.ts @@ -1,33 +1,31 @@ +import { defaultTableFields } from '#database/default_table_fields'; import { BaseSchema } from '@adonisjs/lucid/schema'; -export default class extends BaseSchema { - protected tableName = 'links'; +export default class CreateLinksTable extends BaseSchema { + static tableName = 'links'; async up() { - this.schema.createTable(this.tableName, (table) => { - table.uuid('id').primary().unique().notNullable(); - + this.schema.createTableIfNotExists(CreateLinksTable.tableName, (table) => { table.string('name', 254).notNullable(); - table.string('description', 254); + table.string('description', 254).nullable(); table.text('url').notNullable(); table.boolean('favorite').notNullable().defaultTo(0); table - .uuid('collection_id') + .integer('collection_id') .references('id') .inTable('collections') .onDelete('CASCADE'); table - .uuid('author_id') + .integer('author_id') .references('id') .inTable('users') .onDelete('CASCADE'); - table.timestamp('created_at'); - table.timestamp('updated_at'); + defaultTableFields(table); }); } async down() { - this.schema.dropTable(this.tableName); + this.schema.dropTable(CreateLinksTable.tableName); } } diff --git a/database/seeders/user_seeder.ts b/database/seeders/user_seeder.ts index 91b22d8..69889cf 100644 --- a/database/seeders/user_seeder.ts +++ b/database/seeders/user_seeder.ts @@ -16,13 +16,14 @@ export default class extends BaseSeeder { export function createRandomUser() { return { - id: faker.string.uuid(), + id: faker.number.int(), email: faker.internet.email(), name: faker.internet.userName(), nickName: faker.internet.displayName(), avatarUrl: faker.image.avatar(), isAdmin: false, - providerId: faker.string.uuid(), + providerId: faker.number.int(), + providerType: 'google' as const, token: {} as GoogleToken, }; } diff --git a/inertia/components/dashboard/side_nav/user_card.tsx b/inertia/components/dashboard/side_nav/user_card.tsx index fc7b361..7d02a9e 100644 --- a/inertia/components/dashboard/side_nav/user_card.tsx +++ b/inertia/components/dashboard/side_nav/user_card.tsx @@ -4,7 +4,7 @@ import useUser from '~/hooks/use_user'; export default function UserCard() { const { user, isAuthenticated } = useUser(); - const altImage = `${user?.nickName}'s avatar`; + const altImage = `${user?.fullname}'s avatar`; return ( isAuthenticated && ( @@ -14,7 +14,7 @@ export default function UserCard() { alt={altImage} referrerPolicy="no-referrer" /> - {user.nickName} + {user.fullname} ) ); diff --git a/inertia/components/navbar/navbar.tsx b/inertia/components/navbar/navbar.tsx index 5b513be..6c19cce 100644 --- a/inertia/components/navbar/navbar.tsx +++ b/inertia/components/navbar/navbar.tsx @@ -103,7 +103,7 @@ function ProfileDropdown() { width={22} referrerPolicy="no-referrer" /> - {user!.nickName} + {user!.fullname} } > diff --git a/inertia/components/settings/modal.tsx b/inertia/components/settings/modal.tsx index 8d59332..76f809a 100644 --- a/inertia/components/settings/modal.tsx +++ b/inertia/components/settings/modal.tsx @@ -1,7 +1,7 @@ import { Link } from '@inertiajs/react'; import { route } from '@izzyjs/route/client'; import { useTranslation } from 'react-i18next'; -import { BsGear } from 'react-icons/bs'; +import { PiGearLight } from 'react-icons/pi'; import Modal from '~/components/common/modal/modal'; import LangSelector from '~/components/lang_selector'; import ThemeSwitcher from '~/components/theme_switcher'; @@ -20,7 +20,7 @@ export default function ModalSettings({ return ( <> - + {t('settings')} diff --git a/inertia/types/app.d.ts b/inertia/types/app.d.ts index 670788e..0756812 100644 --- a/inertia/types/app.d.ts +++ b/inertia/types/app.d.ts @@ -1,10 +1,11 @@ import type Collection from '#models/collection'; type User = { - id: string; + id: number; email: string; name: string; nickName: string; + fullname: string; avatarUrl: string; isAdmin: boolean; collections: Collection[];