Compare commits

..

12 Commits

Author SHA1 Message Date
640e25ce12 chore(deps): update dependency drizzle-kit to v0.31.7
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
Lint / Lint and Check (push) Successful in 32s
2025-11-16 21:02:11 +00:00
55a0729628 try db lazy loading for coolify build
All checks were successful
Lint / Lint and Check (push) Successful in 45s
2025-11-15 19:24:32 +01:00
5bb861d881 env config
All checks were successful
Lint / Lint and Check (push) Successful in 41s
2025-11-15 17:23:10 +01:00
2f6b0ed098 remove dotenv
All checks were successful
Lint / Lint and Check (push) Successful in 41s
2025-11-15 17:00:33 +01:00
b630a0ca33 linter issues
All checks were successful
Lint / Lint and Check (push) Successful in 40s
2025-11-15 16:08:05 +01:00
2089e5d01d fix eslint & update packages 2025-11-15 16:03:45 +01:00
a18ae4f3df next 16 2025-11-15 15:39:31 +01:00
d03ed2b4d5 chore(deps): update pnpm to v10.22.0
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
Lint / Lint and Check (push) Successful in 30s
2025-11-15 14:03:25 +00:00
f69c73392e chore(deps): update dependency @tanstack/react-query to v5.90.8
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
Lint / Lint and Check (push) Successful in 29s
2025-11-15 13:02:59 +00:00
a2550f370e chore(deps): update pnpm to v10.21.0
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
Lint / Lint and Check (push) Successful in 37s
2025-11-15 08:03:42 +00:00
0272f71822 chore(deps): update dependency tailwind-merge to v3.4.0
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
Lint / Lint and Check (push) Successful in 30s
2025-11-15 07:04:10 +00:00
1278c134a9 chore(deps): update dependency @types/react-dom to v19.2.3
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
Lint / Lint and Check (push) Successful in 30s
2025-11-15 06:05:27 +00:00
12 changed files with 725 additions and 787 deletions

View File

@@ -4,7 +4,7 @@ on:
pull_request: pull_request:
push: push:
branches: branches:
- "**" # matches every branch - '**' # matches every branch
jobs: jobs:
lint_and_check: lint_and_check:
@@ -22,10 +22,10 @@ jobs:
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with: with:
node-version: 24 node-version: 24
cache: "pnpm" cache: 'pnpm'
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
- name: Run check - name: Run check
run: pnpm run check run: pnpm run lint

View File

