mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-09 07:03:25 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb0345bf68 | ||
|
|
e28d5ebea8 | ||
|
|
e2494e8cf0 | ||
|
|
0d87a3f4bc | ||
|
|
c46cc1a8fb | ||
|
|
2f820bb877 | ||
|
|
01298661a5 | ||
|
|
2de2556a20 | ||
|
|
6005374340 | ||
|
|
eac0c135d6 | ||
|
|
aef2db6071 | ||
|
|
c989772b04 | ||
|
|
1938f6ea23 | ||
|
|
e8aca90870 | ||
|
|
fe849d7d69 | ||
|
|
cc63ce37c3 | ||
|
|
01efb11f70 | ||
|
|
466c8dec3a | ||
|
|
b2b388b77e | ||
|
|
a073fac47b | ||
|
|
4c2e9ddc82 | ||
|
|
ea8350bb61 |
@@ -11,6 +11,7 @@
|
||||
</div>
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Main Features](#main-features)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Setup](#setup)
|
||||
@@ -124,6 +125,7 @@ ssh-copy-id -i ./id_rsa.pub user@host
|
||||
> Source: https://github.com/appleboy/ssh-action#setting-up-a-ssh-key
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions! Please visit our Trello board for project management and roadmap details. You can contribute by:
|
||||
|
||||
- Creating issues for bugs, features, or discussions.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import CollectionsController from '#controllers/collections_controller';
|
||||
import LinksController from '#controllers/links_controller';
|
||||
import UsersController from '#controllers/users_controller';
|
||||
import User from '#models/user';
|
||||
import AuthController from '#auth/controllers/auth_controller';
|
||||
import CollectionsController from '#collections/controllers/collections_controller';
|
||||
import LinksController from '#links/controllers/links_controller';
|
||||
import User from '#user/models/user';
|
||||
import { inject } from '@adonisjs/core';
|
||||
import type { HttpContext } from '@adonisjs/core/http';
|
||||
import { HttpContext } from '@adonisjs/core/http';
|
||||
|
||||
class UserWithRelationCountDto {
|
||||
constructor(private user: User) {}
|
||||
@@ -26,7 +26,7 @@ class UserWithRelationCountDto {
|
||||
@inject()
|
||||
export default class AdminController {
|
||||
constructor(
|
||||
protected usersController: UsersController,
|
||||
protected usersController: AuthController,
|
||||
protected linksController: LinksController,
|
||||
protected collectionsController: CollectionsController
|
||||
) {}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { middleware } from '#start/kernel';
|
||||
import router from '@adonisjs/core/services/router';
|
||||
|
||||
const AdminController = () => import('#controllers/admin_controller');
|
||||
const AdminController = () => import('#admin/controllers/admin_controller');
|
||||
|
||||
/**
|
||||
* Routes for admin dashboard
|
||||
1
app/admin/routes/routes.ts
Normal file
1
app/admin/routes/routes.ts
Normal file
@@ -0,0 +1 @@
|
||||
import './admin_routes.js';
|
||||
@@ -1,10 +1,10 @@
|
||||
import User from '#models/user';
|
||||
import User from '#user/models/user';
|
||||
import type { HttpContext } from '@adonisjs/core/http';
|
||||
import logger from '@adonisjs/core/services/logger';
|
||||
import db from '@adonisjs/lucid/services/db';
|
||||
import { RouteName } from '@izzyjs/route/types';
|
||||
|
||||
export default class UsersController {
|
||||
export default class AuthController {
|
||||
private redirectTo: RouteName = 'auth';
|
||||
|
||||
login({ inertia }: HttpContext) {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { middleware } from '#start/kernel';
|
||||
import router from '@adonisjs/core/services/router';
|
||||
const UsersController = () => import('#controllers/users_controller');
|
||||
const AuthController = () => import('#auth/controllers/auth_controller');
|
||||
|
||||
const ROUTES_PREFIX = '/auth';
|
||||
|
||||
@@ -9,9 +9,9 @@ const ROUTES_PREFIX = '/auth';
|
||||
*/
|
||||
router
|
||||
.group(() => {
|
||||
router.get('/google', [UsersController, 'google']).as('auth');
|
||||
router.get('/google', [AuthController, 'google']).as('auth');
|
||||
router
|
||||
.get('/callback', [UsersController, 'callbackAuth'])
|
||||
.get('/callback', [AuthController, 'callbackAuth'])
|
||||
.as('auth.callback');
|
||||
})
|
||||
.prefix(ROUTES_PREFIX);
|
||||
@@ -21,7 +21,7 @@ router
|
||||
*/
|
||||
router
|
||||
.group(() => {
|
||||
router.get('/logout', [UsersController, 'logout']).as('auth.logout');
|
||||
router.get('/logout', [AuthController, 'logout']).as('auth.logout');
|
||||
})
|
||||
.middleware([middleware.auth()])
|
||||
.prefix(ROUTES_PREFIX);
|
||||
1
app/auth/routes/routes.ts
Normal file
1
app/auth/routes/routes.ts
Normal file
@@ -0,0 +1 @@
|
||||
import './auth_routes.js';
|
||||
@@ -1,10 +1,8 @@
|
||||
import Collection from '#models/collection';
|
||||
import User from '#models/user';
|
||||
import {
|
||||
createCollectionValidator,
|
||||
deleteCollectionValidator,
|
||||
updateCollectionValidator,
|
||||
} from '#validators/collection';
|
||||
import Collection from '#collections/models/collection';
|
||||
import { createCollectionValidator } from '#collections/validators/create_collection_validator';
|
||||
import { deleteCollectionValidator } from '#collections/validators/delete_collection_validator';
|
||||
import { updateCollectionValidator } from '#collections/validators/update_collection_validator';
|
||||
import User from '#user/models/user';
|
||||
import type { HttpContext } from '@adonisjs/core/http';
|
||||
import db from '@adonisjs/lucid/services/db';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import AppBaseModel from '#models/app_base_model';
|
||||
import Link from '#models/link';
|
||||
import User from '#models/user';
|
||||
import { Visibility } from '#collections/enums/visibility';
|
||||
import AppBaseModel from '#core/models/app_base_model';
|
||||
import Link from '#links/models/link';
|
||||
import User from '#user/models/user';
|
||||
import { belongsTo, column, hasMany } from '@adonisjs/lucid/orm';
|
||||
import type { BelongsTo, HasMany } from '@adonisjs/lucid/types/relations';
|
||||
import { Visibility } from '#enums/visibility';
|
||||
|
||||
export default class Collection extends AppBaseModel {
|
||||
@column()
|
||||
@@ -1,7 +1,7 @@
|
||||
import { middleware } from '#start/kernel';
|
||||
import router from '@adonisjs/core/services/router';
|
||||
const CollectionsController = () =>
|
||||
import('#controllers/collections_controller');
|
||||
import('#collections/controllers/collections_controller');
|
||||
|
||||
router
|
||||
.group(() => {
|
||||
1
app/collections/routes/routes.ts
Normal file
1
app/collections/routes/routes.ts
Normal file
@@ -0,0 +1 @@
|
||||
import './collections_routes.js';
|
||||
@@ -1,10 +1,6 @@
|
||||
import { Visibility } from '#enums/visibility';
|
||||
import { Visibility } from '#collections/enums/visibility';
|
||||
import vine, { SimpleMessagesProvider } from '@vinejs/vine';
|
||||
|
||||
const params = vine.object({
|
||||
id: vine.number(),
|
||||
});
|
||||
|
||||
export const createCollectionValidator = vine.compile(
|
||||
vine.object({
|
||||
name: vine.string().trim().minLength(1).maxLength(254),
|
||||
@@ -14,23 +10,6 @@ export const createCollectionValidator = vine.compile(
|
||||
})
|
||||
);
|
||||
|
||||
export const updateCollectionValidator = vine.compile(
|
||||
vine.object({
|
||||
name: vine.string().trim().minLength(1).maxLength(254),
|
||||
description: vine.string().trim().maxLength(254).nullable(),
|
||||
visibility: vine.enum(Visibility),
|
||||
nextId: vine.number().optional(),
|
||||
|
||||
params,
|
||||
})
|
||||
);
|
||||
|
||||
export const deleteCollectionValidator = vine.compile(
|
||||
vine.object({
|
||||
params,
|
||||
})
|
||||
);
|
||||
|
||||
createCollectionValidator.messagesProvider = new SimpleMessagesProvider({
|
||||
name: 'Collection name is required',
|
||||
'visibility.required': 'Collection visibiliy is required',
|
||||
@@ -0,0 +1,8 @@
|
||||
import { params } from '#core/validators/params_object';
|
||||
import vine from '@vinejs/vine';
|
||||
|
||||
export const deleteCollectionValidator = vine.compile(
|
||||
vine.object({
|
||||
params,
|
||||
})
|
||||
);
|
||||
14
app/collections/validators/update_collection_validator.ts
Normal file
14
app/collections/validators/update_collection_validator.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Visibility } from '#collections/enums/visibility';
|
||||
import { params } from '#core/validators/params_object';
|
||||
import vine from '@vinejs/vine';
|
||||
|
||||
export const updateCollectionValidator = vine.compile(
|
||||
vine.object({
|
||||
name: vine.string().trim().minLength(1).maxLength(254),
|
||||
description: vine.string().trim().maxLength(254).nullable(),
|
||||
visibility: vine.enum(Visibility),
|
||||
nextId: vine.number().optional(),
|
||||
|
||||
params,
|
||||
})
|
||||
);
|
||||
@@ -1,2 +0,0 @@
|
||||
export const PREFER_DARK_THEME = 'prefer_dark_theme';
|
||||
export const DARK_THEME_DEFAULT_VALUE = true;
|
||||
@@ -1,13 +0,0 @@
|
||||
import { PREFER_DARK_THEME } from '#constants/session';
|
||||
import { updateUserThemeValidator } from '#validators/user';
|
||||
import type { HttpContext } from '@adonisjs/core/http';
|
||||
|
||||
export default class AppsController {
|
||||
async updateUserTheme({ request, session, response }: HttpContext) {
|
||||
const { preferDarkTheme } = await request.validateUsing(
|
||||
updateUserThemeValidator
|
||||
);
|
||||
session.put(PREFER_DARK_THEME, preferDarkTheme);
|
||||
return response.ok({ message: 'ok' });
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
const PATHS = {
|
||||
AUTHOR: 'https://www.sonny.dev/?utm_source=mylinks',
|
||||
REPO_GITHUB: 'https://github.com/Sonny93/my-links',
|
||||
REPO_GITHUB: 'https://github.com/my-links/my-links',
|
||||
EXTENSION:
|
||||
'https://chromewebstore.google.com/detail/mylinks/agkmlplihacolkakgeccnbhphnepphma',
|
||||
} as const;
|
||||
18
app/core/middlewares/service_worker_scope_extender.ts
Normal file
18
app/core/middlewares/service_worker_scope_extender.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { HttpContext } from '@adonisjs/core/http';
|
||||
|
||||
const HEADER_NAME = 'Service-Worker-Allowed';
|
||||
|
||||
export default class ServiceWorkerScopeExtender {
|
||||
async handle(
|
||||
{ request, response, logger }: HttpContext,
|
||||
next: () => Promise<void>
|
||||
) {
|
||||
if (request.url().startsWith('/assets/sw.js')) {
|
||||
response.header(HEADER_NAME, '/');
|
||||
logger.debug(
|
||||
`Header ${HEADER_NAME} for ${request.url()} set to ${response.getHeader(HEADER_NAME)}`
|
||||
);
|
||||
}
|
||||
await next();
|
||||
}
|
||||
}
|
||||
5
app/core/validators/params_object.ts
Normal file
5
app/core/validators/params_object.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import vine from '@vinejs/vine';
|
||||
|
||||
export const params = vine.object({
|
||||
id: vine.number(),
|
||||
});
|
||||
34
app/favicons/controllers/favicons_controller.ts
Normal file
34
app/favicons/controllers/favicons_controller.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { CacheService } from '#favicons/services/cache_service';
|
||||
import { FaviconService } from '#favicons/services/favicons_service';
|
||||
import { Favicon } from '#favicons/types/favicon_type';
|
||||
import { inject } from '@adonisjs/core';
|
||||
import type { HttpContext } from '@adonisjs/core/http';
|
||||
|
||||
@inject()
|
||||
export default class FaviconsController {
|
||||
private faviconService: FaviconService;
|
||||
private cacheService: CacheService;
|
||||
|
||||
constructor(faviconService: FaviconService, cacheService: CacheService) {
|
||||
this.faviconService = faviconService;
|
||||
this.cacheService = cacheService;
|
||||
}
|
||||
|
||||
async index(ctx: HttpContext) {
|
||||
const url = ctx.request.qs()?.url;
|
||||
if (!url) {
|
||||
throw new Error('Missing URL');
|
||||
}
|
||||
|
||||
const favicon = await this.cacheService.getOrSetFavicon(url, () =>
|
||||
this.faviconService.getFavicon(url)
|
||||
);
|
||||
return this.sendImage(ctx, favicon);
|
||||
}
|
||||
|
||||
private sendImage(ctx: HttpContext, { buffer, type, size }: Favicon) {
|
||||
ctx.response.header('Content-Type', type);
|
||||
ctx.response.header('Content-Length', size.toString());
|
||||
ctx.response.send(buffer, true);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import router from '@adonisjs/core/services/router';
|
||||
const FaviconsController = () => import('#controllers/favicons_controller');
|
||||
const FaviconsController = () =>
|
||||
import('#favicons/controllers/favicons_controller');
|
||||
|
||||
/**
|
||||
* Favicon routes
|
||||
1
app/favicons/routes/routes.ts
Normal file
1
app/favicons/routes/routes.ts
Normal file
@@ -0,0 +1 @@
|
||||
import './favicons_routes.js';
|
||||
17
app/favicons/services/cache_service.ts
Normal file
17
app/favicons/services/cache_service.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { cache } from '#core/lib/cache';
|
||||
import { Favicon } from '#favicons/types/favicon_type';
|
||||
|
||||
export class CacheService {
|
||||
private cacheNs = cache.namespace('favicon');
|
||||
|
||||
async getOrSetFavicon(
|
||||
url: string,
|
||||
factory: () => Promise<Favicon>
|
||||
): Promise<Favicon> {
|
||||
return this.cacheNs.getOrSet({
|
||||
key: url,
|
||||
ttl: '1h',
|
||||
factory,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,9 @@
|
||||
import FaviconNotFoundException from '#exceptions/favicon_not_found_exception';
|
||||
import { cache } from '#lib/cache';
|
||||
import type { HttpContext } from '@adonisjs/core/http';
|
||||
import FaviconNotFoundException from '#favicons/exceptions/favicon_not_found_exception';
|
||||
import { Favicon } from '#favicons/types/favicon_type';
|
||||
import logger from '@adonisjs/core/services/logger';
|
||||
import { parse } from 'node-html-parser';
|
||||
|
||||
interface Favicon {
|
||||
buffer: Buffer;
|
||||
url: string;
|
||||
type: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export default class FaviconsController {
|
||||
export class FaviconService {
|
||||
private userAgent =
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0';
|
||||
private relList = [
|
||||
@@ -24,27 +16,11 @@ export default class FaviconsController {
|
||||
'fluid-icon',
|
||||
];
|
||||
|
||||
async index(ctx: HttpContext) {
|
||||
const url = ctx.request.qs()?.url;
|
||||
if (!url) {
|
||||
throw new Error('Missing URL');
|
||||
}
|
||||
|
||||
const cacheNs = cache.namespace('favicon');
|
||||
const favicon = await cacheNs.getOrSet({
|
||||
key: url,
|
||||
ttl: '1h',
|
||||
factory: () => this.tryGetFavicon(url),
|
||||
});
|
||||
return this.sendImage(ctx, favicon);
|
||||
}
|
||||
|
||||
private async tryGetFavicon(url: string): Promise<Favicon> {
|
||||
const faviconUrl = this.buildFaviconUrl(url, '/favicon.ico');
|
||||
async getFavicon(url: string): Promise<Favicon> {
|
||||
try {
|
||||
return await this.fetchFavicon(faviconUrl);
|
||||
return await this.fetchFavicon(this.buildFaviconUrl(url, '/favicon.ico'));
|
||||
} catch {
|
||||
logger.debug(`Unable to retrieve favicon from ${faviconUrl}`);
|
||||
logger.debug(`Unable to retrieve favicon from ${url}/favicon.ico`);
|
||||
}
|
||||
|
||||
const documentText = await this.fetchDocumentText(url);
|
||||
@@ -54,15 +30,7 @@ export default class FaviconsController {
|
||||
throw new FaviconNotFoundException(`No favicon path found in ${url}`);
|
||||
}
|
||||
|
||||
if (faviconPath.startsWith('http')) {
|
||||
try {
|
||||
return await this.fetchFavicon(faviconPath);
|
||||
} catch {
|
||||
logger.debug(`Unable to retrieve favicon from ${faviconPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
return this.fetchFaviconFromPath(url, faviconPath);
|
||||
return await this.fetchFaviconFromPath(url, faviconPath);
|
||||
}
|
||||
|
||||
private async fetchFavicon(url: string): Promise<Favicon> {
|
||||
@@ -127,7 +95,9 @@ export default class FaviconsController {
|
||||
|
||||
const basePath = this.urlWithoutSearchParams(base);
|
||||
const baseUrl = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
|
||||
return `${baseUrl}/${path}`;
|
||||
const finalUrl = `${baseUrl}/${path}`;
|
||||
logger.debug(`Built favicon URL: ${finalUrl}`);
|
||||
return finalUrl;
|
||||
}
|
||||
|
||||
private urlWithoutSearchParams(url: string): string {
|
||||
@@ -151,10 +121,4 @@ export default class FaviconsController {
|
||||
const headers = new Headers({ 'User-Agent': this.userAgent });
|
||||
return fetch(url, { headers });
|
||||
}
|
||||
|
||||
private sendImage(ctx: HttpContext, { buffer, type, size }: Favicon) {
|
||||
ctx.response.header('Content-Type', type);
|
||||
ctx.response.header('Content-Length', size.toString());
|
||||
ctx.response.send(buffer, true);
|
||||
}
|
||||
}
|
||||
6
app/favicons/types/favicon_type.ts
Normal file
6
app/favicons/types/favicon_type.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type Favicon = {
|
||||
buffer: Buffer;
|
||||
url: string;
|
||||
type: string;
|
||||
size: number;
|
||||
};
|
||||
3
app/home/routes/home_routes.ts
Normal file
3
app/home/routes/home_routes.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import router from '@adonisjs/core/services/router';
|
||||
|
||||
router.on('/').renderInertia('home').as('home');
|
||||
1
app/home/routes/routes.ts
Normal file
1
app/home/routes/routes.ts
Normal file
@@ -0,0 +1 @@
|
||||
import './home_routes.js';
|
||||
4
app/legal/routes/legal_routes.ts
Normal file
4
app/legal/routes/legal_routes.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import router from '@adonisjs/core/services/router';
|
||||
|
||||
router.on('/terms').renderInertia('terms').as('terms');
|
||||
router.on('/privacy').renderInertia('privacy').as('privacy');
|
||||
1
app/legal/routes/routes.ts
Normal file
1
app/legal/routes/routes.ts
Normal file
@@ -0,0 +1 @@
|
||||
import './legal_routes.js';
|
||||
@@ -1,13 +1,11 @@
|
||||
import CollectionsController from '#controllers/collections_controller';
|
||||
import Link from '#models/link';
|
||||
import {
|
||||
createLinkValidator,
|
||||
deleteLinkValidator,
|
||||
updateLinkFavoriteStatusValidator,
|
||||
updateLinkValidator,
|
||||
} from '#validators/link';
|
||||
import CollectionsController from '#collections/controllers/collections_controller';
|
||||
import Link from '#links/models/link';
|
||||
import { createLinkValidator } from '#links/validators/create_link_validator';
|
||||
import { deleteLinkValidator } from '#links/validators/delete_link_validator';
|
||||
import { updateLinkFavoriteStatusValidator } from '#links/validators/update_favorite_link_validator';
|
||||
import { updateLinkValidator } from '#links/validators/update_link_validator';
|
||||
import { inject } from '@adonisjs/core';
|
||||
import type { HttpContext } from '@adonisjs/core/http';
|
||||
import { HttpContext } from '@adonisjs/core/http';
|
||||
import db from '@adonisjs/lucid/services/db';
|
||||
|
||||
@inject()
|
||||
@@ -1,6 +1,6 @@
|
||||
import AppBaseModel from '#models/app_base_model';
|
||||
import Collection from '#models/collection';
|
||||
import User from '#models/user';
|
||||
import Collection from '#collections/models/collection';
|
||||
import AppBaseModel from '#core/models/app_base_model';
|
||||
import User from '#user/models/user';
|
||||
import { belongsTo, column } from '@adonisjs/lucid/orm';
|
||||
import type { BelongsTo } from '@adonisjs/lucid/types/relations';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { middleware } from '#start/kernel';
|
||||
import router from '@adonisjs/core/services/router';
|
||||
const LinksController = () => import('#controllers/links_controller');
|
||||
const LinksController = () => import('#links/controllers/links_controller');
|
||||
|
||||
/**
|
||||
* Routes for authenticated users
|
||||
1
app/links/routes/routes.ts
Normal file
1
app/links/routes/routes.ts
Normal file
@@ -0,0 +1 @@
|
||||
import './links_routes.js';
|
||||
11
app/links/validators/create_link_validator.ts
Normal file
11
app/links/validators/create_link_validator.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import vine from '@vinejs/vine';
|
||||
|
||||
export const createLinkValidator = vine.compile(
|
||||
vine.object({
|
||||
name: vine.string().trim().minLength(1).maxLength(254),
|
||||
description: vine.string().trim().maxLength(300).optional(),
|
||||
url: vine.string().trim(),
|
||||
favorite: vine.boolean(),
|
||||
collectionId: vine.number(),
|
||||
})
|
||||
);
|
||||
8
app/links/validators/delete_link_validator.ts
Normal file
8
app/links/validators/delete_link_validator.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { params } from '#core/validators/params_object';
|
||||
import vine from '@vinejs/vine';
|
||||
|
||||
export const deleteLinkValidator = vine.compile(
|
||||
vine.object({
|
||||
params,
|
||||
})
|
||||
);
|
||||
10
app/links/validators/update_favorite_link_validator.ts
Normal file
10
app/links/validators/update_favorite_link_validator.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { params } from '#core/validators/params_object';
|
||||
import vine from '@vinejs/vine';
|
||||
|
||||
export const updateLinkFavoriteStatusValidator = vine.compile(
|
||||
vine.object({
|
||||
favorite: vine.boolean(),
|
||||
|
||||
params,
|
||||
})
|
||||
);
|
||||
14
app/links/validators/update_link_validator.ts
Normal file
14
app/links/validators/update_link_validator.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { params } from '#core/validators/params_object';
|
||||
import vine from '@vinejs/vine';
|
||||
|
||||
export const updateLinkValidator = vine.compile(
|
||||
vine.object({
|
||||
name: vine.string().trim().minLength(1).maxLength(254),
|
||||
description: vine.string().trim().maxLength(300).optional(),
|
||||
url: vine.string().trim(),
|
||||
favorite: vine.boolean(),
|
||||
collectionId: vine.number(),
|
||||
|
||||
params,
|
||||
})
|
||||
);
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { HttpContext } from '@adonisjs/core/http';
|
||||
import db from '@adonisjs/lucid/services/db';
|
||||
|
||||
export default class SearchesController {
|
||||
export default class SearchController {
|
||||
async search({ request, auth }: HttpContext) {
|
||||
const term = request.qs()?.term;
|
||||
if (!term) {
|
||||
1
app/search/routes/routes.ts
Normal file
1
app/search/routes/routes.ts
Normal file
@@ -0,0 +1 @@
|
||||
import './search_routes.js';
|
||||
@@ -1,7 +1,8 @@
|
||||
import { middleware } from '#start/kernel';
|
||||
import router from '@adonisjs/core/services/router';
|
||||
|
||||
const SearchesController = () => import('#controllers/searches_controller');
|
||||
const SearchesController = () =>
|
||||
import('#search/controllers/search_controller');
|
||||
|
||||
/**
|
||||
* Search routes
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Visibility } from '#enums/visibility';
|
||||
import Collection from '#models/collection';
|
||||
import Link from '#models/link';
|
||||
import User from '#models/user';
|
||||
import { getSharedCollectionValidator } from '#validators/shared_collection';
|
||||
import { Visibility } from '#collections/enums/visibility';
|
||||
import Collection from '#collections/models/collection';
|
||||
import Link from '#links/models/link';
|
||||
import User from '#user/models/user';
|
||||
import type { HttpContext } from '@adonisjs/core/http';
|
||||
import { getSharedCollectionValidator } from '../validators/shared_collection.js';
|
||||
|
||||
class LinkWithoutFavoriteDto {
|
||||
constructor(private link: Link) {}
|
||||
1
app/shared_collections/routes/routes.ts
Normal file
1
app/shared_collections/routes/routes.ts
Normal file
@@ -0,0 +1 @@
|
||||
import './shared_collections_routes.js';
|
||||
@@ -1,6 +1,6 @@
|
||||
import router from '@adonisjs/core/services/router';
|
||||
|
||||
const SharedCollectionsController = () =>
|
||||
import('#controllers/shared_collections_controller');
|
||||
import('#shared_collections/controllers/shared_collections_controller');
|
||||
|
||||
router.get('/shared/:id', [SharedCollectionsController, 'index']).as('shared');
|
||||
@@ -1,9 +1,6 @@
|
||||
import { params } from '#core/validators/params_object';
|
||||
import vine from '@vinejs/vine';
|
||||
|
||||
const params = vine.object({
|
||||
id: vine.number(),
|
||||
});
|
||||
|
||||
export const getSharedCollectionValidator = vine.compile(
|
||||
vine.object({
|
||||
params,
|
||||
3
app/user/constants/theme.ts
Normal file
3
app/user/constants/theme.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const KEY_USER_THEME = 'user_theme';
|
||||
export const THEMES = ['dark', 'light'] as const;
|
||||
export const DEFAULT_USER_THEME = THEMES.at(0);
|
||||
11
app/user/controllers/theme_controller.ts
Normal file
11
app/user/controllers/theme_controller.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { KEY_USER_THEME } from '#user/constants/theme';
|
||||
import { updateThemeValidator } from '#user/validators/update_theme_validator';
|
||||
import type { HttpContext } from '@adonisjs/core/http';
|
||||
|
||||
export default class ThemeController {
|
||||
async index({ request, session, response }: HttpContext) {
|
||||
const { theme } = await request.validateUsing(updateThemeValidator);
|
||||
session.put(KEY_USER_THEME, theme);
|
||||
return response.ok({ message: 'ok' });
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import Collection from '#models/collection';
|
||||
import Link from '#models/link';
|
||||
import Collection from '#collections/models/collection';
|
||||
import AppBaseModel from '#core/models/app_base_model';
|
||||
import Link from '#links/models/link';
|
||||
import type { GoogleToken } from '@adonisjs/ally/types';
|
||||
import { column, computed, hasMany } from '@adonisjs/lucid/orm';
|
||||
import type { HasMany } from '@adonisjs/lucid/types/relations';
|
||||
import AppBaseModel from './app_base_model.js';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
export default class User extends AppBaseModel {
|
||||
1
app/user/routes/routes.ts
Normal file
1
app/user/routes/routes.ts
Normal file
@@ -0,0 +1 @@
|
||||
import './user_theme_route.js';
|
||||
5
app/user/routes/user_theme_route.ts
Normal file
5
app/user/routes/user_theme_route.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import router from '@adonisjs/core/services/router';
|
||||
|
||||
const ThemeController = () => import('#user/controllers/theme_controller');
|
||||
|
||||
router.post('/user/theme', [ThemeController, 'index']).as('user.theme');
|
||||
8
app/user/validators/update_theme_validator.ts
Normal file
8
app/user/validators/update_theme_validator.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { THEMES } from '#user/constants/theme';
|
||||
import vine from '@vinejs/vine';
|
||||
|
||||
export const updateThemeValidator = vine.compile(
|
||||
vine.object({
|
||||
theme: vine.enum(THEMES),
|
||||
})
|
||||
);
|
||||
@@ -1,43 +0,0 @@
|
||||
import vine from '@vinejs/vine';
|
||||
|
||||
const params = vine.object({
|
||||
id: vine.number(),
|
||||
});
|
||||
|
||||
export const createLinkValidator = vine.compile(
|
||||
vine.object({
|
||||
name: vine.string().trim().minLength(1).maxLength(254),
|
||||
description: vine.string().trim().maxLength(300).optional(),
|
||||
url: vine.string().trim(),
|
||||
favorite: vine.boolean(),
|
||||
collectionId: vine.number(),
|
||||
})
|
||||
);
|
||||
|
||||
export const updateLinkValidator = vine.compile(
|
||||
vine.object({
|
||||
name: vine.string().trim().minLength(1).maxLength(254),
|
||||
description: vine.string().trim().maxLength(300).optional(),
|
||||
url: vine.string().trim(),
|
||||
favorite: vine.boolean(),
|
||||
collectionId: vine.number(),
|
||||
|
||||
params,
|
||||
})
|
||||
);
|
||||
|
||||
export const deleteLinkValidator = vine.compile(
|
||||
vine.object({
|
||||
params,
|
||||
})
|
||||
);
|
||||
|
||||
export const updateLinkFavoriteStatusValidator = vine.compile(
|
||||
vine.object({
|
||||
favorite: vine.boolean(),
|
||||
|
||||
params: vine.object({
|
||||
id: vine.number(),
|
||||
}),
|
||||
})
|
||||
);
|
||||
@@ -1,7 +0,0 @@
|
||||
import vine from '@vinejs/vine';
|
||||
|
||||
export const updateUserThemeValidator = vine.compile(
|
||||
vine.object({
|
||||
preferDarkTheme: vine.boolean(),
|
||||
})
|
||||
);
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineConfig } from '@adonisjs/auth';
|
||||
import { InferAuthEvents, Authenticators } from '@adonisjs/auth/types';
|
||||
import { sessionGuard, sessionUserProvider } from '@adonisjs/auth/session';
|
||||
import { Authenticators, InferAuthEvents } from '@adonisjs/auth/types';
|
||||
|
||||
const authConfig = defineConfig({
|
||||
default: 'web',
|
||||
@@ -8,7 +8,7 @@ const authConfig = defineConfig({
|
||||
web: sessionGuard({
|
||||
useRememberMeTokens: false,
|
||||
provider: sessionUserProvider({
|
||||
model: () => import('#models/user'),
|
||||
model: () => import('#user/models/user'),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
DARK_THEME_DEFAULT_VALUE,
|
||||
PREFER_DARK_THEME,
|
||||
} from '#constants/session';
|
||||
import { isSSREnableForPage } from '#config/ssr';
|
||||
import { DEFAULT_USER_THEME, KEY_USER_THEME } from '#user/constants/theme';
|
||||
import logger from '@adonisjs/core/services/logger';
|
||||
import { defineConfig } from '@adonisjs/inertia';
|
||||
|
||||
export default defineConfig({
|
||||
@@ -15,8 +14,9 @@ export default defineConfig({
|
||||
*/
|
||||
sharedData: {
|
||||
errors: (ctx) => ctx.session?.flashMessages.get('errors'),
|
||||
preferDarkTheme: (ctx) =>
|
||||
ctx.session?.get(PREFER_DARK_THEME, DARK_THEME_DEFAULT_VALUE),
|
||||
user: (ctx) => ({
|
||||
theme: ctx.session?.get(KEY_USER_THEME, DEFAULT_USER_THEME),
|
||||
}),
|
||||
auth: async (ctx) => {
|
||||
await ctx.auth?.check();
|
||||
return {
|
||||
@@ -32,5 +32,10 @@ export default defineConfig({
|
||||
ssr: {
|
||||
enabled: true,
|
||||
entrypoint: 'inertia/app/ssr.tsx',
|
||||
pages: (_, page) => {
|
||||
const ssrEnabled = isSSREnableForPage(page);
|
||||
logger.debug(`Page "${page}" SSR enabled: ${ssrEnabled}`);
|
||||
return ssrEnabled;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
12
config/project.ts
Normal file
12
config/project.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
const PROJECT_NAME = 'MyLinks';
|
||||
const PROJECT_DESCRIPTION =
|
||||
'Another bookmark manager that lets you manage and share your favorite links in an intuitive interface';
|
||||
const PROJECT_URL = 'https://www.mylinks.app';
|
||||
const APP_COLOR = '#f0eef6';
|
||||
|
||||
export default {
|
||||
name: PROJECT_NAME,
|
||||
description: PROJECT_DESCRIPTION,
|
||||
url: PROJECT_URL,
|
||||
color: APP_COLOR,
|
||||
};
|
||||
2
config/ssr.ts
Normal file
2
config/ssr.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const CSR_ROUTES = ['dashboard'];
|
||||
export const isSSREnableForPage = (page: string) => !CSR_ROUTES.includes(page);
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Visibility } from '#collections/enums/visibility';
|
||||
import { defaultTableFields } from '#database/default_table_fields';
|
||||
import { Visibility } from '#enums/visibility';
|
||||
import { BaseSchema } from '@adonisjs/lucid/schema';
|
||||
|
||||
export default class CreateCollectionTable extends BaseSchema {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Visibility } from '#enums/visibility';
|
||||
import Collection from '#models/collection';
|
||||
import User from '#models/user';
|
||||
import { Visibility } from '#collections/enums/visibility';
|
||||
import Collection from '#collections/models/collection';
|
||||
import User from '#user/models/user';
|
||||
import { BaseSeeder } from '@adonisjs/lucid/seeders';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Collection from '#collections/models/collection';
|
||||
import { getUserIds } from '#database/seeders/collection_seeder';
|
||||
import Collection from '#models/collection';
|
||||
import Link from '#models/link';
|
||||
import User from '#models/user';
|
||||
import Link from '#links/models/link';
|
||||
import User from '#user/models/user';
|
||||
import { BaseSeeder } from '@adonisjs/lucid/seeders';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import User from '#models/user';
|
||||
import User from '#user/models/user';
|
||||
import { GoogleToken } from '@adonisjs/ally/types';
|
||||
import { BaseSeeder } from '@adonisjs/lucid/seeders';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
|
Before Width: | Height: | Size: 957 B After Width: | Height: | Size: 957 B |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
@@ -1,8 +1,9 @@
|
||||
import { resolvePageComponent } from '@adonisjs/inertia/helpers';
|
||||
import { createInertiaApp } from '@inertiajs/react';
|
||||
import { isSSREnableForPage } from 'config-ssr';
|
||||
import 'dayjs/locale/en';
|
||||
import 'dayjs/locale/fr';
|
||||
import { hydrateRoot } from 'react-dom/client';
|
||||
import { createRoot, hydrateRoot } from 'react-dom/client';
|
||||
import '../i18n/index';
|
||||
|
||||
const appName = import.meta.env.VITE_APP_NAME || 'MyLinks';
|
||||
@@ -20,6 +21,13 @@ createInertiaApp({
|
||||
},
|
||||
|
||||
setup({ el, App, props }) {
|
||||
const componentName = props.initialPage.component;
|
||||
const isSSREnabled = isSSREnableForPage(componentName);
|
||||
console.debug(`Page "${componentName}" SSR enabled: ${isSSREnabled}`);
|
||||
if (isSSREnabled) {
|
||||
hydrateRoot(el, <App {...props} />);
|
||||
} else {
|
||||
createRoot(el).render(<App {...props} />);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@mantine/core';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { useState } from 'react';
|
||||
import { ChangeEvent, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TbSearch } from 'react-icons/tb';
|
||||
import { Th } from '~/components/admin/users/th';
|
||||
@@ -25,6 +25,11 @@ export type UserWithCounts = User & {
|
||||
};
|
||||
export type UsersWithCounts = UserWithCounts[];
|
||||
|
||||
export type Columns = keyof UserWithCounts;
|
||||
|
||||
const DEFAULT_SORT_BY: Columns = 'lastSeenAt';
|
||||
const DEFAULT_SORT_DIRECTION = true;
|
||||
|
||||
export interface UsersTableProps {
|
||||
users: UsersWithCounts;
|
||||
totalCollections: number;
|
||||
@@ -37,10 +42,19 @@ export function UsersTable({
|
||||
totalLinks,
|
||||
}: UsersTableProps) {
|
||||
const { t } = useTranslation();
|
||||
const [search, setSearch] = useState('');
|
||||
const [sortedData, setSortedData] = useState(users);
|
||||
const [sortBy, setSortBy] = useState<keyof UserWithCounts | null>(null);
|
||||
const [reverseSortDirection, setReverseSortDirection] = useState(false);
|
||||
|
||||
const [search, setSearch] = useState<string>('');
|
||||
const [sortBy, setSortBy] = useState<Columns | null>(DEFAULT_SORT_BY);
|
||||
const [reverseSortDirection, setReverseSortDirection] = useState(
|
||||
DEFAULT_SORT_DIRECTION
|
||||
);
|
||||
const [sortedData, setSortedData] = useState(() =>
|
||||
sortData(users, {
|
||||
sortBy: sortBy,
|
||||
reversed: reverseSortDirection,
|
||||
search: '',
|
||||
})
|
||||
);
|
||||
|
||||
const setSorting = (field: keyof UserWithCounts) => {
|
||||
const reversed = field === sortBy ? !reverseSortDirection : false;
|
||||
@@ -49,7 +63,7 @@ export function UsersTable({
|
||||
setSortedData(sortData(users, { sortBy: field, reversed, search }));
|
||||
};
|
||||
|
||||
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleSearchChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = event.currentTarget;
|
||||
setSearch(value);
|
||||
setSortedData(
|
||||
@@ -83,7 +97,7 @@ export function UsersTable({
|
||||
return (
|
||||
<ScrollArea>
|
||||
<TextInput
|
||||
placeholder="Search by any field"
|
||||
placeholder={`Search by any field (${users.length} users)`}
|
||||
mb="md"
|
||||
leftSection={<TbSearch style={{ width: rem(16), height: rem(16) }} />}
|
||||
value={search}
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import { ActionIcon, useMantineColorScheme } from '@mantine/core';
|
||||
import { TbMoonStars, TbSun } from 'react-icons/tb';
|
||||
import { makeRequest } from '~/lib/request';
|
||||
|
||||
export function MantineThemeSwitcher() {
|
||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||
const handleThemeChange = () => {
|
||||
toggleColorScheme();
|
||||
makeRequest({
|
||||
url: '/user/theme',
|
||||
method: 'POST',
|
||||
body: { theme: colorScheme === 'dark' ? 'light' : 'dark' },
|
||||
});
|
||||
};
|
||||
return (
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
aria-label="Toggle color scheme"
|
||||
onClick={() => toggleColorScheme()}
|
||||
onClick={handleThemeChange}
|
||||
size="lg"
|
||||
>
|
||||
{colorScheme === 'dark' ? <TbSun /> : <TbMoonStars />}
|
||||
|
||||
@@ -33,13 +33,14 @@ export default function CollectionItem({
|
||||
href={appendCollectionId(route('dashboard').path, collection.id)}
|
||||
key={collection.id}
|
||||
ref={itemRef}
|
||||
title={collection.name}
|
||||
>
|
||||
<FolderIcon className={classes.linkIcon} />
|
||||
<Text lineClamp={1} maw={showLinks ? '160px' : '200px'}>
|
||||
<Text lineClamp={1} maw={'200px'} style={{ wordBreak: 'break-all' }}>
|
||||
{collection.name}
|
||||
</Text>
|
||||
{showLinks && (
|
||||
<Text c="var(--mantine-color-gray-5)" ml="xs">
|
||||
<Text style={{ whiteSpace: 'nowrap' }} c="dimmed" ml="sm">
|
||||
— {linksCount}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
}
|
||||
|
||||
.collectionList {
|
||||
padding: 1px;
|
||||
padding-right: 5px;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 0.35em;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Visibility } from '#enums/visibility';
|
||||
import { Link } from '@inertiajs/react';
|
||||
import { route } from '@izzyjs/route/client';
|
||||
import {
|
||||
ActionIcon,
|
||||
AppShell,
|
||||
Badge,
|
||||
Box,
|
||||
Burger,
|
||||
Flex,
|
||||
Group,
|
||||
Menu,
|
||||
Text,
|
||||
@@ -19,6 +18,7 @@ import { IoTrashOutline } from 'react-icons/io5';
|
||||
import { ShareCollection } from '~/components/share/share_collection';
|
||||
import { appendCollectionId } from '~/lib/navigation';
|
||||
import { useActiveCollection } from '~/stores/collection_store';
|
||||
import { Visibility } from '~/types/app';
|
||||
|
||||
interface DashboardHeaderProps {
|
||||
navbar: {
|
||||
@@ -35,15 +35,15 @@ export function DashboardHeader({ navbar, aside }: DashboardHeaderProps) {
|
||||
const { activeCollection } = useActiveCollection();
|
||||
return (
|
||||
<AppShell.Header style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Group justify="space-between" px="md" flex={1}>
|
||||
<Group h="100%">
|
||||
<Group justify="space-between" px="md" flex={1} wrap="nowrap">
|
||||
<Group h="100%" wrap="nowrap">
|
||||
<Burger
|
||||
opened={navbar.opened}
|
||||
onClick={navbar.toggle}
|
||||
hiddenFrom="sm"
|
||||
size="sm"
|
||||
/>
|
||||
<Flex direction="column">
|
||||
<Box>
|
||||
<Text lineClamp={1}>
|
||||
{activeCollection?.name}{' '}
|
||||
{activeCollection?.visibility === Visibility.PUBLIC && (
|
||||
@@ -55,9 +55,9 @@ export function DashboardHeader({ navbar, aside }: DashboardHeaderProps) {
|
||||
{activeCollection.description}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
</Group>
|
||||
<Group>
|
||||
<Group wrap="nowrap">
|
||||
<ShareCollection />
|
||||
|
||||
<Menu withinPortal shadow="md" width={225}>
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
.sideMenu {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 0.35em;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.listContainer {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.collectionList {
|
||||
padding: 1px;
|
||||
padding-right: 5px;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 0.35em;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.noFavorite {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Box, Group, ScrollArea, Stack, Text } from '@mantine/core';
|
||||
import { Flex, Group, Stack, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FavoriteItem } from '~/components/dashboard/favorite/item/favorite_item';
|
||||
import { useFavorites } from '~/stores/collection_store';
|
||||
import styles from './favorite_list.module.css';
|
||||
|
||||
export function FavoriteList() {
|
||||
const { t } = useTranslation('common');
|
||||
@@ -19,21 +18,15 @@ export function FavoriteList() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className={styles.sideMenu}>
|
||||
<Box className={styles.listContainer}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Flex direction="column">
|
||||
<Text c="dimmed" mt="xs" ml="md" mb={4}>
|
||||
{t('favorite')} • {favorites.length}
|
||||
</Text>
|
||||
<ScrollArea className={styles.collectionList}>
|
||||
<Stack gap={4}>
|
||||
{favorites.map((link) => (
|
||||
<FavoriteItem link={link} key={link.id} />
|
||||
))}
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,43 +2,11 @@
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
background-color: light-dark(var(--mantine-color-gray-1), rgb(50, 58, 71));
|
||||
background-color: light-dark(var(--mantine-color-white), rgb(50, 58, 71));
|
||||
padding: 0.25em 0.5em !important;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.linkWrapper:hover {
|
||||
border: 1px solid var(--mantine-color-blue-4);
|
||||
}
|
||||
|
||||
.linkName {
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.linkDescription {
|
||||
margin-top: 0.5em;
|
||||
font-size: 0.8em;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.linkUrl {
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
font-size: 0.8em;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.linkWrapper:hover .linkUrlPathname {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.linkUrlPathname {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Card, Group, Text } from '@mantine/core';
|
||||
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';
|
||||
@@ -6,18 +6,26 @@ import { LinkWithCollection } from '~/types/app';
|
||||
import styles from './favorite_item.module.css';
|
||||
|
||||
export const FavoriteItem = ({ link }: { link: LinkWithCollection }) => (
|
||||
<Card className={styles.linkWrapper} radius="sm" withBorder>
|
||||
<Group justify="center" gap="xs">
|
||||
<LinkFavicon size={32} url={link.url} />
|
||||
<ExternalLinkStyled href={link.url} style={{ flex: 1 }}>
|
||||
<div className={styles.linkName}>
|
||||
<Text lineClamp={1}>{link.name} </Text>
|
||||
</div>
|
||||
<Text c="gray" size="xs" mb={4} lineClamp={1}>
|
||||
<ExternalLinkStyled href={link.url} title={link.url}>
|
||||
<Card className={styles.linkWrapper}>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<LinkFavicon url={link.url} />
|
||||
<Flex style={{ width: '100%' }} direction="column">
|
||||
<Text lineClamp={1} c="blue">
|
||||
{link.name}
|
||||
</Text>
|
||||
<Text
|
||||
c="gray"
|
||||
size="xs"
|
||||
mb={4}
|
||||
lineClamp={1}
|
||||
style={{ wordBreak: 'break-all' }}
|
||||
>
|
||||
{link.collection.name}
|
||||
</Text>
|
||||
</ExternalLinkStyled>
|
||||
</Flex>
|
||||
<LinkControls link={link} showGoToCollection />
|
||||
</Group>
|
||||
</Card>
|
||||
</ExternalLinkStyled>
|
||||
);
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
background-color: light-dark(var(--mantine-color-gray-1), rgb(50, 58, 71));
|
||||
padding: 0.75em 1em;
|
||||
border-radius: var(--border-radius);
|
||||
background-color: light-dark(var(--mantine-color-white), rgb(50, 58, 71));
|
||||
padding: 0.5em 0.75em !important;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
@@ -12,32 +11,8 @@
|
||||
border: 1px solid var(--mantine-color-blue-4);
|
||||
}
|
||||
|
||||
.linkHeader {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.linkName {
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.linkDescription {
|
||||
margin-top: 0.5em;
|
||||
font-size: 0.8em;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.linkUrl {
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
font-size: 0.8em;
|
||||
transition: opacity 0.3s;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.linkWrapper:hover .linkUrlPathname {
|
||||
@@ -46,5 +21,5 @@
|
||||
|
||||
.linkUrlPathname {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Link as InertiaLink } from '@inertiajs/react';
|
||||
import { route } from '@izzyjs/route/client';
|
||||
import { ActionIcon, Menu } from '@mantine/core';
|
||||
import { MouseEvent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BsThreeDotsVertical } from 'react-icons/bs';
|
||||
import { FaRegEye } from 'react-icons/fa';
|
||||
@@ -24,10 +25,17 @@ export default function LinkControls({
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
const onFavoriteCallback = () => toggleFavorite(link.id);
|
||||
const handleStopPropagation = (event: MouseEvent<HTMLButtonElement>) =>
|
||||
event.preventDefault();
|
||||
|
||||
return (
|
||||
<Menu withinPortal shadow="md" width={200}>
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="subtle" color="var(--mantine-color-text)">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="var(--mantine-color-text)"
|
||||
onClick={handleStopPropagation}
|
||||
>
|
||||
<BsThreeDotsVertical />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Card, Group, Text } from '@mantine/core';
|
||||
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';
|
||||
@@ -15,48 +15,35 @@ export function LinkItem({ link, hideMenu: hideMenu = false }: LinkItemProps) {
|
||||
const { name, url, description } = link;
|
||||
const showFavoriteIcon = !hideMenu && 'favorite' in link && link.favorite;
|
||||
return (
|
||||
<Card className={styles.linkWrapper} padding="sm" radius="sm" withBorder>
|
||||
<Group className={styles.linkHeader} justify="center">
|
||||
<ExternalLinkStyled href={url} title={url}>
|
||||
<Card className={styles.linkWrapper}>
|
||||
<Group justify="center" wrap="nowrap">
|
||||
<LinkFavicon url={url} />
|
||||
<ExternalLinkStyled href={url} style={{ flex: 1 }}>
|
||||
<div className={styles.linkName}>
|
||||
<Text lineClamp={1}>
|
||||
<Flex style={{ width: '100%' }} direction="column">
|
||||
<Text lineClamp={1} c="blue">
|
||||
{name} {showFavoriteIcon && <AiFillStar color="gold" />}
|
||||
</Text>
|
||||
</div>
|
||||
<LinkItemURL url={url} />
|
||||
</ExternalLinkStyled>
|
||||
</Flex>
|
||||
{!hideMenu && <LinkControls link={link} />}
|
||||
</Group>
|
||||
{description && (
|
||||
<Text className={styles.linkDescription} c="dimmed" size="sm">
|
||||
<Text c="dimmed" size="sm" mt="xs" lineClamp={3}>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Card>
|
||||
</ExternalLinkStyled>
|
||||
);
|
||||
}
|
||||
|
||||
function LinkItemURL({ url }: { url: Link['url'] }) {
|
||||
try {
|
||||
const { origin, pathname, search } = new URL(url);
|
||||
let text = '';
|
||||
|
||||
if (pathname !== '/') {
|
||||
text += pathname;
|
||||
}
|
||||
|
||||
if (search !== '') {
|
||||
if (text === '') {
|
||||
text += '/';
|
||||
}
|
||||
text += search;
|
||||
}
|
||||
|
||||
const { origin, pathname } = new URL(url);
|
||||
return (
|
||||
<Text className={styles.linkUrl} c="gray" size="xs" lineClamp={1}>
|
||||
{origin}
|
||||
<span className={styles.linkUrlPathname}>{text}</span>
|
||||
{pathname !== '/' && pathname}
|
||||
</Text>
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import PATHS from '#constants/paths';
|
||||
import PATHS from '#core/constants/paths';
|
||||
import { Link } from '@inertiajs/react';
|
||||
import { route } from '@izzyjs/route/client';
|
||||
import { Anchor, Group, Text } from '@mantine/core';
|
||||
@@ -13,15 +13,16 @@ export function MantineFooter() {
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
const links = [
|
||||
{ link: route('privacy').url, label: t('privacy') },
|
||||
{ link: route('terms').url, label: t('terms') },
|
||||
{ link: PATHS.EXTENSION, label: 'Extension' },
|
||||
{ link: route('privacy').path, label: t('privacy'), external: false },
|
||||
{ link: route('terms').path, label: t('terms'), external: false },
|
||||
{ link: PATHS.EXTENSION, label: 'Extension', external: true },
|
||||
];
|
||||
|
||||
const items = links.map((link) => (
|
||||
<Anchor
|
||||
c="dimmed"
|
||||
component={Link}
|
||||
// @ts-expect-error
|
||||
component={link.external ? ExternalLink : Link}
|
||||
key={link.label}
|
||||
href={link.link}
|
||||
size="sm"
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Visibility } from '#enums/visibility';
|
||||
import { Box, Group, SegmentedControl, Text, TextInput } from '@mantine/core';
|
||||
import { FormEvent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import BackToDashboard from '~/components/common/navigation/back_to_dashboard';
|
||||
import { FormLayout, FormLayoutProps } from '~/layouts/form_layout';
|
||||
import { Collection } from '~/types/app';
|
||||
import { Collection, Visibility } from '~/types/app';
|
||||
|
||||
export type FormCollectionData = {
|
||||
name: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import PATHS from '#constants/paths';
|
||||
import PATHS from '#core/constants/paths';
|
||||
import { Link } from '@inertiajs/react';
|
||||
import { route } from '@izzyjs/route/client';
|
||||
import {
|
||||
@@ -17,7 +17,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import ExternalLink from '~/components/common/external_link';
|
||||
import { MantineLanguageSwitcher } from '~/components/common/language_switcher';
|
||||
import { MantineThemeSwitcher } from '~/components/common/theme_switcher';
|
||||
import { MantineUserCard } from '~/components/common/user_card';
|
||||
import useUser from '~/hooks/use_user';
|
||||
import classes from './mobile.module.css';
|
||||
|
||||
@@ -31,7 +30,9 @@ export default function Navbar() {
|
||||
<Box pb={40}>
|
||||
<header className={classes.header}>
|
||||
<Group justify="space-between" h="100%">
|
||||
<Image src="/logo-light.png" h={35} alt="MyLinks's logo" />
|
||||
<Link href="/">
|
||||
<Image src="/logo.png" h={35} alt="MyLinks's logo" />
|
||||
</Link>
|
||||
|
||||
<Group h="100%" gap={0} visibleFrom="sm">
|
||||
<Link href="/" className={classes.link}>
|
||||
@@ -102,11 +103,13 @@ export default function Navbar() {
|
||||
|
||||
<Group justify="center" grow pb="xl" px="md">
|
||||
{!isAuthenticated ? (
|
||||
<Button component="a" href={route('auth').path}>
|
||||
<Button component="a" href={route('auth').path} w={110}>
|
||||
{t('login')}
|
||||
</Button>
|
||||
) : (
|
||||
<MantineUserCard />
|
||||
<Button component={Link} href={route('dashboard').path} w={110}>
|
||||
Dashboard
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</ScrollArea>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Visibility } from '#enums/visibility';
|
||||
import { ActionIcon, Anchor, CopyButton, Popover, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TbShare3 } from 'react-icons/tb';
|
||||
import { Fragment } from 'react/jsx-runtime';
|
||||
import { generateShareUrl } from '~/lib/navigation';
|
||||
import { useActiveCollection } from '~/stores/collection_store';
|
||||
import { Visibility } from '~/types/app';
|
||||
|
||||
const COPY_SUCCESS_TIMEOUT = 2_000;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import KEYS from '#constants/keys';
|
||||
import KEYS from '#core/constants/keys';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useGlobalHotkeysStore } from '~/stores/global_hotkeys_store';
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { router } from '@inertiajs/react';
|
||||
import { router, usePage } from '@inertiajs/react';
|
||||
import { ColorSchemeScript, createTheme, MantineProvider } from '@mantine/core';
|
||||
import dayjs from 'dayjs';
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { InertiaPage } from '~/types/inertia';
|
||||
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/spotlight/styles.css';
|
||||
@@ -14,6 +15,8 @@ const TRANSITION_OUT_CLASS = '__transition_fadeOut';
|
||||
|
||||
export default function BaseLayout({ children }: { children: ReactNode }) {
|
||||
const { i18n } = useTranslation();
|
||||
const { language } = usePage<InertiaPage>().props;
|
||||
i18n.changeLanguage(language);
|
||||
dayjs.locale(i18n.language);
|
||||
|
||||
const findAppElement = () => document.getElementById('app');
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Visibility } from '#enums/visibility';
|
||||
import { useForm } from '@inertiajs/react';
|
||||
import { route } from '@izzyjs/route/client';
|
||||
import { useMemo } from 'react';
|
||||
@@ -6,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import MantineFormCollection, {
|
||||
FormCollectionData,
|
||||
} from '~/components/form/form_collection';
|
||||
import { Visibility } from '~/types/app';
|
||||
|
||||
export default function CreateCollectionPage({
|
||||
disableHomeLink,
|
||||
|
||||
@@ -11,10 +11,12 @@ body {
|
||||
}
|
||||
|
||||
.__transition_fadeIn {
|
||||
transform-origin: 50% top;
|
||||
animation: fadeIn 0.15s ease both;
|
||||
}
|
||||
|
||||
.__transition_fadeOut {
|
||||
transform-origin: 50% top;
|
||||
animation: fadeOut 0.15s ease both;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,21 +2,13 @@
|
||||
"extends": "@adonisjs/tsconfig/tsconfig.client.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"target": "ESNext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"jsx": "react-jsx",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"~/*": ["./*"],
|
||||
"@/*": ["../*"]
|
||||
},
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["./**/*.ts", "./**/*.tsx"]
|
||||
"config-ssr": ["../config/ssr"]
|
||||
}
|
||||
},
|
||||
"include": ["./**/*.ts", "./**/*.tsx", "../config/ssr.ts"]
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Visibility } from '@/app/enums/visibility';
|
||||
|
||||
type CommonBase = {
|
||||
id: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
type User = CommonBase & {
|
||||
export type User = CommonBase & {
|
||||
email: string;
|
||||
fullname: string;
|
||||
avatarUrl: string;
|
||||
@@ -14,15 +12,15 @@ type User = CommonBase & {
|
||||
lastSeenAt: string;
|
||||
};
|
||||
|
||||
type PublicUser = Omit<User, 'email'>;
|
||||
export type PublicUser = Omit<User, 'email'>;
|
||||
|
||||
type Users = User[];
|
||||
export type Users = User[];
|
||||
|
||||
type UserWithCollections = User & {
|
||||
export type UserWithCollections = User & {
|
||||
collections: Collection[];
|
||||
};
|
||||
|
||||
type UserWithRelationCount = CommonBase & {
|
||||
export type UserWithRelationCount = CommonBase & {
|
||||
email: string;
|
||||
fullname: string;
|
||||
avatarUrl: string;
|
||||
@@ -32,7 +30,7 @@ type UserWithRelationCount = CommonBase & {
|
||||
lastSeenAt: string;
|
||||
};
|
||||
|
||||
type Link = CommonBase & {
|
||||
export type Link = CommonBase & {
|
||||
name: string;
|
||||
description: string | null;
|
||||
url: string;
|
||||
@@ -40,14 +38,14 @@ type Link = CommonBase & {
|
||||
collectionId: number;
|
||||
};
|
||||
|
||||
type LinkWithCollection = Link & {
|
||||
export type LinkWithCollection = Link & {
|
||||
collection: Collection;
|
||||
};
|
||||
|
||||
type PublicLink = Omit<Link, 'favorite'>;
|
||||
type PublicLinkWithCollection = Omit<Link, 'favorite'>;
|
||||
export type PublicLink = Omit<Link, 'favorite'>;
|
||||
export type PublicLinkWithCollection = Omit<Link, 'favorite'>;
|
||||
|
||||
type Collection = CommonBase & {
|
||||
export type Collection = CommonBase & {
|
||||
name: string;
|
||||
description: string | null;
|
||||
visibility: Visibility;
|
||||
@@ -55,6 +53,11 @@ type Collection = CommonBase & {
|
||||
authorId: number;
|
||||
};
|
||||
|
||||
type CollectionWithLinks = Collection & {
|
||||
export type CollectionWithLinks = Collection & {
|
||||
links: Link[];
|
||||
};
|
||||
|
||||
export enum Visibility {
|
||||
PUBLIC = 'PUBLIC',
|
||||
PRIVATE = 'PRIVATE',
|
||||
}
|
||||
110
package.json
110
package.json
@@ -1,107 +1,107 @@
|
||||
{
|
||||
"name": "my-links",
|
||||
"version": "3.0.0",
|
||||
"version": "3.1.0",
|
||||
"type": "module",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"start": "node bin/server.js",
|
||||
"build": "node ace build",
|
||||
"dev": "node ace serve --watch",
|
||||
"dev": "node ace serve --hmr",
|
||||
"test": "node ace test",
|
||||
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
|
||||
"format": "prettier --write --parser typescript '**/*.{ts,tsx}'",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"prepare": "husky",
|
||||
"release": "release-it"
|
||||
"release": "release-it",
|
||||
"generate-icons": "pwa-assets-generator"
|
||||
},
|
||||
"imports": {
|
||||
"#controllers/*": "./app/controllers/*.js",
|
||||
"#exceptions/*": "./app/exceptions/*.js",
|
||||
"#models/*": "./app/models/*.js",
|
||||
"#constants/*": "./app/constants/*.js",
|
||||
"#enums/*": "./app/enums/*.js",
|
||||
"#mails/*": "./app/mails/*.js",
|
||||
"#services/*": "./app/services/*.js",
|
||||
"#listeners/*": "./app/listeners/*.js",
|
||||
"#events/*": "./app/events/*.js",
|
||||
"#middleware/*": "./app/middleware/*.js",
|
||||
"#validators/*": "./app/validators/*.js",
|
||||
"#admin/*": "./app/admin/*.js",
|
||||
"#auth/*": "./app/auth/*.js",
|
||||
"#collections/*": "./app/collections/*.js",
|
||||
"#core/*": "./app/core/*.js",
|
||||
"#favicons/*": "./app/favicons/*.js",
|
||||
"#home/*": "./app/home/*.js",
|
||||
"#legal/*": "./app/legal/*.js",
|
||||
"#links/*": "./app/links/*.js",
|
||||
"#search/*": "./app/search/*.js",
|
||||
"#shared_collections/*": "./app/shared_collections/*.js",
|
||||
"#user/*": "./app/user/*.js",
|
||||
"#providers/*": "./providers/*.js",
|
||||
"#policies/*": "./app/policies/*.js",
|
||||
"#abilities/*": "./app/abilities/*.js",
|
||||
"#database/*": "./database/*.js",
|
||||
"#tests/*": "./tests/*.js",
|
||||
"#start/*": "./start/*.js",
|
||||
"#config/*": "./config/*.js",
|
||||
"#lib/*": "./app/lib/*.js"
|
||||
"#config/*": "./config/*.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@adonisjs/assembler": "^7.8.2",
|
||||
"@adonisjs/eslint-config": "2.0.0-beta.6",
|
||||
"@adonisjs/prettier-config": "^1.4.0",
|
||||
"@adonisjs/tsconfig": "^1.4.0",
|
||||
"@faker-js/faker": "^9.2.0",
|
||||
"@japa/assert": "^3.0.0",
|
||||
"@faker-js/faker": "^9.3.0",
|
||||
"@japa/assert": "^4.0.0",
|
||||
"@japa/plugin-adonisjs": "^3.0.1",
|
||||
"@japa/runner": "^3.1.4",
|
||||
"@swc/core": "^1.9.1",
|
||||
"@swc/core": "^1.10.4",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.13.0",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"eslint": "^9.14.0",
|
||||
"hot-hook": "^0.3.1",
|
||||
"husky": "^9.1.6",
|
||||
"lint-staged": "^15.2.10",
|
||||
"pino-pretty": "^11.3.0",
|
||||
"postcss": "^8.4.47",
|
||||
"@types/node": "^22.10.4",
|
||||
"@types/react": "^19.0.2",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.19.0",
|
||||
"@vite-pwa/assets-generator": "^0.2.6",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.17.0",
|
||||
"hot-hook": "^0.4.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.3.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "^3.3.3",
|
||||
"release-it": "^17.10.0",
|
||||
"prettier": "^3.4.2",
|
||||
"release-it": "^17.11.0",
|
||||
"ts-node-maintained": "^10.9.4",
|
||||
"typescript": "~5.6.3",
|
||||
"vite": "^5.4.10"
|
||||
"typescript": "~5.7.2",
|
||||
"vite": "^6.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@adonisjs/ally": "^5.0.2",
|
||||
"@adonisjs/auth": "^9.2.3",
|
||||
"@adonisjs/core": "^6.14.1",
|
||||
"@adonisjs/auth": "^9.3.0",
|
||||
"@adonisjs/core": "^6.17.0",
|
||||
"@adonisjs/cors": "^2.2.1",
|
||||
"@adonisjs/inertia": "^1.2.3",
|
||||
"@adonisjs/lucid": "^21.3.0",
|
||||
"@adonisjs/inertia": "^2.1.2",
|
||||
"@adonisjs/lucid": "^21.6.0",
|
||||
"@adonisjs/session": "^7.5.0",
|
||||
"@adonisjs/shield": "^8.1.1",
|
||||
"@adonisjs/static": "^1.1.1",
|
||||
"@adonisjs/vite": "^3.0.0",
|
||||
"@inertiajs/react": "^1.2.0",
|
||||
"@adonisjs/vite": "^4.0.0",
|
||||
"@inertiajs/react": "^2.0.0",
|
||||
"@izzyjs/route": "^1.2.0",
|
||||
"@mantine/core": "^7.13.4",
|
||||
"@mantine/hooks": "^7.13.4",
|
||||
"@mantine/spotlight": "^7.13.5",
|
||||
"@vinejs/vine": "^2.1.0",
|
||||
"@mantine/core": "^7.15.2",
|
||||
"@mantine/hooks": "^7.15.2",
|
||||
"@mantine/spotlight": "^7.15.2",
|
||||
"@vinejs/vine": "^3.0.0",
|
||||
"bentocache": "^1.0.0-beta.9",
|
||||
"dayjs": "^1.11.13",
|
||||
"edge.js": "^6.2.0",
|
||||
"i18next": "^23.16.4",
|
||||
"i18next": "^24.2.0",
|
||||
"knex": "^3.1.0",
|
||||
"luxon": "^3.5.0",
|
||||
"node-html-parser": "^6.1.13",
|
||||
"node-html-parser": "^7.0.1",
|
||||
"pg": "^8.13.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hotkeys-hook": "^4.6.1",
|
||||
"react-i18next": "^15.1.0",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"zustand": "^5.0.1"
|
||||
"vite-plugin-pwa": "^0.21.1",
|
||||
"zustand": "^5.0.2"
|
||||
},
|
||||
"hotHook": {
|
||||
"boundaries": [
|
||||
"./app/controllers/**/*.ts",
|
||||
"./app/middleware/*.ts"
|
||||
"./app/**/controllers/*.ts",
|
||||
"./app/**/middleware/*.ts"
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
|
||||
5528
pnpm-lock.yaml
generated
5528
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user