From c88e5d9d82c7e38775e1baa2a38d9c2f194e928b Mon Sep 17 00:00:00 2001 From: Sean O'Connor Date: Sat, 29 Nov 2025 00:49:24 -0500 Subject: [PATCH] feat: Implement dynamic accent color selection and refactor appearance settings --- .env.example | 6 + .gitignore | 2 + README.md | 7 +- bun.lock | 11 +- docker-compose.yml | 32 +- drizzle.config.ts | 14 +- package.json | 12 +- scripts/clone-local.sh | 71 + .../_components/accent-color-selector.tsx | 0 .../_components/accent-color-switcher.tsx | 0 .../_components/appearance-settings.tsx | 13 + .../settings/_components/color-picker.tsx | 0 .../_components/color-theme-selector.tsx | 54 - .../settings/_components/mode-switcher.tsx | 14 +- .../settings/_components/settings-content.tsx | 6 +- .../settings/_components/theme-selector.tsx | 90 +- src/app/layout.tsx | 108 +- .../providers/color-theme-provider.tsx | 156 ++ src/components/providers/theme-provider.tsx | 83 +- .../theme/accent-color-switcher.tsx | 147 ++ src/components/theme/theme-switcher.tsx | 40 +- src/components/ui/tooltip.tsx | 61 + src/env.js | 5 +- src/lib/color-utils.ts | 274 ++++ src/styles/globals.css | 1295 ++++------------- tailwind.config.ts | 53 +- 26 files changed, 1319 insertions(+), 1235 deletions(-) create mode 100755 scripts/clone-local.sh create mode 100644 src/app/dashboard/settings/_components/accent-color-selector.tsx create mode 100644 src/app/dashboard/settings/_components/accent-color-switcher.tsx create mode 100644 src/app/dashboard/settings/_components/appearance-settings.tsx create mode 100644 src/app/dashboard/settings/_components/color-picker.tsx create mode 100644 src/components/providers/color-theme-provider.tsx create mode 100644 src/components/theme/accent-color-switcher.tsx create mode 100644 src/components/ui/tooltip.tsx create mode 100644 src/lib/color-utils.ts diff --git a/.env.example b/.env.example index 07ed22e..6ec8127 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,12 @@ HOSTNAME=0.0.0.0 AUTH_SECRET=replace-with-strong-secret # Database (Postgres) +# These are required for Docker container initialization +POSTGRES_USER=beenvoice +POSTGRES_PASSWORD=beenvoice +POSTGRES_DB=beenvoice + +# Connect string for the app DATABASE_URL=postgres://beenvoice:beenvoice@db:5432/beenvoice # Disable SSL for Docker local Postgres; set to false or remove for managed Postgres DB_DISABLE_SSL=true diff --git a/.gitignore b/.gitignore index c24a835..ffb43f1 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,8 @@ yarn-error.log* # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables .env .env*.local +.env*.production +.env*.vercel # vercel .vercel diff --git a/README.md b/README.md index e36a1cf..3e32f94 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ A modern, professional invoicing application built for freelancers and small bus - **Authentication**: NextAuth.js with email/password - **UI Components**: shadcn/ui with Tailwind CSS - **Styling**: Geist font family -- **Package Manager**: Bun (with npm fallback) +- **Package Manager**: Bun ## 📦 Installation @@ -44,11 +44,8 @@ A modern, professional invoicing application built for freelancers and small bus 2. **Install dependencies** ```bash - # Using Bun (recommended) + ```bash bun install - - # Or using npm - npm install ``` 3. **Set up environment variables** diff --git a/bun.lock b/bun.lock index 95d8257..7e0df4d 100644 --- a/bun.lock +++ b/bun.lock @@ -23,6 +23,7 @@ "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", "@react-pdf/renderer": "^4.3.1", "@t3-oss/env-nextjs": "^0.12.0", "@tanstack/react-query": "^5.90.10", @@ -48,7 +49,6 @@ "lucide-react": "^0.525.0", "next": "^15.5.6", "next-auth": "5.0.0-beta.25", - "next-themes": "^0.3.0", "pg": "^8.16.3", "react": "^19.2.0", "react-day-picker": "^9.11.2", @@ -72,6 +72,7 @@ "@types/raf": "^3.4.3", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", + "dotenv": "^17.2.3", "drizzle-kit": "^0.30.6", "eslint": "^9.39.1", "eslint-config-next": "^15.5.6", @@ -354,6 +355,8 @@ "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], @@ -806,6 +809,8 @@ "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], + "drizzle-kit": ["drizzle-kit@0.30.6", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g=="], "drizzle-orm": ["drizzle-orm@0.44.7", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ=="], @@ -1152,8 +1157,6 @@ "next-auth": ["next-auth@5.0.0-beta.25", "", { "dependencies": { "@auth/core": "0.37.2" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "next": "^14.0.0-0 || ^15.0.0-0", "nodemailer": "^6.6.5", "react": "^18.2.0 || ^19.0.0-0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-2dJJw1sHQl2qxCrRk+KTQbeH+izFbGFPuJj5eGgBZFYyiYYtvlrBeUw1E/OJJxTRjuxbSYGnCTkUIRsIIW0bog=="], - "next-themes": ["next-themes@0.3.0", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18", "react-dom": "^16.8 || ^17 || ^18" } }, "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w=="], - "normalize-svg-path": ["normalize-svg-path@1.1.0", "", { "dependencies": { "svg-arc-to-cubic-bezier": "^3.0.0" } }, "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg=="], "oauth4webapi": ["oauth4webapi@3.5.5", "", {}, "sha512-1K88D2GiAydGblHo39NBro5TebGXa+7tYoyIbxvqv3+haDDry7CBE1eSYuNbOSsYCCU6y0gdynVZAkm4YPw4hg=="], @@ -1542,6 +1545,8 @@ "@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@react-pdf/reconciler/scheduler": ["scheduler@0.25.0-rc-603e6108-20241029", "", {}, "sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], diff --git a/docker-compose.yml b/docker-compose.yml index 9649d83..98f213b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,9 @@ services: db: - image: postgres:16-alpine + image: postgres:17-alpine container_name: beenvoice-db - environment: - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_DB: ${POSTGRES_DB} + env_file: + - .env.local volumes: - beenvoice_pg_data:/var/lib/postgresql/data ports: @@ -16,30 +14,6 @@ services: timeout: 5s retries: 10 - app: - build: - context: . - dockerfile: Dockerfile - image: beenvoice-app:latest - container_name: beenvoice-app - depends_on: - db: - condition: service_healthy - environment: - NODE_ENV: production - # no memory constraint - AUTH_SECRET: ${AUTH_SECRET} - RESEND_API_KEY: ${RESEND_API_KEY} - RESEND_DOMAIN: ${RESEND_DOMAIN} - DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} - DB_DISABLE_SSL: "true" - PORT: ${PORT} - HOSTNAME: 0.0.0.0 - SKIP_ENV_VALIDATION: "1" - ports: - - "${PORT:-3000}:3000" - restart: unless-stopped - volumes: beenvoice_pg_data: driver: local diff --git a/drizzle.config.ts b/drizzle.config.ts index 9ce75bd..4ef0d89 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,12 +1,22 @@ import type { Config } from "drizzle-kit"; +import * as dotenv from "dotenv"; +// Load .env.local if it exists +dotenv.config({ path: ".env.local" }); +// Load .env if it exists (fallback) +dotenv.config({ path: ".env" }); + // Use a relative import; path alias "~" may not resolve in CLI context -import { env } from "./src/env.js"; +// import { env } from "./src/env.js"; + +if (!process.env.DATABASE_URL) { + throw new Error("DATABASE_URL is not set"); +} export default { schema: "./src/server/db/schema.ts", dialect: "postgresql", dbCredentials: { - url: env.DATABASE_URL, + url: process.env.DATABASE_URL, }, tablesFilter: ["beenvoice_*"], } satisfies Config; diff --git a/package.json b/package.json index e557366..fa110ec 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,9 @@ "db:migrate": "drizzle-kit migrate", "db:push": "drizzle-kit push", "db:studio": "drizzle-kit studio", - "docker-up": "colima start && docker-compose up -d", - "docker-down": "docker-compose down && colima stop", + "db:clone": "./scripts/clone-local.sh", + "docker:up": "colima start && docker-compose up -d", + "docker:down": "docker-compose down && colima stop", "deploy": "drizzle-kit push && next build", "dev": "next dev --turbo", "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache", @@ -41,6 +42,7 @@ "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", "@react-pdf/renderer": "^4.3.1", "@t3-oss/env-nextjs": "^0.12.0", "@tanstack/react-query": "^5.90.10", @@ -66,7 +68,6 @@ "lucide-react": "^0.525.0", "next": "^15.5.6", "next-auth": "5.0.0-beta.25", - "next-themes": "^0.3.0", "pg": "^8.16.3", "react": "^19.2.0", "react-day-picker": "^9.11.2", @@ -85,11 +86,12 @@ "@tailwindcss/postcss": "^4.1.17", "@types/bcryptjs": "^2.4.6", "@types/file-saver": "^2.0.7", - "@types/pg": "^8.15.6", "@types/node": "^20.19.25", + "@types/pg": "^8.15.6", "@types/raf": "^3.4.3", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", + "dotenv": "^17.2.3", "drizzle-kit": "^0.30.6", "eslint": "^9.39.1", "eslint-config-next": "^15.5.6", @@ -113,4 +115,4 @@ "sharp", "unrs-resolver" ] -} +} \ No newline at end of file diff --git a/scripts/clone-local.sh b/scripts/clone-local.sh new file mode 100755 index 0000000..33c31d1 --- /dev/null +++ b/scripts/clone-local.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +# Function to read a variable from a specific env file +read_env_var() { + local file="$1" + local var="$2" + if [ -f "$file" ]; then + grep "^$var=" "$file" | cut -d '=' -f2- | tr -d '"' | tr -d "'" + fi +} + +# 1. Get Production URL +# Priority: Argument > .env.production > .env +PROD_DB_URL="$1" + +if [ -z "$PROD_DB_URL" ]; then + echo "Checking .env.production for DATABASE_URL..." + PROD_DB_URL=$(read_env_var ".env.production" "DATABASE_URL") +fi + +if [ -z "$PROD_DB_URL" ]; then + echo "Checking .env for PROD_DATABASE_URL..." + PROD_DB_URL=$(read_env_var ".env" "PROD_DATABASE_URL") +fi + +if [ -z "$PROD_DB_URL" ]; then + echo "Error: Could not find production database URL." + echo "Please provide it as an argument, or set DATABASE_URL in .env.production, or PROD_DATABASE_URL in .env" + echo "Usage: $0 " + exit 1 +fi + +# 2. Get Target URL +# Priority: .env.local > .env +TARGET_DB_URL=$(read_env_var ".env.local" "DATABASE_URL") +if [ -z "$TARGET_DB_URL" ]; then TARGET_DB_URL=$(read_env_var ".env" "DATABASE_URL"); fi + +if [ -z "$TARGET_DB_URL" ]; then + echo "Error: Could not find target DATABASE_URL in .env.local or .env" + exit 1 +fi + +echo "Configuration:" +echo " Source: Production (Found in env/arg)" +echo " Target: $TARGET_DB_URL" +echo +echo "⚠️ WARNING: This will OVERWRITE the target database at the above URL." +echo "This is a one-time migration script." +read -p "Are you sure you want to continue? (y/N) " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Aborted." + exit 1 +fi + +echo "Cloning database..." + +# Pipe pg_dump from a temporary container to another temporary container running psql +# This avoids needing local tools and ensures consistent environment +docker run --rm -i postgres:17-alpine pg_dump "$PROD_DB_URL" \ + --clean --if-exists \ + --no-owner --no-privileges \ + --format=plain \ + | docker run --rm -i postgres:17-alpine psql "$TARGET_DB_URL" + +if [ $? -eq 0 ]; then + echo "✅ Database cloned successfully!" +else + echo "❌ Database clone failed." + exit 1 +fi diff --git a/src/app/dashboard/settings/_components/accent-color-selector.tsx b/src/app/dashboard/settings/_components/accent-color-selector.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/app/dashboard/settings/_components/accent-color-switcher.tsx b/src/app/dashboard/settings/_components/accent-color-switcher.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/app/dashboard/settings/_components/appearance-settings.tsx b/src/app/dashboard/settings/_components/appearance-settings.tsx new file mode 100644 index 0000000..df60518 --- /dev/null +++ b/src/app/dashboard/settings/_components/appearance-settings.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { ModeSwitcher } from "./mode-switcher"; +import { ThemeSelector } from "./theme-selector"; + +export function AppearanceSettings() { + return ( +
+ + +
+ ); +} diff --git a/src/app/dashboard/settings/_components/color-picker.tsx b/src/app/dashboard/settings/_components/color-picker.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/app/dashboard/settings/_components/color-theme-selector.tsx b/src/app/dashboard/settings/_components/color-theme-selector.tsx index ec0cb8a..e69de29 100644 --- a/src/app/dashboard/settings/_components/color-theme-selector.tsx +++ b/src/app/dashboard/settings/_components/color-theme-selector.tsx @@ -1,54 +0,0 @@ -"use client"; - -import { useTheme } from "next-themes"; -import { Check, Palette } from "lucide-react"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "~/components/ui/dropdown-menu"; -import { Button } from "~/components/ui/button"; - -export function ColorThemeSelector() { - const { theme, setTheme } = useTheme(); - - const themes = [ - { name: "theme-ocean", label: "Ocean" }, - { name: "theme-sunset", label: "Sunset" }, - { name: "theme-forest", label: "Forest" }, - ]; - - const currentTheme = themes.find((t) => t.name === theme)?.label ?? "Ocean"; - - return ( -
-
- -

- Select a color theme for the application. -

-
- - - - - - {themes.map((t) => ( - setTheme(t.name)} - > - {t.label} - {theme === t.name && } - - ))} - - -
- ); -} diff --git a/src/app/dashboard/settings/_components/mode-switcher.tsx b/src/app/dashboard/settings/_components/mode-switcher.tsx index 0cec1a5..f39ae89 100644 --- a/src/app/dashboard/settings/_components/mode-switcher.tsx +++ b/src/app/dashboard/settings/_components/mode-switcher.tsx @@ -1,6 +1,6 @@ "use client"; -import { useTheme } from "next-themes"; +import { useTheme } from "~/components/providers/theme-provider"; import { Sun, Moon, Laptop } from "lucide-react"; import { Tabs, TabsList, TabsTrigger } from "~/components/ui/tabs"; @@ -9,13 +9,19 @@ export function ModeSwitcher() { return (
-
+

- Select a light or dark mode, or sync with your system. + {theme === "system" + ? "Follows system preference" + : `Currently in ${theme} mode`}

- + setTheme(value as "light" | "dark" | "system")} + className="w-auto" + > diff --git a/src/app/dashboard/settings/_components/settings-content.tsx b/src/app/dashboard/settings/_components/settings-content.tsx index 8923ed6..b8d83db 100644 --- a/src/app/dashboard/settings/_components/settings-content.tsx +++ b/src/app/dashboard/settings/_components/settings-content.tsx @@ -62,8 +62,7 @@ import { Textarea } from "~/components/ui/textarea"; import { api } from "~/trpc/react"; import { Switch } from "~/components/ui/switch"; import { Slider } from "~/components/ui/slider"; -import { ModeSwitcher } from "./mode-switcher"; -import { ColorThemeSelector } from "./color-theme-selector"; +import { AppearanceSettings } from "./appearance-settings"; import { useAnimationPreferences } from "~/components/providers/animation-preferences-provider"; export function SettingsContent() { @@ -630,8 +629,7 @@ export function SettingsContent() { - - + diff --git a/src/app/dashboard/settings/_components/theme-selector.tsx b/src/app/dashboard/settings/_components/theme-selector.tsx index b534933..13be2fa 100644 --- a/src/app/dashboard/settings/_components/theme-selector.tsx +++ b/src/app/dashboard/settings/_components/theme-selector.tsx @@ -1,55 +1,61 @@ "use client"; -import { useTheme } from "next-themes"; -import { Check, Palette } from "lucide-react"; +import * as React from "react"; +import { Check } from "lucide-react"; +import { cn } from "~/lib/utils"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "~/components/ui/dropdown-menu"; + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "~/components/ui/tooltip"; import { Button } from "~/components/ui/button"; +import { useColorTheme } from "~/components/providers/color-theme-provider"; + +const themes = [ + { name: "slate", hex: "#64748b" }, + { name: "blue", hex: "#3b82f6" }, + { name: "green", hex: "#22c55e" }, + { name: "rose", hex: "#be123c" }, + { name: "orange", hex: "#ea580c" }, +]; export function ThemeSelector() { - const { theme, setTheme } = useTheme(); - - const themes = [ - { name: "light", label: "Light" }, - { name: "dark", label: "Dark" }, - { name: "theme-sunset", label: "Sunset" }, - { name: "theme-forest", label: "Forest" }, - ]; + const { colorTheme, setColorTheme } = useColorTheme(); return ( -
-
- -

- Select a theme for the application. -

-
- - - - - +
+ +

+ Select a theme for the application. +

+
+ {themes.map((t) => ( - setTheme(t.name)} - > - {t.label} - {theme === t.name && } - + + + + + +

{t.name}

+
+
))} - - +
+
); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index ff6e9cb..053c792 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -9,6 +9,7 @@ import { Toaster } from "~/components/ui/sonner"; import { AnimationPreferencesProvider } from "~/components/providers/animation-preferences-provider"; import { ThemeProvider } from "~/components/providers/theme-provider"; +import { ColorThemeProvider } from "~/components/providers/color-theme-provider"; export const metadata: Metadata = { title: "beenvoice - Invoicing Made Simple", @@ -46,27 +47,106 @@ export default function RootLayout({ className={`${geistSans.variable} ${geistMono.variable} ${instrumentSerif.variable}`} > - {/* Inline early animation preference script to avoid FOUC */} + {/* Inline early theme and animation preference script to avoid FOUC */}