@@ -73,11 +73,11 @@ interface HabitResponse {
export default function Dashboard() { export default function Dashboard() {
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [userToken, setUserToken] = useState<string | null>(null);
const [showNewHabitDialog, setShowNewHabitDialog] = useState(false); const [showNewHabitDialog, setShowNewHabitDialog] = useState(false);
const [newHabitName, setNewHabitName] = useState(''); const [newHabitName, setNewHabitName] = useState('');
const [newHabitType, setNewHabitType] = useState<'positive' | 'neutral' | 'negative'>('neutral'); const [newHabitType, setNewHabitType] = useState<'positive' | 'neutral' | 'negative'>('neutral');
const [copiedToken, setCopiedToken] = useState(false); const [copiedToken, setCopiedToken] = useState(false);
const [currentTime, setCurrentTime] = useState(() => Date.now());
// Check authentication // Check authentication
const { data: authData, isLoading: authLoading } = useQuery<AuthData>({ const { data: authData, isLoading: authLoading } = useQuery<AuthData>({
@@ -92,11 +92,15 @@ export default function Dashboard() {
}, },
}); });
// Update current time periodically to avoid impure Date.now() calls during render
useEffect(() => { useEffect(() => {
if (authData?.token) { const interval = setInterval(() => {
setUserToken(authData.token); setCurrentTime(Date.now());
} }, 60000); // Update every minute
}, [authData]); return () => {
clearInterval(interval);
};
}, []);
// Fetch habits // Fetch habits
const { data: habitsData, isLoading: habitsLoading } = useQuery<HabitsResponse>({ const { data: habitsData, isLoading: habitsLoading } = useQuery<HabitsResponse>({
@@ -154,8 +158,8 @@ export default function Dashboard() {
}; };
const copyToken = () => { const copyToken = () => {
if (userToken) { if (authData?.token) {
void navigator.clipboard.writeText(userToken); void navigator.clipboard.writeText(authData.token);
setCopiedToken(true); setCopiedToken(true);
setTimeout(() => { setTimeout(() => {
setCopiedToken(false); setCopiedToken(false);
@@ -199,7 +203,7 @@ export default function Dashboard() {
const getAverageFrequency = (habit: Habit) => { const getAverageFrequency = (habit: Habit) => {
const daysSinceCreation = Math.max( const daysSinceCreation = Math.max(
1, 1,
Math.floor((Date.now() - new Date(habit.createdAt).getTime()) / (1000 * 60 * 60 * 24)), Math.floor((currentTime - new Date(habit.createdAt).getTime()) / (1000 * 60 * 60 * 24)),
); );
if (daysSinceCreation <= 7) { if (daysSinceCreation <= 7) {
@@ -320,13 +324,13 @@ export default function Dashboard() {
</div> </div>
{/* Token Alert */} {/* Token Alert */}
{userToken && ( {authData?.token && (
<Alert className="border-zinc-800 bg-zinc-950"> <Alert className="border-zinc-800 bg-zinc-950">
<AlertDescription className="flex items-center justify-between"> <AlertDescription className="flex items-center justify-between">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-sm text-zinc-400">Your access token:</p> <p className="text-sm text-zinc-400">Your access token:</p>
<code className="rounded bg-zinc-900 px-2 py-1 font-mono text-sm text-white"> <code className="rounded bg-zinc-900 px-2 py-1 font-mono text-sm text-white">
{userToken} {authData.token}
</code> </code>
</div> </div>
<TooltipProvider> <TooltipProvider>

View File

@@ -1,7 +1,8 @@
'use client'; 'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactNode, useState } from 'react'; import { useState } from 'react';
import type { ReactNode } from 'react';
export function Providers({ children }: { children: ReactNode }) { export function Providers({ children }: { children: ReactNode }) {
const [queryClient] = useState( const [queryClient] = useState(

View File

@@ -1,6 +1,5 @@
import { defineConfig } from 'drizzle-kit'; import { defineConfig } from 'drizzle-kit';
import dotenv from 'dotenv'; import '@/lib/env-config';
dotenv.config({ path: ['.env.local', '.env'] });
const DATABASE_URL = process.env.POSTGRES_URL; const DATABASE_URL = process.env.POSTGRES_URL;
if (!DATABASE_URL) { if (!DATABASE_URL) {

View File

@@ -1,29 +1,19 @@
import js from '@eslint/js'; // @ts-check
import { defineConfig, globalIgnores } from 'eslint/config';
import nextVitals from 'eslint-config-next/core-web-vitals';
import nextTs from 'eslint-config-next/typescript';
import tseslint from 'typescript-eslint'; import tseslint from 'typescript-eslint';
import nextPlugin from '@next/eslint-plugin-next';
export default tseslint.config( const eslintConfig = defineConfig([
// Base recommended configs // Next.js core-web-vitals and TypeScript configs
js.configs.recommended, ...nextVitals,
...nextTs,
// Next.js recommended configs // Add strict TypeScript rules on top
{
plugins: {
'@next/next': nextPlugin,
},
rules: {
...nextPlugin.configs.recommended.rules,
...nextPlugin.configs['core-web-vitals'].rules,
},
},
// TypeScript configs
...tseslint.configs.recommended,
...tseslint.configs.strictTypeChecked, ...tseslint.configs.strictTypeChecked,
...tseslint.configs.stylisticTypeChecked, ...tseslint.configs.stylisticTypeChecked,
// Configure TypeScript parser options
// Project-specific configuration
{ {
files: ['**/*.{ts,tsx}'],
languageOptions: { languageOptions: {
parserOptions: { parserOptions: {
projectService: true, projectService: true,
@@ -31,30 +21,18 @@ export default tseslint.config(
}, },
}, },
}, },
// Override default ignores of eslint-config-next
globalIgnores([
// Default ignores of eslint-config-next:
'.next/**',
'out/**',
'build/**',
'next-env.d.ts',
// Additional ignores:
'*.mjs',
'tailwind.config.ts',
'eslint.config.js',
]),
]);
// Next.js specific overrides export default eslintConfig;
{
files: ['**/*.{js,jsx,ts,tsx}'],
rules: {
// Next.js already handles React imports
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
},
},
// Config files don't need strict type checking
{
files: ['**/*.config.{js,ts,mjs}', 'tailwind.config.{js,ts}'],
...tseslint.configs.disableTypeChecked,
},
// Ignore build outputs and dependencies
{
ignores: ['.next/**', 'node_modules/**', 'dist/**', 'build/**', 'drizzle/**/*.sql'],
},
);

View File

@@ -1,4 +1,5 @@
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
import '@/lib/env-config';
const TOKEN_COOKIE_NAME = 'habit-tracker-token'; const TOKEN_COOKIE_NAME = 'habit-tracker-token';
const TOKEN_COOKIE_OPTIONS = { const TOKEN_COOKIE_OPTIONS = {

View File

@@ -1,14 +1,28 @@
import { drizzle } from 'drizzle-orm/node-postgres'; import { drizzle } from 'drizzle-orm/node-postgres';
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
import * as schema from './schema'; import * as schema from './schema';
import dotenv from 'dotenv'; import '@/lib/env-config';
dotenv.config({ path: ['.env.local', '.env'] });
let _db: NodePgDatabase<typeof schema> | null = null;
function getDb() {
if (!_db) {
const DATABASE_URL = process.env.POSTGRES_URL; const DATABASE_URL = process.env.POSTGRES_URL;
if (!DATABASE_URL) { if (!DATABASE_URL) {
throw new Error('POSTGRES_URL environment variable is required'); throw new Error('POSTGRES_URL environment variable is required');
} }
_db = drizzle(DATABASE_URL, { schema });
}
return _db;
}
export const db = drizzle(DATABASE_URL, { schema }); export const db = new Proxy({} as NodePgDatabase<typeof schema>, {
get(target, prop) {
const database = getDb();
const value = database[prop as keyof typeof database];
return typeof value === 'function' ? value.bind(database) : value;
},
});
// Re-export schema types for convenience // Re-export schema types for convenience
export * from './schema'; export * from './schema';

4
lib/env-config.ts Normal file
View File

@@ -0,0 +1,4 @@
import { loadEnvConfig } from '@next/env';
const projectDir = process.cwd();
loadEnvConfig(projectDir);

View File

@@ -7,7 +7,7 @@
"dev": "next dev --turbopack", "dev": "next dev --turbopack",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"check": "next lint && npx tsc --noEmit", "lint": "next typegen && eslint . && npx tsc --noEmit",
"db:generate": "drizzle-kit generate", "db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate", "db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push", "db:push": "drizzle-kit push",
@@ -16,45 +16,42 @@
"format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache" "format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache"
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-dialog": "^1.1.14", "@next/env": "^16.0.3",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-select": "^2.2.5", "@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-tooltip": "^1.2.7", "@radix-ui/react-slot": "^1.2.4",
"@tanstack/react-query": "^5.83.0", "@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-query": "^5.90.9",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cssnano": "^7.1.0", "cssnano": "^7.1.2",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dotenv": "^17.2.0", "drizzle-orm": "^0.44.7",
"drizzle-orm": "^0.44.3",
"lucide-react": "^0.553.0", "lucide-react": "^0.553.0",
"nanoid": "^5.1.5", "nanoid": "^5.1.6",
"next": "16.0.1", "next": "16.0.3",
"next-plausible": "^3.12.4", "next-plausible": "^3.12.5",
"pg": "^8.16.3", "pg": "^8.16.3",
"pg-native": "^3.5.2", "pg-native": "^3.5.2",
"postcss-flexbugs-fixes": "^5.0.2", "postcss-flexbugs-fixes": "^5.0.2",
"postcss-preset-env": "^10.2.4", "postcss-preset-env": "^10.4.0",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "9.39.1",
"@next/eslint-plugin-next": "16.0.1",
"@tailwindcss/postcss": "4.1.17", "@tailwindcss/postcss": "4.1.17",
"@types/node": "24.10.1", "@types/node": "24.10.1",
"@types/pg": "8.15.6", "@types/pg": "8.15.6",
"@types/react": "19.2.3", "@types/react": "19.2.5",
"@types/react-dom": "19.2.2", "@types/react-dom": "19.2.3",
"@typescript-eslint/eslint-plugin": "8.46.4", "drizzle-kit": "0.31.7",
"@typescript-eslint/parser": "8.46.4",
"drizzle-kit": "0.31.6",
"eslint": "9.39.1", "eslint": "9.39.1",
"eslint-config-next": "16.0.1", "eslint-config-next": "16.0.3",
"eslint-config-prettier": "^10.1.8",
"postcss": "8.5.6", "postcss": "8.5.6",
"prettier": "3.6.2", "prettier": "3.6.2",
"prettier-plugin-tailwindcss": "0.7.1", "prettier-plugin-tailwindcss": "0.7.1",
@@ -63,5 +60,11 @@
"typescript": "5.9.3", "typescript": "5.9.3",
"typescript-eslint": "8.46.4" "typescript-eslint": "8.46.4"
}, },
"packageManager": "pnpm@10.20.0" "packageManager": "pnpm@10.22.0",
"pnpm": {
"overrides": {
"@types/react": "19.2.5",
"@types/react-dom": "19.2.3"
}
}
} }

1298
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server'; import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) { export function proxy(request: NextRequest) {
const token = request.cookies.get('habit-tracker-token'); const token = request.cookies.get('habit-tracker-token');
const isAuthPage = request.nextUrl.pathname === '/welcome'; const isAuthPage = request.nextUrl.pathname === '/welcome';
const isDashboard = request.nextUrl.pathname === '/dashboard'; const isDashboard = request.nextUrl.pathname === '/dashboard';

View File

@@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, "allowJs": false,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
"noEmit": true, "noEmit": true,
@@ -10,18 +10,20 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "react-jsx",
"incremental": true, "incremental": true,
"plugins": [ "baseUrl": ".",
{
"name": "next"
}
],
"paths": { "paths": {
"@/*": ["./*"] "@/*": ["./*"]
}, },
"target": "ES2022" "target": "ES2022",
"forceConsistentCasingInFileNames": true,
"verbatimModuleSyntax": true,
"noUncheckedIndexedAccess": false,
"exactOptionalPropertyTypes": false,
"noImplicitReturns": false,
"plugins": [{ "name": "next" }]
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }