Compare commits
18 Commits
4206e69a35
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 640e25ce12 | |||
| 55a0729628 | |||
| 5bb861d881 | |||
| 2f6b0ed098 | |||
| b630a0ca33 | |||
| 2089e5d01d | |||
| a18ae4f3df | |||
| d03ed2b4d5 | |||
| f69c73392e | |||
| a2550f370e | |||
| 0272f71822 | |||
| 1278c134a9 | |||
| dc00bddfc0 | |||
| 7d500a04cd | |||
| 91fc73e57b | |||
| f016bfedd6 | |||
| 8d752d681d | |||
| 78305bcc9b |
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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'],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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
4
lib/env-config.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { loadEnvConfig } from '@next/env';
|
||||||
|
|
||||||
|
const projectDir = process.cwd();
|
||||||
|
loadEnvConfig(projectDir);
|
||||||
61
package.json
61
package.json
@@ -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,52 +16,55 @@
|
|||||||
"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.552.0",
|
"nanoid": "^5.1.6",
|
||||||
"nanoid": "^5.1.5",
|
"next": "16.0.3",
|
||||||
"next": "16.0.1",
|
"next-plausible": "^3.12.5",
|
||||||
"next-plausible": "^3.12.4",
|
|
||||||
"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.0",
|
"@types/node": "24.10.1",
|
||||||
"@types/pg": "8.15.6",
|
"@types/pg": "8.15.6",
|
||||||
"@types/react": "19.2.2",
|
"@types/react": "19.2.5",
|
||||||
"@types/react-dom": "19.2.2",
|
"@types/react-dom": "19.2.3",
|
||||||
"@typescript-eslint/eslint-plugin": "8.46.3",
|
"drizzle-kit": "0.31.7",
|
||||||
"@typescript-eslint/parser": "8.46.3",
|
|
||||||
"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",
|
||||||
"tailwindcss": "4.1.17",
|
"tailwindcss": "4.1.17",
|
||||||
"turbo": "2.6.0",
|
"turbo": "2.6.1",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "8.46.3"
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1538
pnpm-lock.yaml
generated
1538
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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';
|
||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user