2 Commits

Author SHA1 Message Date
Sonny
05f067a430 feat: remove inactive users (scheduler) 2024-09-18 16:59:16 +02:00
Sonny
b0e3bfa0f6 fix: favorite link and search result styles 2024-09-17 16:14:25 +02:00
18 changed files with 796 additions and 836 deletions

View File

@@ -13,4 +13,4 @@ DB_PASSWORD=my-links-pwd
DB_DATABASE=my-links
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_CLIENT_CALLBACK_URL=http://localhost:3333/auth/callback
GOOGLE_CLIENT_CALLBACK_URL=http://localhost:3333/auth/callback

12
.vscode/settings.json vendored
View File

@@ -1,3 +1,13 @@
{
"typescript.preferences.importModuleSpecifier": "non-relative"
"typescript.preferences.importModuleSpecifier": "non-relative",
/* Prefer tabs over spaces for accessibility */
"editor.insertSpaces": true,
"editor.detectIndentation": false,
/* Explorer */
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"*.js": "${capture}.js.map, ${capture}.min.js, ${capture}.d.ts",
"package.json": "package-lock.json, yarn.lock, pnpm-lock.yaml, rollup.config.mjs, tsconfig.json, eslint.config.js",
"Makefile": "*compose.yml, Dockerfile, .dockerignore, *startup.sh"
}
}

View File

@@ -53,9 +53,10 @@ ENV PORT=$PORT
WORKDIR /app
COPY --from=production-deps /app/node_modules /app/node_modules
COPY --from=build /app/build /app
COPY --from=build /app/startup.sh /app/startup.sh
# Expose port
EXPOSE $PORT
# Start app
CMD node bin/console.js migration:run --force && node bin/server.js
CMD node bin/console.js migration:run --force && sh startup.sh

View File

@@ -2,7 +2,7 @@ dev:
@docker compose down
@docker compose -f dev.docker-compose.yml up -d --wait
@node ace migration:fresh
@pnpm run dev
@./dev.startup.sh
prod:
@docker compose -f dev.docker-compose.yml down

View File

@@ -14,6 +14,7 @@ export default defineConfig({
() => import('@adonisjs/core/commands'),
() => import('@adonisjs/lucid/commands'),
() => import('@izzyjs/route/commands'),
() => import('adonisjs-scheduler/commands'),
],
/*
@@ -45,6 +46,10 @@ export default defineConfig({
() => import('@adonisjs/ally/ally_provider'),
() => import('@izzyjs/route/izzy_provider'),
() => import('#providers/route_provider'),
{
file: () => import('adonisjs-scheduler/scheduler_provider'),
environment: ['console'],
},
],
/*
@@ -55,7 +60,14 @@ export default defineConfig({
| List of modules to import before starting the application.
|
*/
preloads: [() => import('#start/routes'), () => import('#start/kernel')],
preloads: [
() => import('#start/routes'),
() => import('#start/kernel'),
{
file: () => import('#start/scheduler'),
environment: ['console'],
},
],
/*
|--------------------------------------------------------------------------

View File

@@ -25,9 +25,11 @@ export default class CollectionsController {
return response.redirectToNamedRoute('dashboard');
}
// TODO: Create DTOs
return inertia.render('dashboard', {
collections,
activeCollection: activeCollection || collections[0],
collections: collections.map((collection) => collection.serialize()),
activeCollection:
activeCollection?.serialize() || collections[0].serialize(),
});
}

View File

@@ -4,6 +4,8 @@ import logger from '@adonisjs/core/services/logger';
import db from '@adonisjs/lucid/services/db';
import { RouteName } from '@izzyjs/route/types';
const INACTIVE_USER_THRESHOLD = 7;
export default class UsersController {
private redirectTo: RouteName = 'auth.login';
@@ -75,4 +77,18 @@ export default class UsersController {
.withCount('collections', (q) => q.as('totalCollections'))
.withCount('links', (q) => q.as('totalLinks'));
}
async getAllInactiveUsers() {
const users = await this.getAllUsersWithTotalRelations();
const inactiveUsers = users.filter((user) => {
const totalLinks = Number(user.$extras.totalLinks);
const totalCollections = Number(user.$extras.totalCollections);
const isOlderThan7Days =
Math.abs(user.updatedAt.diffNow('days').days) > INACTIVE_USER_THRESHOLD;
return (totalLinks === 0 || totalCollections === 0) && isOlderThan7Days;
});
return inactiveUsers ?? [];
}
}

View File

@@ -0,0 +1,18 @@
import UsersController from '#controllers/users_controller';
import { inject } from '@adonisjs/core';
import { BaseCommand } from '@adonisjs/core/ace';
import type { CommandOptions } from '@adonisjs/core/types/ace';
export default class RemoveInactiveUsers extends BaseCommand {
static commandName = 'remove:inactive-users';
static description = '';
static options: CommandOptions = {};
@inject()
async run(usersController: UsersController) {
const inactiveUsers = await usersController.getAllInactiveUsers();
await Promise.all(inactiveUsers.map((user) => user.delete()));
console.log(`Removed ${inactiveUsers.length} inactive users`);
}
}

6
dev.startup.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
(trap 'kill 0' SIGINT; node ace scheduler:run & pnpm run dev)
wait -n
exit $?

View File

@@ -80,6 +80,7 @@ export default function DashboardProviders(
enabled: globalHotkeysEnabled,
}
);
return (
<CollectionsContext.Provider value={collectionsContextValue}>
<ActiveCollectionContext.Provider value={activeCollectionContextValue}>

View File

@@ -15,7 +15,7 @@ const SearchItemStyle = styled('li', {
shouldForwardProp: (propName) => propName !== 'isActive',
})<{ isActive: boolean }>(({ theme, isActive }) => ({
fontSize: '16px',
backgroundColor: isActive ? theme.colors.background : 'transparent',
backgroundColor: isActive ? theme.colors.secondary : 'transparent',
display: 'flex',
gap: '0.35em',
alignItems: 'center',

View File

@@ -16,6 +16,7 @@ import { appendCollectionId, appendLinkId } from '~/lib/navigation';
import { LinkWithCollection } from '~/types/app';
const FavoriteItemStyle = styled(ItemExternalLink)(({ theme }) => ({
height: 'auto',
backgroundColor: theme.colors.secondary,
}));
@@ -23,13 +24,20 @@ const FavoriteDropdown = styled(Dropdown)(({ theme }) => ({
backgroundColor: theme.colors.secondary,
}));
const FavoriteContainer = styled.div({
flex: 1,
lineHeight: '1.1rem',
});
export default function FavoriteItem({ link }: { link: LinkWithCollection }) {
const { t } = useTranslation();
return (
<FavoriteItemStyle href={link.url}>
<LinkFavicon url={link.url} size={24} />
<TextEllipsis>{link.name}</TextEllipsis>
<Legend>({link.collection.name})</Legend>
<FavoriteContainer>
<TextEllipsis>{link.name}</TextEllipsis>
<Legend>{link.collection.name}</Legend>
</FavoriteContainer>
<FavoriteDropdown
onClick={(event) => {
event.preventDefault();

View File

@@ -14,7 +14,7 @@ const ProfileStyle = styled(UnstyledList)({
gap: '1.25em',
});
const Column = styled.li({
const Column = styled.div({
display: 'flex',
gap: '1rem',
flexDirection: 'column',

View File

@@ -33,7 +33,8 @@
"#tests/*": "./tests/*.js",
"#start/*": "./start/*.js",
"#config/*": "./config/*.js",
"#lib/*": "./app/lib/*.js"
"#lib/*": "./app/lib/*.js",
"#routes/*": "./start/routes/*.js"
},
"devDependencies": {
"@adonisjs/assembler": "^7.8.2",
@@ -81,6 +82,7 @@
"@izzyjs/route": "^1.1.0-0",
"@tanstack/react-table": "^8.20.5",
"@vinejs/vine": "^2.1.0",
"adonisjs-scheduler": "^1.0.5",
"bentocache": "^1.0.0-beta.9",
"dayjs": "^1.11.13",
"edge.js": "^6.0.2",
@@ -124,4 +126,4 @@
"volta": {
"node": "22.2.0"
}
}
}

1505
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
import './routes/admin.js';
import './routes/app.js';
import './routes/auth.js';
import './routes/collection.js';
import './routes/favicon.js';
import './routes/link.js';
import './routes/search.js';
import './routes/shared_collection.js';
import '#routes/admin';
import '#routes/app';
import '#routes/auth';
import '#routes/collection';
import '#routes/favicon';
import '#routes/link';
import '#routes/search';
import '#routes/shared_collection';

3
start/scheduler.ts Normal file
View File

@@ -0,0 +1,3 @@
import scheduler from 'adonisjs-scheduler/services/main';
scheduler.command('remove:inactive-users').cron('0 20 * * *');

6
startup.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
(trap 'kill 0' SIGINT; node ace scheduler:run & pnpm start)
wait -n
exit $?