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>
|
</div>
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- [Main Features](#main-features)
|
- [Main Features](#main-features)
|
||||||
- [Getting Started](#getting-started)
|
- [Getting Started](#getting-started)
|
||||||
- [Setup](#setup)
|
- [Setup](#setup)
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
- [Contributing](#contributing)
|
- [Contributing](#contributing)
|
||||||
- [License](#license)
|
- [License](#license)
|
||||||
|
|
||||||
## Main Features
|
## Main Features
|
||||||
|
|
||||||
- **Organize bookmarks with collections**: Keep your links tidy and easily accessible by grouping them into customizable collections.
|
- **Organize bookmarks with collections**: Keep your links tidy and easily accessible by grouping them into customizable collections.
|
||||||
- **Intuitive link management**: Add, edit, and manage your bookmarks effortlessly with a user-friendly interface.
|
- **Intuitive link management**: Add, edit, and manage your bookmarks effortlessly with a user-friendly interface.
|
||||||
@@ -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
|
> Source: https://github.com/appleboy/ssh-action#setting-up-a-ssh-key
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
We welcome contributions! Please visit our Trello board for project management and roadmap details. You can contribute by:
|
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.
|
- Creating issues for bugs, features, or discussions.
|
||||||
@@ -133,4 +135,4 @@ For detailed contribution guidelines, refer to the CONTRIBUTING.md file.
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the [GPLv3 License](./LICENCE).
|
This project is licensed under the [GPLv3 License](./LICENCE).
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import CollectionsController from '#controllers/collections_controller';
|
import AuthController from '#auth/controllers/auth_controller';
|
||||||
import LinksController from '#controllers/links_controller';
|
import CollectionsController from '#collections/controllers/collections_controller';
|
||||||
import UsersController from '#controllers/users_controller';
|
import LinksController from '#links/controllers/links_controller';
|
||||||
import User from '#models/user';
|
import User from '#user/models/user';
|
||||||
import { inject } from '@adonisjs/core';
|
import { inject } from '@adonisjs/core';
|
||||||
import type { HttpContext } from '@adonisjs/core/http';
|
import { HttpContext } from '@adonisjs/core/http';
|
||||||
|
|
||||||
class UserWithRelationCountDto {
|
class UserWithRelationCountDto {
|
||||||
constructor(private user: User) {}
|
constructor(private user: User) {}
|
||||||
@@ -26,7 +26,7 @@ class UserWithRelationCountDto {
|
|||||||
@inject()
|
@inject()
|
||||||
export default class AdminController {
|
export default class AdminController {
|
||||||
constructor(
|
constructor(
|
||||||
protected usersController: UsersController,
|
protected usersController: AuthController,
|
||||||
protected linksController: LinksController,
|
protected linksController: LinksController,
|
||||||
protected collectionsController: CollectionsController
|
protected collectionsController: CollectionsController
|
||||||
) {}
|
) {}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { middleware } from '#start/kernel';
|
import { middleware } from '#start/kernel';
|
||||||
import router from '@adonisjs/core/services/router';
|
import router from '@adonisjs/core/services/router';
|
||||||
|
|
||||||
const AdminController = () => import('#controllers/admin_controller');
|
const AdminController = () => import('#admin/controllers/admin_controller');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routes for admin dashboard
|
* 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 type { HttpContext } from '@adonisjs/core/http';
|
||||||
import logger from '@adonisjs/core/services/logger';
|
import logger from '@adonisjs/core/services/logger';
|
||||||
import db from '@adonisjs/lucid/services/db';
|
import db from '@adonisjs/lucid/services/db';
|
||||||
import { RouteName } from '@izzyjs/route/types';
|
import { RouteName } from '@izzyjs/route/types';
|
||||||
|
|
||||||
export default class UsersController {
|
export default class AuthController {
|
||||||
private redirectTo: RouteName = 'auth';
|
private redirectTo: RouteName = 'auth';
|
||||||
|
|
||||||
login({ inertia }: HttpContext) {
|
login({ inertia }: HttpContext) {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { middleware } from '#start/kernel';
|
import { middleware } from '#start/kernel';
|
||||||
import router from '@adonisjs/core/services/router';
|
import router from '@adonisjs/core/services/router';
|
||||||
const UsersController = () => import('#controllers/users_controller');
|
const AuthController = () => import('#auth/controllers/auth_controller');
|
||||||
|
|
||||||
const ROUTES_PREFIX = '/auth';
|
const ROUTES_PREFIX = '/auth';
|
||||||
|
|
||||||
@@ -9,9 +9,9 @@ const ROUTES_PREFIX = '/auth';
|
|||||||
*/
|
*/
|
||||||
router
|
router
|
||||||
.group(() => {
|
.group(() => {
|
||||||
router.get('/google', [UsersController, 'google']).as('auth');
|
router.get('/google', [AuthController, 'google']).as('auth');
|
||||||
router
|
router
|
||||||
.get('/callback', [UsersController, 'callbackAuth'])
|
.get('/callback', [AuthController, 'callbackAuth'])
|
||||||
.as('auth.callback');
|
.as('auth.callback');
|
||||||
})
|
})
|
||||||
.prefix(ROUTES_PREFIX);
|
.prefix(ROUTES_PREFIX);
|
||||||
@@ -21,7 +21,7 @@ router
|
|||||||
*/
|
*/
|
||||||
router
|
router
|
||||||
.group(() => {
|
.group(() => {
|
||||||
router.get('/logout', [UsersController, 'logout']).as('auth.logout');
|
router.get('/logout', [AuthController, 'logout']).as('auth.logout');
|
||||||
})
|
})
|
||||||
.middleware([middleware.auth()])
|
.middleware([middleware.auth()])
|
||||||
.prefix(ROUTES_PREFIX);
|
.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 Collection from '#collections/models/collection';
|
||||||
import User from '#models/user';
|
import { createCollectionValidator } from '#collections/validators/create_collection_validator';
|
||||||
import {
|
import { deleteCollectionValidator } from '#collections/validators/delete_collection_validator';
|
||||||
createCollectionValidator,
|
import { updateCollectionValidator } from '#collections/validators/update_collection_validator';
|
||||||
deleteCollectionValidator,
|
import User from '#user/models/user';
|
||||||
updateCollectionValidator,
|
|
||||||
} from '#validators/collection';
|
|
||||||
import type { HttpContext } from '@adonisjs/core/http';
|
import type { HttpContext } from '@adonisjs/core/http';
|
||||||
import db from '@adonisjs/lucid/services/db';
|
import db from '@adonisjs/lucid/services/db';
|
||||||
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import AppBaseModel from '#models/app_base_model';
|
import { Visibility } from '#collections/enums/visibility';
|
||||||
import Link from '#models/link';
|
import AppBaseModel from '#core/models/app_base_model';
|
||||||
import User from '#models/user';
|
import Link from '#links/models/link';
|
||||||
|
import User from '#user/models/user';
|
||||||
import { belongsTo, column, hasMany } from '@adonisjs/lucid/orm';
|
import { belongsTo, column, hasMany } from '@adonisjs/lucid/orm';
|
||||||
import type { BelongsTo, HasMany } from '@adonisjs/lucid/types/relations';
|
import type { BelongsTo, HasMany } from '@adonisjs/lucid/types/relations';
|
||||||
import { Visibility } from '#enums/visibility';
|
|
||||||
|
|
||||||
export default class Collection extends AppBaseModel {
|
export default class Collection extends AppBaseModel {
|
||||||
@column()
|
@column()
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { middleware } from '#start/kernel';
|
import { middleware } from '#start/kernel';
|
||||||
import router from '@adonisjs/core/services/router';
|
import router from '@adonisjs/core/services/router';
|
||||||
const CollectionsController = () =>
|
const CollectionsController = () =>
|
||||||
import('#controllers/collections_controller');
|
import('#collections/controllers/collections_controller');
|
||||||
|
|
||||||
router
|
router
|
||||||
.group(() => {
|
.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';
|
import vine, { SimpleMessagesProvider } from '@vinejs/vine';
|
||||||
|
|
||||||
const params = vine.object({
|
|
||||||
id: vine.number(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createCollectionValidator = vine.compile(
|
export const createCollectionValidator = vine.compile(
|
||||||
vine.object({
|
vine.object({
|
||||||
name: vine.string().trim().minLength(1).maxLength(254),
|
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({
|
createCollectionValidator.messagesProvider = new SimpleMessagesProvider({
|
||||||
name: 'Collection name is required',
|
name: 'Collection name is required',
|
||||||
'visibility.required': 'Collection visibiliy 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 = {
|
const PATHS = {
|
||||||
AUTHOR: 'https://www.sonny.dev/?utm_source=mylinks',
|
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:
|
EXTENSION:
|
||||||
'https://chromewebstore.google.com/detail/mylinks/agkmlplihacolkakgeccnbhphnepphma',
|
'https://chromewebstore.google.com/detail/mylinks/agkmlplihacolkakgeccnbhphnepphma',
|
||||||
} as const;
|
} 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';
|
import router from '@adonisjs/core/services/router';
|
||||||
const FaviconsController = () => import('#controllers/favicons_controller');
|
const FaviconsController = () =>
|
||||||
|
import('#favicons/controllers/favicons_controller');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Favicon routes
|
* 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 FaviconNotFoundException from '#favicons/exceptions/favicon_not_found_exception';
|
||||||
import { cache } from '#lib/cache';
|
import { Favicon } from '#favicons/types/favicon_type';
|
||||||
import type { HttpContext } from '@adonisjs/core/http';
|
|
||||||
import logger from '@adonisjs/core/services/logger';
|
import logger from '@adonisjs/core/services/logger';
|
||||||
import { parse } from 'node-html-parser';
|
import { parse } from 'node-html-parser';
|
||||||
|
|
||||||
interface Favicon {
|
export class FaviconService {
|
||||||
buffer: Buffer;
|
|
||||||
url: string;
|
|
||||||
type: string;
|
|
||||||
size: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class FaviconsController {
|
|
||||||
private userAgent =
|
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';
|
'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 = [
|
private relList = [
|
||||||
@@ -24,27 +16,11 @@ export default class FaviconsController {
|
|||||||
'fluid-icon',
|
'fluid-icon',
|
||||||
];
|
];
|
||||||
|
|
||||||
async index(ctx: HttpContext) {
|
async getFavicon(url: string): Promise<Favicon> {
|
||||||
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');
|
|
||||||
try {
|
try {
|
||||||
return await this.fetchFavicon(faviconUrl);
|
return await this.fetchFavicon(this.buildFaviconUrl(url, '/favicon.ico'));
|
||||||
} catch {
|
} 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);
|
const documentText = await this.fetchDocumentText(url);
|
||||||
@@ -54,15 +30,7 @@ export default class FaviconsController {
|
|||||||
throw new FaviconNotFoundException(`No favicon path found in ${url}`);
|
throw new FaviconNotFoundException(`No favicon path found in ${url}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (faviconPath.startsWith('http')) {
|
return await this.fetchFaviconFromPath(url, faviconPath);
|
||||||
try {
|
|
||||||
return await this.fetchFavicon(faviconPath);
|
|
||||||
} catch {
|
|
||||||
logger.debug(`Unable to retrieve favicon from ${faviconPath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.fetchFaviconFromPath(url, faviconPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchFavicon(url: string): Promise<Favicon> {
|
private async fetchFavicon(url: string): Promise<Favicon> {
|
||||||
@@ -127,7 +95,9 @@ export default class FaviconsController {
|
|||||||
|
|
||||||
const basePath = this.urlWithoutSearchParams(base);
|
const basePath = this.urlWithoutSearchParams(base);
|
||||||
const baseUrl = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
|
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 {
|
private urlWithoutSearchParams(url: string): string {
|
||||||
@@ -151,10 +121,4 @@ export default class FaviconsController {
|
|||||||
const headers = new Headers({ 'User-Agent': this.userAgent });
|
const headers = new Headers({ 'User-Agent': this.userAgent });
|
||||||
return fetch(url, { headers });
|
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 CollectionsController from '#collections/controllers/collections_controller';
|
||||||
import Link from '#models/link';
|
import Link from '#links/models/link';
|
||||||
import {
|
import { createLinkValidator } from '#links/validators/create_link_validator';
|
||||||
createLinkValidator,
|
import { deleteLinkValidator } from '#links/validators/delete_link_validator';
|
||||||
deleteLinkValidator,
|
import { updateLinkFavoriteStatusValidator } from '#links/validators/update_favorite_link_validator';
|
||||||
updateLinkFavoriteStatusValidator,
|
import { updateLinkValidator } from '#links/validators/update_link_validator';
|
||||||
updateLinkValidator,
|
|
||||||
} from '#validators/link';
|
|
||||||
import { inject } from '@adonisjs/core';
|
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';
|
import db from '@adonisjs/lucid/services/db';
|
||||||
|
|
||||||
@inject()
|
@inject()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import AppBaseModel from '#models/app_base_model';
|
import Collection from '#collections/models/collection';
|
||||||
import Collection from '#models/collection';
|
import AppBaseModel from '#core/models/app_base_model';
|
||||||
import User from '#models/user';
|
import User from '#user/models/user';
|
||||||
import { belongsTo, column } from '@adonisjs/lucid/orm';
|
import { belongsTo, column } from '@adonisjs/lucid/orm';
|
||||||
import type { BelongsTo } from '@adonisjs/lucid/types/relations';
|
import type { BelongsTo } from '@adonisjs/lucid/types/relations';
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { middleware } from '#start/kernel';
|
import { middleware } from '#start/kernel';
|
||||||
import router from '@adonisjs/core/services/router';
|
import router from '@adonisjs/core/services/router';
|
||||||
const LinksController = () => import('#controllers/links_controller');
|
const LinksController = () => import('#links/controllers/links_controller');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routes for authenticated users
|
* 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 type { HttpContext } from '@adonisjs/core/http';
|
||||||
import db from '@adonisjs/lucid/services/db';
|
import db from '@adonisjs/lucid/services/db';
|
||||||
|
|
||||||
export default class SearchesController {
|
export default class SearchController {
|
||||||
async search({ request, auth }: HttpContext) {
|
async search({ request, auth }: HttpContext) {
|
||||||
const term = request.qs()?.term;
|
const term = request.qs()?.term;
|
||||||
if (!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 { middleware } from '#start/kernel';
|
||||||
import router from '@adonisjs/core/services/router';
|
import router from '@adonisjs/core/services/router';
|
||||||
|
|
||||||
const SearchesController = () => import('#controllers/searches_controller');
|
const SearchesController = () =>
|
||||||
|
import('#search/controllers/search_controller');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search routes
|
* Search routes
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Visibility } from '#enums/visibility';
|
import { Visibility } from '#collections/enums/visibility';
|
||||||
import Collection from '#models/collection';
|
import Collection from '#collections/models/collection';
|
||||||
import Link from '#models/link';
|
import Link from '#links/models/link';
|
||||||
import User from '#models/user';
|
import User from '#user/models/user';
|
||||||
import { getSharedCollectionValidator } from '#validators/shared_collection';
|
|
||||||
import type { HttpContext } from '@adonisjs/core/http';
|
import type { HttpContext } from '@adonisjs/core/http';
|
||||||
|
import { getSharedCollectionValidator } from '../validators/shared_collection.js';
|
||||||
|
|
||||||
class LinkWithoutFavoriteDto {
|
class LinkWithoutFavoriteDto {
|
||||||
constructor(private link: Link) {}
|
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';
|
import router from '@adonisjs/core/services/router';
|
||||||
|
|
||||||
const SharedCollectionsController = () =>
|
const SharedCollectionsController = () =>
|
||||||
import('#controllers/shared_collections_controller');
|
import('#shared_collections/controllers/shared_collections_controller');
|
||||||
|
|
||||||
router.get('/shared/:id', [SharedCollectionsController, 'index']).as('shared');
|
router.get('/shared/:id', [SharedCollectionsController, 'index']).as('shared');
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
|
import { params } from '#core/validators/params_object';
|
||||||
import vine from '@vinejs/vine';
|
import vine from '@vinejs/vine';
|
||||||
|
|
||||||
const params = vine.object({
|
|
||||||
id: vine.number(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getSharedCollectionValidator = vine.compile(
|
export const getSharedCollectionValidator = vine.compile(
|
||||||
vine.object({
|
vine.object({
|
||||||
params,
|
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 Collection from '#collections/models/collection';
|
||||||
import Link from '#models/link';
|
import AppBaseModel from '#core/models/app_base_model';
|
||||||
|
import Link from '#links/models/link';
|
||||||
import type { GoogleToken } from '@adonisjs/ally/types';
|
import type { GoogleToken } from '@adonisjs/ally/types';
|
||||||
import { column, computed, hasMany } from '@adonisjs/lucid/orm';
|
import { column, computed, hasMany } from '@adonisjs/lucid/orm';
|
||||||
import type { HasMany } from '@adonisjs/lucid/types/relations';
|
import type { HasMany } from '@adonisjs/lucid/types/relations';
|
||||||
import AppBaseModel from './app_base_model.js';
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
export default class User extends AppBaseModel {
|
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 { defineConfig } from '@adonisjs/auth';
|
||||||
import { InferAuthEvents, Authenticators } from '@adonisjs/auth/types';
|
|
||||||
import { sessionGuard, sessionUserProvider } from '@adonisjs/auth/session';
|
import { sessionGuard, sessionUserProvider } from '@adonisjs/auth/session';
|
||||||
|
import { Authenticators, InferAuthEvents } from '@adonisjs/auth/types';
|
||||||
|
|
||||||
const authConfig = defineConfig({
|
const authConfig = defineConfig({
|
||||||
default: 'web',
|
default: 'web',
|
||||||
@@ -8,7 +8,7 @@ const authConfig = defineConfig({
|
|||||||
web: sessionGuard({
|
web: sessionGuard({
|
||||||
useRememberMeTokens: false,
|
useRememberMeTokens: false,
|
||||||
provider: sessionUserProvider({
|
provider: sessionUserProvider({
|
||||||
model: () => import('#models/user'),
|
model: () => import('#user/models/user'),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {
|
import { isSSREnableForPage } from '#config/ssr';
|
||||||
DARK_THEME_DEFAULT_VALUE,
|
import { DEFAULT_USER_THEME, KEY_USER_THEME } from '#user/constants/theme';
|
||||||
PREFER_DARK_THEME,
|
import logger from '@adonisjs/core/services/logger';
|
||||||
} from '#constants/session';
|
|
||||||
import { defineConfig } from '@adonisjs/inertia';
|
import { defineConfig } from '@adonisjs/inertia';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@@ -15,8 +14,9 @@ export default defineConfig({
|
|||||||
*/
|
*/
|
||||||
sharedData: {
|
sharedData: {
|
||||||
errors: (ctx) => ctx.session?.flashMessages.get('errors'),
|
errors: (ctx) => ctx.session?.flashMessages.get('errors'),
|
||||||
preferDarkTheme: (ctx) =>
|
user: (ctx) => ({
|
||||||
ctx.session?.get(PREFER_DARK_THEME, DARK_THEME_DEFAULT_VALUE),
|
theme: ctx.session?.get(KEY_USER_THEME, DEFAULT_USER_THEME),
|
||||||
|
}),
|
||||||
auth: async (ctx) => {
|
auth: async (ctx) => {
|
||||||
await ctx.auth?.check();
|
await ctx.auth?.check();
|
||||||
return {
|
return {
|
||||||
@@ -32,5 +32,10 @@ export default defineConfig({
|
|||||||
ssr: {
|
ssr: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
entrypoint: 'inertia/app/ssr.tsx',
|
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 { defaultTableFields } from '#database/default_table_fields';
|
||||||
import { Visibility } from '#enums/visibility';
|
|
||||||
import { BaseSchema } from '@adonisjs/lucid/schema';
|
import { BaseSchema } from '@adonisjs/lucid/schema';
|
||||||
|
|
||||||
export default class CreateCollectionTable extends BaseSchema {
|
export default class CreateCollectionTable extends BaseSchema {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Visibility } from '#enums/visibility';
|
import { Visibility } from '#collections/enums/visibility';
|
||||||
import Collection from '#models/collection';
|
import Collection from '#collections/models/collection';
|
||||||
import User from '#models/user';
|
import User from '#user/models/user';
|
||||||
import { BaseSeeder } from '@adonisjs/lucid/seeders';
|
import { BaseSeeder } from '@adonisjs/lucid/seeders';
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import Collection from '#collections/models/collection';
|
||||||
import { getUserIds } from '#database/seeders/collection_seeder';
|
import { getUserIds } from '#database/seeders/collection_seeder';
|
||||||
import Collection from '#models/collection';
|
import Link from '#links/models/link';
|
||||||
import Link from '#models/link';
|
import User from '#user/models/user';
|
||||||
import User from '#models/user';
|
|
||||||
import { BaseSeeder } from '@adonisjs/lucid/seeders';
|
import { BaseSeeder } from '@adonisjs/lucid/seeders';
|
||||||
import { faker } from '@faker-js/faker';
|
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 { GoogleToken } from '@adonisjs/ally/types';
|
||||||
import { BaseSeeder } from '@adonisjs/lucid/seeders';
|
import { BaseSeeder } from '@adonisjs/lucid/seeders';
|
||||||
import { faker } from '@faker-js/faker';
|
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 { resolvePageComponent } from '@adonisjs/inertia/helpers';
|
||||||
import { createInertiaApp } from '@inertiajs/react';
|
import { createInertiaApp } from '@inertiajs/react';
|
||||||
|
import { isSSREnableForPage } from 'config-ssr';
|
||||||
import 'dayjs/locale/en';
|
import 'dayjs/locale/en';
|
||||||
import 'dayjs/locale/fr';
|
import 'dayjs/locale/fr';
|
||||||
import { hydrateRoot } from 'react-dom/client';
|
import { createRoot, hydrateRoot } from 'react-dom/client';
|
||||||
import '../i18n/index';
|
import '../i18n/index';
|
||||||
|
|
||||||
const appName = import.meta.env.VITE_APP_NAME || 'MyLinks';
|
const appName = import.meta.env.VITE_APP_NAME || 'MyLinks';
|
||||||
@@ -20,6 +21,13 @@ createInertiaApp({
|
|||||||
},
|
},
|
||||||
|
|
||||||
setup({ el, App, props }) {
|
setup({ el, App, props }) {
|
||||||
hydrateRoot(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';
|
} from '@mantine/core';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import { useState } from 'react';
|
import { ChangeEvent, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { TbSearch } from 'react-icons/tb';
|
import { TbSearch } from 'react-icons/tb';
|
||||||
import { Th } from '~/components/admin/users/th';
|
import { Th } from '~/components/admin/users/th';
|
||||||
@@ -25,6 +25,11 @@ export type UserWithCounts = User & {
|
|||||||
};
|
};
|
||||||
export type UsersWithCounts = UserWithCounts[];
|
export type UsersWithCounts = UserWithCounts[];
|
||||||
|
|
||||||
|
export type Columns = keyof UserWithCounts;
|
||||||
|
|
||||||
|
const DEFAULT_SORT_BY: Columns = 'lastSeenAt';
|
||||||
|
const DEFAULT_SORT_DIRECTION = true;
|
||||||
|
|
||||||
export interface UsersTableProps {
|
export interface UsersTableProps {
|
||||||
users: UsersWithCounts;
|
users: UsersWithCounts;
|
||||||
totalCollections: number;
|
totalCollections: number;
|
||||||
@@ -37,10 +42,19 @@ export function UsersTable({
|
|||||||
totalLinks,
|
totalLinks,
|
||||||
}: UsersTableProps) {
|
}: UsersTableProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
const [sortedData, setSortedData] = useState(users);
|
const [search, setSearch] = useState<string>('');
|
||||||
const [sortBy, setSortBy] = useState<keyof UserWithCounts | null>(null);
|
const [sortBy, setSortBy] = useState<Columns | null>(DEFAULT_SORT_BY);
|
||||||
const [reverseSortDirection, setReverseSortDirection] = useState(false);
|
const [reverseSortDirection, setReverseSortDirection] = useState(
|
||||||
|
DEFAULT_SORT_DIRECTION
|
||||||
|
);
|
||||||
|
const [sortedData, setSortedData] = useState(() =>
|
||||||
|
sortData(users, {
|
||||||
|
sortBy: sortBy,
|
||||||
|
reversed: reverseSortDirection,
|
||||||
|
search: '',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const setSorting = (field: keyof UserWithCounts) => {
|
const setSorting = (field: keyof UserWithCounts) => {
|
||||||
const reversed = field === sortBy ? !reverseSortDirection : false;
|
const reversed = field === sortBy ? !reverseSortDirection : false;
|
||||||
@@ -49,7 +63,7 @@ export function UsersTable({
|
|||||||
setSortedData(sortData(users, { sortBy: field, reversed, search }));
|
setSortedData(sortData(users, { sortBy: field, reversed, search }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleSearchChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
const { value } = event.currentTarget;
|
const { value } = event.currentTarget;
|
||||||
setSearch(value);
|
setSearch(value);
|
||||||
setSortedData(
|
setSortedData(
|
||||||
@@ -83,7 +97,7 @@ export function UsersTable({
|
|||||||
return (
|
return (
|
||||||
<ScrollArea>
|
<ScrollArea>
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="Search by any field"
|
placeholder={`Search by any field (${users.length} users)`}
|
||||||
mb="md"
|
mb="md"
|
||||||
leftSection={<TbSearch style={{ width: rem(16), height: rem(16) }} />}
|
leftSection={<TbSearch style={{ width: rem(16), height: rem(16) }} />}
|
||||||
value={search}
|
value={search}
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
import { ActionIcon, useMantineColorScheme } from '@mantine/core';
|
import { ActionIcon, useMantineColorScheme } from '@mantine/core';
|
||||||
import { TbMoonStars, TbSun } from 'react-icons/tb';
|
import { TbMoonStars, TbSun } from 'react-icons/tb';
|
||||||
|
import { makeRequest } from '~/lib/request';
|
||||||
|
|
||||||
export function MantineThemeSwitcher() {
|
export function MantineThemeSwitcher() {
|
||||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||||
|
const handleThemeChange = () => {
|
||||||
|
toggleColorScheme();
|
||||||
|
makeRequest({
|
||||||
|
url: '/user/theme',
|
||||||
|
method: 'POST',
|
||||||
|
body: { theme: colorScheme === 'dark' ? 'light' : 'dark' },
|
||||||
|
});
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="light"
|
variant="light"
|
||||||
aria-label="Toggle color scheme"
|
aria-label="Toggle color scheme"
|
||||||
onClick={() => toggleColorScheme()}
|
onClick={handleThemeChange}
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
{colorScheme === 'dark' ? <TbSun /> : <TbMoonStars />}
|
{colorScheme === 'dark' ? <TbSun /> : <TbMoonStars />}
|
||||||
|
|||||||
@@ -33,13 +33,14 @@ export default function CollectionItem({
|
|||||||
href={appendCollectionId(route('dashboard').path, collection.id)}
|
href={appendCollectionId(route('dashboard').path, collection.id)}
|
||||||
key={collection.id}
|
key={collection.id}
|
||||||
ref={itemRef}
|
ref={itemRef}
|
||||||
|
title={collection.name}
|
||||||
>
|
>
|
||||||
<FolderIcon className={classes.linkIcon} />
|
<FolderIcon className={classes.linkIcon} />
|
||||||
<Text lineClamp={1} maw={showLinks ? '160px' : '200px'}>
|
<Text lineClamp={1} maw={'200px'} style={{ wordBreak: 'break-all' }}>
|
||||||
{collection.name}
|
{collection.name}
|
||||||
</Text>
|
</Text>
|
||||||
{showLinks && (
|
{showLinks && (
|
||||||
<Text c="var(--mantine-color-gray-5)" ml="xs">
|
<Text style={{ whiteSpace: 'nowrap' }} c="dimmed" ml="sm">
|
||||||
— {linksCount}
|
— {linksCount}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -14,8 +14,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.collectionList {
|
.collectionList {
|
||||||
padding: 1px;
|
|
||||||
padding-right: 5px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
gap: 0.35em;
|
gap: 0.35em;
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { Visibility } from '#enums/visibility';
|
|
||||||
import { Link } from '@inertiajs/react';
|
import { Link } from '@inertiajs/react';
|
||||||
import { route } from '@izzyjs/route/client';
|
import { route } from '@izzyjs/route/client';
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
AppShell,
|
AppShell,
|
||||||
Badge,
|
Badge,
|
||||||
|
Box,
|
||||||
Burger,
|
Burger,
|
||||||
Flex,
|
|
||||||
Group,
|
Group,
|
||||||
Menu,
|
Menu,
|
||||||
Text,
|
Text,
|
||||||
@@ -19,6 +18,7 @@ import { IoTrashOutline } from 'react-icons/io5';
|
|||||||
import { ShareCollection } from '~/components/share/share_collection';
|
import { ShareCollection } from '~/components/share/share_collection';
|
||||||
import { appendCollectionId } from '~/lib/navigation';
|
import { appendCollectionId } from '~/lib/navigation';
|
||||||
import { useActiveCollection } from '~/stores/collection_store';
|
import { useActiveCollection } from '~/stores/collection_store';
|
||||||
|
import { Visibility } from '~/types/app';
|
||||||
|
|
||||||
interface DashboardHeaderProps {
|
interface DashboardHeaderProps {
|
||||||
navbar: {
|
navbar: {
|
||||||
@@ -35,15 +35,15 @@ export function DashboardHeader({ navbar, aside }: DashboardHeaderProps) {
|
|||||||
const { activeCollection } = useActiveCollection();
|
const { activeCollection } = useActiveCollection();
|
||||||
return (
|
return (
|
||||||
<AppShell.Header style={{ display: 'flex', alignItems: 'center' }}>
|
<AppShell.Header style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<Group justify="space-between" px="md" flex={1}>
|
<Group justify="space-between" px="md" flex={1} wrap="nowrap">
|
||||||
<Group h="100%">
|
<Group h="100%" wrap="nowrap">
|
||||||
<Burger
|
<Burger
|
||||||
opened={navbar.opened}
|
opened={navbar.opened}
|
||||||
onClick={navbar.toggle}
|
onClick={navbar.toggle}
|
||||||
hiddenFrom="sm"
|
hiddenFrom="sm"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
<Flex direction="column">
|
<Box>
|
||||||
<Text lineClamp={1}>
|
<Text lineClamp={1}>
|
||||||
{activeCollection?.name}{' '}
|
{activeCollection?.name}{' '}
|
||||||
{activeCollection?.visibility === Visibility.PUBLIC && (
|
{activeCollection?.visibility === Visibility.PUBLIC && (
|
||||||
@@ -55,9 +55,9 @@ export function DashboardHeader({ navbar, aside }: DashboardHeaderProps) {
|
|||||||
{activeCollection.description}
|
{activeCollection.description}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Box>
|
||||||
</Group>
|
</Group>
|
||||||
<Group>
|
<Group wrap="nowrap">
|
||||||
<ShareCollection />
|
<ShareCollection />
|
||||||
|
|
||||||
<Menu withinPortal shadow="md" width={225}>
|
<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 { useTranslation } from 'react-i18next';
|
||||||
import { FavoriteItem } from '~/components/dashboard/favorite/item/favorite_item';
|
import { FavoriteItem } from '~/components/dashboard/favorite/item/favorite_item';
|
||||||
import { useFavorites } from '~/stores/collection_store';
|
import { useFavorites } from '~/stores/collection_store';
|
||||||
import styles from './favorite_list.module.css';
|
|
||||||
|
|
||||||
export function FavoriteList() {
|
export function FavoriteList() {
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
@@ -19,21 +18,15 @@ export function FavoriteList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={styles.sideMenu}>
|
<Flex direction="column">
|
||||||
<Box className={styles.listContainer}>
|
<Text c="dimmed" mt="xs" ml="md" mb={4}>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
{t('favorite')} • {favorites.length}
|
||||||
<Text c="dimmed" mt="xs" ml="md" mb={4}>
|
</Text>
|
||||||
{t('favorite')} • {favorites.length}
|
<Stack gap={4}>
|
||||||
</Text>
|
{favorites.map((link) => (
|
||||||
<ScrollArea className={styles.collectionList}>
|
<FavoriteItem link={link} key={link.id} />
|
||||||
<Stack gap={4}>
|
))}
|
||||||
{favorites.map((link) => (
|
</Stack>
|
||||||
<FavoriteItem link={link} key={link.id} />
|
</Flex>
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,43 +2,11 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 100%;
|
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;
|
padding: 0.25em 0.5em !important;
|
||||||
border-radius: var(--border-radius);
|
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.linkWrapper:hover {
|
.linkWrapper:hover {
|
||||||
border: 1px solid var(--mantine-color-blue-4);
|
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 { ExternalLinkStyled } from '~/components/common/external_link_styled';
|
||||||
import LinkFavicon from '~/components/dashboard/link/item/favicon/link_favicon';
|
import LinkFavicon from '~/components/dashboard/link/item/favicon/link_favicon';
|
||||||
import LinkControls from '~/components/dashboard/link/item/link_controls';
|
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';
|
import styles from './favorite_item.module.css';
|
||||||
|
|
||||||
export const FavoriteItem = ({ link }: { link: LinkWithCollection }) => (
|
export const FavoriteItem = ({ link }: { link: LinkWithCollection }) => (
|
||||||
<Card className={styles.linkWrapper} radius="sm" withBorder>
|
<ExternalLinkStyled href={link.url} title={link.url}>
|
||||||
<Group justify="center" gap="xs">
|
<Card className={styles.linkWrapper}>
|
||||||
<LinkFavicon size={32} url={link.url} />
|
<Group gap="xs" wrap="nowrap">
|
||||||
<ExternalLinkStyled href={link.url} style={{ flex: 1 }}>
|
<LinkFavicon url={link.url} />
|
||||||
<div className={styles.linkName}>
|
<Flex style={{ width: '100%' }} direction="column">
|
||||||
<Text lineClamp={1}>{link.name} </Text>
|
<Text lineClamp={1} c="blue">
|
||||||
</div>
|
{link.name}
|
||||||
<Text c="gray" size="xs" mb={4} lineClamp={1}>
|
</Text>
|
||||||
{link.collection.name}
|
<Text
|
||||||
</Text>
|
c="gray"
|
||||||
</ExternalLinkStyled>
|
size="xs"
|
||||||
<LinkControls link={link} showGoToCollection />
|
mb={4}
|
||||||
</Group>
|
lineClamp={1}
|
||||||
</Card>
|
style={{ wordBreak: 'break-all' }}
|
||||||
|
>
|
||||||
|
{link.collection.name}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
<LinkControls link={link} showGoToCollection />
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
</ExternalLinkStyled>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,9 +2,8 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 100%;
|
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.75em 1em;
|
padding: 0.5em 0.75em !important;
|
||||||
border-radius: var(--border-radius);
|
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,32 +11,8 @@
|
|||||||
border: 1px solid var(--mantine-color-blue-4);
|
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 {
|
.linkUrl {
|
||||||
width: 100%;
|
transition: opacity 0.15s;
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 0.8em;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.linkWrapper:hover .linkUrlPathname {
|
.linkWrapper:hover .linkUrlPathname {
|
||||||
@@ -46,5 +21,5 @@
|
|||||||
|
|
||||||
.linkUrlPathname {
|
.linkUrlPathname {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.3s;
|
transition: opacity 0.15s;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Link as InertiaLink } from '@inertiajs/react';
|
import { Link as InertiaLink } from '@inertiajs/react';
|
||||||
import { route } from '@izzyjs/route/client';
|
import { route } from '@izzyjs/route/client';
|
||||||
import { ActionIcon, Menu } from '@mantine/core';
|
import { ActionIcon, Menu } from '@mantine/core';
|
||||||
|
import { MouseEvent } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { BsThreeDotsVertical } from 'react-icons/bs';
|
import { BsThreeDotsVertical } from 'react-icons/bs';
|
||||||
import { FaRegEye } from 'react-icons/fa';
|
import { FaRegEye } from 'react-icons/fa';
|
||||||
@@ -24,10 +25,17 @@ export default function LinkControls({
|
|||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
|
|
||||||
const onFavoriteCallback = () => toggleFavorite(link.id);
|
const onFavoriteCallback = () => toggleFavorite(link.id);
|
||||||
|
const handleStopPropagation = (event: MouseEvent<HTMLButtonElement>) =>
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu withinPortal shadow="md" width={200}>
|
<Menu withinPortal shadow="md" width={200}>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<ActionIcon variant="subtle" color="var(--mantine-color-text)">
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
color="var(--mantine-color-text)"
|
||||||
|
onClick={handleStopPropagation}
|
||||||
|
>
|
||||||
<BsThreeDotsVertical />
|
<BsThreeDotsVertical />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Menu.Target>
|
</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 { AiFillStar } from 'react-icons/ai';
|
||||||
import { ExternalLinkStyled } from '~/components/common/external_link_styled';
|
import { ExternalLinkStyled } from '~/components/common/external_link_styled';
|
||||||
import LinkFavicon from '~/components/dashboard/link/item/favicon/link_favicon';
|
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 { name, url, description } = link;
|
||||||
const showFavoriteIcon = !hideMenu && 'favorite' in link && link.favorite;
|
const showFavoriteIcon = !hideMenu && 'favorite' in link && link.favorite;
|
||||||
return (
|
return (
|
||||||
<Card className={styles.linkWrapper} padding="sm" radius="sm" withBorder>
|
<ExternalLinkStyled href={url} title={url}>
|
||||||
<Group className={styles.linkHeader} justify="center">
|
<Card className={styles.linkWrapper}>
|
||||||
<LinkFavicon url={url} />
|
<Group justify="center" wrap="nowrap">
|
||||||
<ExternalLinkStyled href={url} style={{ flex: 1 }}>
|
<LinkFavicon url={url} />
|
||||||
<div className={styles.linkName}>
|
<Flex style={{ width: '100%' }} direction="column">
|
||||||
<Text lineClamp={1}>
|
<Text lineClamp={1} c="blue">
|
||||||
{name} {showFavoriteIcon && <AiFillStar color="gold" />}
|
{name} {showFavoriteIcon && <AiFillStar color="gold" />}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
<LinkItemURL url={url} />
|
||||||
<LinkItemURL url={url} />
|
</Flex>
|
||||||
</ExternalLinkStyled>
|
{!hideMenu && <LinkControls link={link} />}
|
||||||
{!hideMenu && <LinkControls link={link} />}
|
</Group>
|
||||||
</Group>
|
{description && (
|
||||||
{description && (
|
<Text c="dimmed" size="sm" mt="xs" lineClamp={3}>
|
||||||
<Text className={styles.linkDescription} c="dimmed" size="sm">
|
{description}
|
||||||
{description}
|
</Text>
|
||||||
</Text>
|
)}
|
||||||
)}
|
</Card>
|
||||||
</Card>
|
</ExternalLinkStyled>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LinkItemURL({ url }: { url: Link['url'] }) {
|
function LinkItemURL({ url }: { url: Link['url'] }) {
|
||||||
try {
|
try {
|
||||||
const { origin, pathname, search } = new URL(url);
|
const { origin, pathname } = new URL(url);
|
||||||
let text = '';
|
|
||||||
|
|
||||||
if (pathname !== '/') {
|
|
||||||
text += pathname;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (search !== '') {
|
|
||||||
if (text === '') {
|
|
||||||
text += '/';
|
|
||||||
}
|
|
||||||
text += search;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text className={styles.linkUrl} c="gray" size="xs" lineClamp={1}>
|
<Text className={styles.linkUrl} c="gray" size="xs" lineClamp={1}>
|
||||||
{origin}
|
{origin}
|
||||||
<span className={styles.linkUrlPathname}>{text}</span>
|
{pathname !== '/' && pathname}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import PATHS from '#constants/paths';
|
import PATHS from '#core/constants/paths';
|
||||||
import { Link } from '@inertiajs/react';
|
import { Link } from '@inertiajs/react';
|
||||||
import { route } from '@izzyjs/route/client';
|
import { route } from '@izzyjs/route/client';
|
||||||
import { Anchor, Group, Text } from '@mantine/core';
|
import { Anchor, Group, Text } from '@mantine/core';
|
||||||
@@ -13,15 +13,16 @@ export function MantineFooter() {
|
|||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
{ link: route('privacy').url, label: t('privacy') },
|
{ link: route('privacy').path, label: t('privacy'), external: false },
|
||||||
{ link: route('terms').url, label: t('terms') },
|
{ link: route('terms').path, label: t('terms'), external: false },
|
||||||
{ link: PATHS.EXTENSION, label: 'Extension' },
|
{ link: PATHS.EXTENSION, label: 'Extension', external: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
const items = links.map((link) => (
|
const items = links.map((link) => (
|
||||||
<Anchor
|
<Anchor
|
||||||
c="dimmed"
|
c="dimmed"
|
||||||
component={Link}
|
// @ts-expect-error
|
||||||
|
component={link.external ? ExternalLink : Link}
|
||||||
key={link.label}
|
key={link.label}
|
||||||
href={link.link}
|
href={link.link}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Visibility } from '#enums/visibility';
|
|
||||||
import { Box, Group, SegmentedControl, Text, TextInput } from '@mantine/core';
|
import { Box, Group, SegmentedControl, Text, TextInput } from '@mantine/core';
|
||||||
import { FormEvent } from 'react';
|
import { FormEvent } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import BackToDashboard from '~/components/common/navigation/back_to_dashboard';
|
import BackToDashboard from '~/components/common/navigation/back_to_dashboard';
|
||||||
import { FormLayout, FormLayoutProps } from '~/layouts/form_layout';
|
import { FormLayout, FormLayoutProps } from '~/layouts/form_layout';
|
||||||
import { Collection } from '~/types/app';
|
import { Collection, Visibility } from '~/types/app';
|
||||||
|
|
||||||
export type FormCollectionData = {
|
export type FormCollectionData = {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import PATHS from '#constants/paths';
|
import PATHS from '#core/constants/paths';
|
||||||
import { Link } from '@inertiajs/react';
|
import { Link } from '@inertiajs/react';
|
||||||
import { route } from '@izzyjs/route/client';
|
import { route } from '@izzyjs/route/client';
|
||||||
import {
|
import {
|
||||||
@@ -17,7 +17,6 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import ExternalLink from '~/components/common/external_link';
|
import ExternalLink from '~/components/common/external_link';
|
||||||
import { MantineLanguageSwitcher } from '~/components/common/language_switcher';
|
import { MantineLanguageSwitcher } from '~/components/common/language_switcher';
|
||||||
import { MantineThemeSwitcher } from '~/components/common/theme_switcher';
|
import { MantineThemeSwitcher } from '~/components/common/theme_switcher';
|
||||||
import { MantineUserCard } from '~/components/common/user_card';
|
|
||||||
import useUser from '~/hooks/use_user';
|
import useUser from '~/hooks/use_user';
|
||||||
import classes from './mobile.module.css';
|
import classes from './mobile.module.css';
|
||||||
|
|
||||||
@@ -31,7 +30,9 @@ export default function Navbar() {
|
|||||||
<Box pb={40}>
|
<Box pb={40}>
|
||||||
<header className={classes.header}>
|
<header className={classes.header}>
|
||||||
<Group justify="space-between" h="100%">
|
<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">
|
<Group h="100%" gap={0} visibleFrom="sm">
|
||||||
<Link href="/" className={classes.link}>
|
<Link href="/" className={classes.link}>
|
||||||
@@ -102,11 +103,13 @@ export default function Navbar() {
|
|||||||
|
|
||||||
<Group justify="center" grow pb="xl" px="md">
|
<Group justify="center" grow pb="xl" px="md">
|
||||||
{!isAuthenticated ? (
|
{!isAuthenticated ? (
|
||||||
<Button component="a" href={route('auth').path}>
|
<Button component="a" href={route('auth').path} w={110}>
|
||||||
{t('login')}
|
{t('login')}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<MantineUserCard />
|
<Button component={Link} href={route('dashboard').path} w={110}>
|
||||||
|
Dashboard
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Visibility } from '#enums/visibility';
|
|
||||||
import { ActionIcon, Anchor, CopyButton, Popover, Text } from '@mantine/core';
|
import { ActionIcon, Anchor, CopyButton, Popover, Text } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { TbShare3 } from 'react-icons/tb';
|
import { TbShare3 } from 'react-icons/tb';
|
||||||
import { Fragment } from 'react/jsx-runtime';
|
import { Fragment } from 'react/jsx-runtime';
|
||||||
import { generateShareUrl } from '~/lib/navigation';
|
import { generateShareUrl } from '~/lib/navigation';
|
||||||
import { useActiveCollection } from '~/stores/collection_store';
|
import { useActiveCollection } from '~/stores/collection_store';
|
||||||
|
import { Visibility } from '~/types/app';
|
||||||
|
|
||||||
const COPY_SUCCESS_TIMEOUT = 2_000;
|
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 { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useGlobalHotkeysStore } from '~/stores/global_hotkeys_store';
|
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 { ColorSchemeScript, createTheme, MantineProvider } from '@mantine/core';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { ReactNode, useEffect } from 'react';
|
import { ReactNode, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { InertiaPage } from '~/types/inertia';
|
||||||
|
|
||||||
import '@mantine/core/styles.css';
|
import '@mantine/core/styles.css';
|
||||||
import '@mantine/spotlight/styles.css';
|
import '@mantine/spotlight/styles.css';
|
||||||
@@ -14,6 +15,8 @@ const TRANSITION_OUT_CLASS = '__transition_fadeOut';
|
|||||||
|
|
||||||
export default function BaseLayout({ children }: { children: ReactNode }) {
|
export default function BaseLayout({ children }: { children: ReactNode }) {
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
|
const { language } = usePage<InertiaPage>().props;
|
||||||
|
i18n.changeLanguage(language);
|
||||||
dayjs.locale(i18n.language);
|
dayjs.locale(i18n.language);
|
||||||
|
|
||||||
const findAppElement = () => document.getElementById('app');
|
const findAppElement = () => document.getElementById('app');
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Visibility } from '#enums/visibility';
|
|
||||||
import { useForm } from '@inertiajs/react';
|
import { useForm } from '@inertiajs/react';
|
||||||
import { route } from '@izzyjs/route/client';
|
import { route } from '@izzyjs/route/client';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
@@ -6,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import MantineFormCollection, {
|
import MantineFormCollection, {
|
||||||
FormCollectionData,
|
FormCollectionData,
|
||||||
} from '~/components/form/form_collection';
|
} from '~/components/form/form_collection';
|
||||||
|
import { Visibility } from '~/types/app';
|
||||||
|
|
||||||
export default function CreateCollectionPage({
|
export default function CreateCollectionPage({
|
||||||
disableHomeLink,
|
disableHomeLink,
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.__transition_fadeIn {
|
.__transition_fadeIn {
|
||||||
|
transform-origin: 50% top;
|
||||||
animation: fadeIn 0.15s ease both;
|
animation: fadeIn 0.15s ease both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.__transition_fadeOut {
|
.__transition_fadeOut {
|
||||||
|
transform-origin: 50% top;
|
||||||
animation: fadeOut 0.15s ease both;
|
animation: fadeOut 0.15s ease both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,13 @@
|
|||||||
"extends": "@adonisjs/tsconfig/tsconfig.client.json",
|
"extends": "@adonisjs/tsconfig/tsconfig.client.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"target": "ESNext",
|
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Bundler",
|
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": ["./*"],
|
"~/*": ["./*"],
|
||||||
"@/*": ["../*"]
|
"config-ssr": ["../config/ssr"]
|
||||||
},
|
}
|
||||||
"types": ["vite/client"]
|
|
||||||
},
|
},
|
||||||
"include": ["./**/*.ts", "./**/*.tsx"]
|
"include": ["./**/*.ts", "./**/*.tsx", "../config/ssr.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { Visibility } from '@/app/enums/visibility';
|
|
||||||
|
|
||||||
type CommonBase = {
|
type CommonBase = {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type User = CommonBase & {
|
export type User = CommonBase & {
|
||||||
email: string;
|
email: string;
|
||||||
fullname: string;
|
fullname: string;
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
@@ -14,15 +12,15 @@ type User = CommonBase & {
|
|||||||
lastSeenAt: string;
|
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[];
|
collections: Collection[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type UserWithRelationCount = CommonBase & {
|
export type UserWithRelationCount = CommonBase & {
|
||||||
email: string;
|
email: string;
|
||||||
fullname: string;
|
fullname: string;
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
@@ -32,7 +30,7 @@ type UserWithRelationCount = CommonBase & {
|
|||||||
lastSeenAt: string;
|
lastSeenAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Link = CommonBase & {
|
export type Link = CommonBase & {
|
||||||
name: string;
|
name: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
url: string;
|
url: string;
|
||||||
@@ -40,14 +38,14 @@ type Link = CommonBase & {
|
|||||||
collectionId: number;
|
collectionId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type LinkWithCollection = Link & {
|
export type LinkWithCollection = Link & {
|
||||||
collection: Collection;
|
collection: Collection;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PublicLink = Omit<Link, 'favorite'>;
|
export type PublicLink = Omit<Link, 'favorite'>;
|
||||||
type PublicLinkWithCollection = Omit<Link, 'favorite'>;
|
export type PublicLinkWithCollection = Omit<Link, 'favorite'>;
|
||||||
|
|
||||||
type Collection = CommonBase & {
|
export type Collection = CommonBase & {
|
||||||
name: string;
|
name: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
visibility: Visibility;
|
visibility: Visibility;
|
||||||
@@ -55,6 +53,11 @@ type Collection = CommonBase & {
|
|||||||
authorId: number;
|
authorId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CollectionWithLinks = Collection & {
|
export type CollectionWithLinks = Collection & {
|
||||||
links: Link[];
|
links: Link[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum Visibility {
|
||||||
|
PUBLIC = 'PUBLIC',
|
||||||
|
PRIVATE = 'PRIVATE',
|
||||||
|
}
|
||||||
110
package.json
110
package.json
@@ -1,107 +1,107 @@
|
|||||||
{
|
{
|
||||||
"name": "my-links",
|
"name": "my-links",
|
||||||
"version": "3.0.0",
|
"version": "3.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node bin/server.js",
|
"start": "node bin/server.js",
|
||||||
"build": "node ace build",
|
"build": "node ace build",
|
||||||
"dev": "node ace serve --watch",
|
"dev": "node ace serve --hmr",
|
||||||
"test": "node ace test",
|
"test": "node ace test",
|
||||||
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
|
||||||
"format": "prettier --write --parser typescript '**/*.{ts,tsx}'",
|
"format": "prettier --write --parser typescript '**/*.{ts,tsx}'",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"release": "release-it"
|
"release": "release-it",
|
||||||
|
"generate-icons": "pwa-assets-generator"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"#controllers/*": "./app/controllers/*.js",
|
"#admin/*": "./app/admin/*.js",
|
||||||
"#exceptions/*": "./app/exceptions/*.js",
|
"#auth/*": "./app/auth/*.js",
|
||||||
"#models/*": "./app/models/*.js",
|
"#collections/*": "./app/collections/*.js",
|
||||||
"#constants/*": "./app/constants/*.js",
|
"#core/*": "./app/core/*.js",
|
||||||
"#enums/*": "./app/enums/*.js",
|
"#favicons/*": "./app/favicons/*.js",
|
||||||
"#mails/*": "./app/mails/*.js",
|
"#home/*": "./app/home/*.js",
|
||||||
"#services/*": "./app/services/*.js",
|
"#legal/*": "./app/legal/*.js",
|
||||||
"#listeners/*": "./app/listeners/*.js",
|
"#links/*": "./app/links/*.js",
|
||||||
"#events/*": "./app/events/*.js",
|
"#search/*": "./app/search/*.js",
|
||||||
"#middleware/*": "./app/middleware/*.js",
|
"#shared_collections/*": "./app/shared_collections/*.js",
|
||||||
"#validators/*": "./app/validators/*.js",
|
"#user/*": "./app/user/*.js",
|
||||||
"#providers/*": "./providers/*.js",
|
"#providers/*": "./providers/*.js",
|
||||||
"#policies/*": "./app/policies/*.js",
|
|
||||||
"#abilities/*": "./app/abilities/*.js",
|
|
||||||
"#database/*": "./database/*.js",
|
"#database/*": "./database/*.js",
|
||||||
"#tests/*": "./tests/*.js",
|
"#tests/*": "./tests/*.js",
|
||||||
"#start/*": "./start/*.js",
|
"#start/*": "./start/*.js",
|
||||||
"#config/*": "./config/*.js",
|
"#config/*": "./config/*.js"
|
||||||
"#lib/*": "./app/lib/*.js"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@adonisjs/assembler": "^7.8.2",
|
"@adonisjs/assembler": "^7.8.2",
|
||||||
"@adonisjs/eslint-config": "2.0.0-beta.6",
|
"@adonisjs/eslint-config": "2.0.0-beta.6",
|
||||||
"@adonisjs/prettier-config": "^1.4.0",
|
"@adonisjs/prettier-config": "^1.4.0",
|
||||||
"@adonisjs/tsconfig": "^1.4.0",
|
"@adonisjs/tsconfig": "^1.4.0",
|
||||||
"@faker-js/faker": "^9.2.0",
|
"@faker-js/faker": "^9.3.0",
|
||||||
"@japa/assert": "^3.0.0",
|
"@japa/assert": "^4.0.0",
|
||||||
"@japa/plugin-adonisjs": "^3.0.1",
|
"@japa/plugin-adonisjs": "^3.0.1",
|
||||||
"@japa/runner": "^3.1.4",
|
"@japa/runner": "^3.1.4",
|
||||||
"@swc/core": "^1.9.1",
|
"@swc/core": "^1.10.4",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^20.14.10",
|
"@types/node": "^22.10.4",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^19.0.2",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^19.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.13.0",
|
"@typescript-eslint/eslint-plugin": "^8.19.0",
|
||||||
"@vitejs/plugin-react": "^4.3.3",
|
"@vite-pwa/assets-generator": "^0.2.6",
|
||||||
"eslint": "^9.14.0",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"hot-hook": "^0.3.1",
|
"eslint": "^9.17.0",
|
||||||
"husky": "^9.1.6",
|
"hot-hook": "^0.4.0",
|
||||||
"lint-staged": "^15.2.10",
|
"husky": "^9.1.7",
|
||||||
"pino-pretty": "^11.3.0",
|
"lint-staged": "^15.3.0",
|
||||||
"postcss": "^8.4.47",
|
"pino-pretty": "^13.0.0",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.4.2",
|
||||||
"release-it": "^17.10.0",
|
"release-it": "^17.11.0",
|
||||||
"ts-node-maintained": "^10.9.4",
|
"ts-node-maintained": "^10.9.4",
|
||||||
"typescript": "~5.6.3",
|
"typescript": "~5.7.2",
|
||||||
"vite": "^5.4.10"
|
"vite": "^6.0.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adonisjs/ally": "^5.0.2",
|
"@adonisjs/ally": "^5.0.2",
|
||||||
"@adonisjs/auth": "^9.2.3",
|
"@adonisjs/auth": "^9.3.0",
|
||||||
"@adonisjs/core": "^6.14.1",
|
"@adonisjs/core": "^6.17.0",
|
||||||
"@adonisjs/cors": "^2.2.1",
|
"@adonisjs/cors": "^2.2.1",
|
||||||
"@adonisjs/inertia": "^1.2.3",
|
"@adonisjs/inertia": "^2.1.2",
|
||||||
"@adonisjs/lucid": "^21.3.0",
|
"@adonisjs/lucid": "^21.6.0",
|
||||||
"@adonisjs/session": "^7.5.0",
|
"@adonisjs/session": "^7.5.0",
|
||||||
"@adonisjs/shield": "^8.1.1",
|
"@adonisjs/shield": "^8.1.1",
|
||||||
"@adonisjs/static": "^1.1.1",
|
"@adonisjs/static": "^1.1.1",
|
||||||
"@adonisjs/vite": "^3.0.0",
|
"@adonisjs/vite": "^4.0.0",
|
||||||
"@inertiajs/react": "^1.2.0",
|
"@inertiajs/react": "^2.0.0",
|
||||||
"@izzyjs/route": "^1.2.0",
|
"@izzyjs/route": "^1.2.0",
|
||||||
"@mantine/core": "^7.13.4",
|
"@mantine/core": "^7.15.2",
|
||||||
"@mantine/hooks": "^7.13.4",
|
"@mantine/hooks": "^7.15.2",
|
||||||
"@mantine/spotlight": "^7.13.5",
|
"@mantine/spotlight": "^7.15.2",
|
||||||
"@vinejs/vine": "^2.1.0",
|
"@vinejs/vine": "^3.0.0",
|
||||||
"bentocache": "^1.0.0-beta.9",
|
"bentocache": "^1.0.0-beta.9",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"edge.js": "^6.2.0",
|
"edge.js": "^6.2.0",
|
||||||
"i18next": "^23.16.4",
|
"i18next": "^24.2.0",
|
||||||
"knex": "^3.1.0",
|
"knex": "^3.1.0",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
"node-html-parser": "^6.1.13",
|
"node-html-parser": "^7.0.1",
|
||||||
"pg": "^8.13.1",
|
"pg": "^8.13.1",
|
||||||
"react": "^18.3.1",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^19.0.0",
|
||||||
"react-hotkeys-hook": "^4.6.1",
|
"react-hotkeys-hook": "^4.6.1",
|
||||||
"react-i18next": "^15.1.0",
|
"react-i18next": "^15.4.0",
|
||||||
"react-icons": "^5.3.0",
|
"react-icons": "^5.4.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"zustand": "^5.0.1"
|
"vite-plugin-pwa": "^0.21.1",
|
||||||
|
"zustand": "^5.0.2"
|
||||||
},
|
},
|
||||||
"hotHook": {
|
"hotHook": {
|
||||||
"boundaries": [
|
"boundaries": [
|
||||||
"./app/controllers/**/*.ts",
|
"./app/**/controllers/*.ts",
|
||||||
"./app/middleware/*.ts"
|
"./app/**/middleware/*.ts"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"prettier": {
|
"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