I fixed the lightmode on the sidebar and added
more colour themes. I fixed the settings buttons.
This commit is contained in:
@@ -40,3 +40,7 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
.git
|
||||||
|
.git_bak
|
||||||
|
node_modules
|
||||||
|
target
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS company_settings (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
company_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
company_email VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
company_phone VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
company_website VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
company_address TEXT NOT NULL DEFAULT '',
|
||||||
|
updated_by UUID REFERENCES users(id),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO company_settings (company_name, company_email, company_phone, company_website, company_address)
|
||||||
|
VALUES ('Coastal IT Solutions', 'info@coastalit.com', '(555) 123-4567', 'https://coastalit.com', '123 Business Ave, Suite 100, San Francisco, CA 94105')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_preferences (
|
||||||
|
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
timezone VARCHAR(100) NOT NULL DEFAULT 'america-los_angeles',
|
||||||
|
date_format VARCHAR(10) NOT NULL DEFAULT 'mdy',
|
||||||
|
items_per_page INTEGER NOT NULL DEFAULT 20,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
@@ -28,6 +28,9 @@
|
|||||||
\echo '=== Running 008_notifications.sql (Notifications + Preferences) ==='
|
\echo '=== Running 008_notifications.sql (Notifications + Preferences) ==='
|
||||||
\i 008_notifications.sql
|
\i 008_notifications.sql
|
||||||
|
|
||||||
|
\echo '=== Running 009_settings.sql (Company Settings + User Preferences) ==='
|
||||||
|
\i 009_settings.sql
|
||||||
|
|
||||||
\echo '=== Migration Complete ==='
|
\echo '=== Migration Complete ==='
|
||||||
\echo ''
|
\echo ''
|
||||||
\echo 'Test accounts created:'
|
\echo 'Test accounts created:'
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
|
import { getSessionUser } from "@/lib/auth"
|
||||||
|
import { query } from "@/lib/db"
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const user = await getSessionUser()
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||||
|
|
||||||
|
const result = await query(`SELECT * FROM company_settings ORDER BY updated_at DESC LIMIT 1`)
|
||||||
|
const row = result.rows[0]
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return NextResponse.json({
|
||||||
|
companyName: "Coastal IT Solutions",
|
||||||
|
companyEmail: "info@coastalit.com",
|
||||||
|
companyPhone: "(555) 123-4567",
|
||||||
|
companyWebsite: "https://coastalit.com",
|
||||||
|
companyAddress: "123 Business Ave, Suite 100, San Francisco, CA 94105",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
companyName: row.company_name || "",
|
||||||
|
companyEmail: row.company_email || "",
|
||||||
|
companyPhone: row.company_phone || "",
|
||||||
|
companyWebsite: row.company_website || "",
|
||||||
|
companyAddress: row.company_address || "",
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Company settings GET error:", error)
|
||||||
|
return NextResponse.json({ error: "Failed to load company settings" }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PATCH(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const user = await getSessionUser()
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
|
||||||
|
await query(
|
||||||
|
`UPDATE company_settings SET
|
||||||
|
company_name = $1, company_email = $2, company_phone = $3,
|
||||||
|
company_website = $4, company_address = $5, updated_by = $6, updated_at = NOW()`,
|
||||||
|
[
|
||||||
|
body.companyName || "",
|
||||||
|
body.companyEmail || "",
|
||||||
|
body.companyPhone || "",
|
||||||
|
body.companyWebsite || "",
|
||||||
|
body.companyAddress || "",
|
||||||
|
user.id,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Company settings PATCH error:", error)
|
||||||
|
return NextResponse.json({ error: "Failed to save company settings" }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
|
import { getSessionUser } from "@/lib/auth"
|
||||||
|
import { query } from "@/lib/db"
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const user = await getSessionUser()
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||||
|
|
||||||
|
const result = await query(
|
||||||
|
`SELECT timezone, date_format, items_per_page FROM user_preferences WHERE user_id = $1`,
|
||||||
|
[user.id],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
return NextResponse.json({
|
||||||
|
timezone: "america-los_angeles",
|
||||||
|
dateFormat: "mdy",
|
||||||
|
itemsPerPage: 20,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = result.rows[0]
|
||||||
|
return NextResponse.json({
|
||||||
|
timezone: r.timezone,
|
||||||
|
dateFormat: r.date_format,
|
||||||
|
itemsPerPage: r.items_per_page,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Preferences GET error:", error)
|
||||||
|
return NextResponse.json({ error: "Failed to load preferences" }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PATCH(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const user = await getSessionUser()
|
||||||
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
|
||||||
|
await query(
|
||||||
|
`INSERT INTO user_preferences (user_id, timezone, date_format, items_per_page, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, NOW())
|
||||||
|
ON CONFLICT (user_id)
|
||||||
|
DO UPDATE SET timezone = $2, date_format = $3, items_per_page = $4, updated_at = NOW()`,
|
||||||
|
[
|
||||||
|
user.id,
|
||||||
|
body.timezone || "america-los_angeles",
|
||||||
|
body.dateFormat || "mdy",
|
||||||
|
body.itemsPerPage || 20,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Preferences PATCH error:", error)
|
||||||
|
return NextResponse.json({ error: "Failed to save preferences" }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
+259
-7
@@ -22,14 +22,14 @@
|
|||||||
--border: 214.3 31.8% 91.4%;
|
--border: 214.3 31.8% 91.4%;
|
||||||
--input: 214.3 31.8% 91.4%;
|
--input: 214.3 31.8% 91.4%;
|
||||||
--ring: 221.2 83.2% 53.3%;
|
--ring: 221.2 83.2% 53.3%;
|
||||||
--sidebar: 222.2 84% 4.9%;
|
--sidebar: 0 0% 100%;
|
||||||
--sidebar-foreground: 210 40% 98%;
|
--sidebar-foreground: 222.2 84% 4.9%;
|
||||||
--sidebar-primary: 217.2 91.2% 59.8%;
|
--sidebar-primary: 221.2 83.2% 53.3%;
|
||||||
--sidebar-primary-foreground: 0 0% 100%;
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
--sidebar-accent: 217.2 32.6% 17.5%;
|
--sidebar-accent: 210 40% 96.1%;
|
||||||
--sidebar-accent-foreground: 210 40% 98%;
|
--sidebar-accent-foreground: 222.2 84% 4.9%;
|
||||||
--sidebar-border: 217.2 32.6% 17.5%;
|
--sidebar-border: 214.3 31.8% 91.4%;
|
||||||
--sidebar-ring: 224.3 76.3% 48%;
|
--sidebar-ring: 221.2 83.2% 53.3%;
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +63,258 @@
|
|||||||
--sidebar-ring: 224.3 76.3% 48%;
|
--sidebar-ring: 224.3 76.3% 48%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ocean {
|
||||||
|
--primary: 187 75% 42%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--ring: 187 75% 42%;
|
||||||
|
--sidebar: 0 0% 100%;
|
||||||
|
--sidebar-foreground: 222.2 84% 4.9%;
|
||||||
|
--sidebar-primary: 187 75% 42%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 187 30% 94%;
|
||||||
|
--sidebar-accent-foreground: 187 50% 20%;
|
||||||
|
--sidebar-border: 187 20% 88%;
|
||||||
|
--sidebar-ring: 187 75% 42%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark.ocean {
|
||||||
|
--primary: 187 75% 50%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--ring: 187 75% 50%;
|
||||||
|
--sidebar: 222.2 84% 4.9%;
|
||||||
|
--sidebar-foreground: 210 40% 98%;
|
||||||
|
--sidebar-primary: 187 75% 55%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 217.2 32.6% 17.5%;
|
||||||
|
--sidebar-accent-foreground: 210 40% 98%;
|
||||||
|
--sidebar-border: 217.2 32.6% 17.5%;
|
||||||
|
--sidebar-ring: 187 75% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forest {
|
||||||
|
--primary: 142 76% 36%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--ring: 142 76% 36%;
|
||||||
|
--sidebar: 0 0% 100%;
|
||||||
|
--sidebar-foreground: 222.2 84% 4.9%;
|
||||||
|
--sidebar-primary: 142 76% 36%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 142 30% 94%;
|
||||||
|
--sidebar-accent-foreground: 142 50% 20%;
|
||||||
|
--sidebar-border: 142 20% 88%;
|
||||||
|
--sidebar-ring: 142 76% 36%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark.forest {
|
||||||
|
--primary: 142 76% 44%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--ring: 142 76% 44%;
|
||||||
|
--sidebar: 222.2 84% 4.9%;
|
||||||
|
--sidebar-foreground: 210 40% 98%;
|
||||||
|
--sidebar-primary: 142 76% 50%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 217.2 32.6% 17.5%;
|
||||||
|
--sidebar-accent-foreground: 210 40% 98%;
|
||||||
|
--sidebar-border: 217.2 32.6% 17.5%;
|
||||||
|
--sidebar-ring: 142 76% 44%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sunset {
|
||||||
|
--primary: 24 95% 53%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--ring: 24 95% 53%;
|
||||||
|
--sidebar: 0 0% 100%;
|
||||||
|
--sidebar-foreground: 222.2 84% 4.9%;
|
||||||
|
--sidebar-primary: 24 95% 53%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 24 30% 94%;
|
||||||
|
--sidebar-accent-foreground: 24 50% 20%;
|
||||||
|
--sidebar-border: 24 20% 88%;
|
||||||
|
--sidebar-ring: 24 95% 53%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark.sunset {
|
||||||
|
--primary: 24 95% 58%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--ring: 24 95% 58%;
|
||||||
|
--sidebar: 222.2 84% 4.9%;
|
||||||
|
--sidebar-foreground: 210 40% 98%;
|
||||||
|
--sidebar-primary: 24 95% 65%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 217.2 32.6% 17.5%;
|
||||||
|
--sidebar-accent-foreground: 210 40% 98%;
|
||||||
|
--sidebar-border: 217.2 32.6% 17.5%;
|
||||||
|
--sidebar-ring: 24 95% 58%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.midnight {
|
||||||
|
--primary: 230 75% 55%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--ring: 230 75% 55%;
|
||||||
|
--sidebar: 0 0% 100%;
|
||||||
|
--sidebar-foreground: 222.2 84% 4.9%;
|
||||||
|
--sidebar-primary: 230 75% 55%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 230 30% 94%;
|
||||||
|
--sidebar-accent-foreground: 230 50% 20%;
|
||||||
|
--sidebar-border: 230 20% 88%;
|
||||||
|
--sidebar-ring: 230 75% 55%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark.midnight {
|
||||||
|
--primary: 230 75% 62%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--ring: 230 75% 62%;
|
||||||
|
--sidebar: 222.2 84% 4.9%;
|
||||||
|
--sidebar-foreground: 210 40% 98%;
|
||||||
|
--sidebar-primary: 230 75% 65%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 217.2 32.6% 17.5%;
|
||||||
|
--sidebar-accent-foreground: 210 40% 98%;
|
||||||
|
--sidebar-border: 217.2 32.6% 17.5%;
|
||||||
|
--sidebar-ring: 230 75% 62%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rose {
|
||||||
|
--primary: 346 77% 50%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--ring: 346 77% 50%;
|
||||||
|
--sidebar: 0 0% 100%;
|
||||||
|
--sidebar-foreground: 222.2 84% 4.9%;
|
||||||
|
--sidebar-primary: 346 77% 50%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 346 30% 94%;
|
||||||
|
--sidebar-accent-foreground: 346 50% 20%;
|
||||||
|
--sidebar-border: 346 20% 88%;
|
||||||
|
--sidebar-ring: 346 77% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark.rose {
|
||||||
|
--primary: 346 77% 58%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--ring: 346 77% 58%;
|
||||||
|
--sidebar: 222.2 84% 4.9%;
|
||||||
|
--sidebar-foreground: 210 40% 98%;
|
||||||
|
--sidebar-primary: 346 77% 60%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 217.2 32.6% 17.5%;
|
||||||
|
--sidebar-accent-foreground: 210 40% 98%;
|
||||||
|
--sidebar-border: 217.2 32.6% 17.5%;
|
||||||
|
--sidebar-ring: 346 77% 58%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amber {
|
||||||
|
--primary: 38 92% 50%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--ring: 38 92% 50%;
|
||||||
|
--sidebar: 0 0% 100%;
|
||||||
|
--sidebar-foreground: 222.2 84% 4.9%;
|
||||||
|
--sidebar-primary: 38 92% 50%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 38 30% 94%;
|
||||||
|
--sidebar-accent-foreground: 38 50% 20%;
|
||||||
|
--sidebar-border: 38 20% 88%;
|
||||||
|
--sidebar-ring: 38 92% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark.amber {
|
||||||
|
--primary: 38 92% 56%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--ring: 38 92% 56%;
|
||||||
|
--sidebar: 222.2 84% 4.9%;
|
||||||
|
--sidebar-foreground: 210 40% 98%;
|
||||||
|
--sidebar-primary: 38 92% 60%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 217.2 32.6% 17.5%;
|
||||||
|
--sidebar-accent-foreground: 210 40% 98%;
|
||||||
|
--sidebar-border: 217.2 32.6% 17.5%;
|
||||||
|
--sidebar-ring: 38 92% 56%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.violet {
|
||||||
|
--primary: 262 83% 58%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--ring: 262 83% 58%;
|
||||||
|
--sidebar: 0 0% 100%;
|
||||||
|
--sidebar-foreground: 222.2 84% 4.9%;
|
||||||
|
--sidebar-primary: 262 83% 58%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 262 30% 94%;
|
||||||
|
--sidebar-accent-foreground: 262 50% 20%;
|
||||||
|
--sidebar-border: 262 20% 88%;
|
||||||
|
--sidebar-ring: 262 83% 58%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark.violet {
|
||||||
|
--primary: 262 83% 65%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--ring: 262 83% 65%;
|
||||||
|
--sidebar: 222.2 84% 4.9%;
|
||||||
|
--sidebar-foreground: 210 40% 98%;
|
||||||
|
--sidebar-primary: 262 83% 68%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 217.2 32.6% 17.5%;
|
||||||
|
--sidebar-accent-foreground: 210 40% 98%;
|
||||||
|
--sidebar-border: 217.2 32.6% 17.5%;
|
||||||
|
--sidebar-ring: 262 83% 65%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slate {
|
||||||
|
--primary: 215 20% 45%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--ring: 215 20% 45%;
|
||||||
|
--sidebar: 0 0% 100%;
|
||||||
|
--sidebar-foreground: 222.2 84% 4.9%;
|
||||||
|
--sidebar-primary: 215 20% 45%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 215 20% 94%;
|
||||||
|
--sidebar-accent-foreground: 215 50% 20%;
|
||||||
|
--sidebar-border: 215 20% 88%;
|
||||||
|
--sidebar-ring: 215 20% 45%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark.slate {
|
||||||
|
--primary: 215 20% 60%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--ring: 215 20% 60%;
|
||||||
|
--sidebar: 222.2 84% 4.9%;
|
||||||
|
--sidebar-foreground: 210 40% 98%;
|
||||||
|
--sidebar-primary: 215 20% 65%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 217.2 32.6% 17.5%;
|
||||||
|
--sidebar-accent-foreground: 210 40% 98%;
|
||||||
|
--sidebar-border: 217.2 32.6% 17.5%;
|
||||||
|
--sidebar-ring: 215 20% 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruby {
|
||||||
|
--primary: 351 85% 45%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--ring: 351 85% 45%;
|
||||||
|
--sidebar: 0 0% 100%;
|
||||||
|
--sidebar-foreground: 222.2 84% 4.9%;
|
||||||
|
--sidebar-primary: 351 85% 45%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 351 30% 94%;
|
||||||
|
--sidebar-accent-foreground: 351 50% 20%;
|
||||||
|
--sidebar-border: 351 20% 88%;
|
||||||
|
--sidebar-ring: 351 85% 45%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark.ruby {
|
||||||
|
--primary: 351 85% 52%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--ring: 351 85% 52%;
|
||||||
|
--sidebar: 222.2 84% 4.9%;
|
||||||
|
--sidebar-foreground: 210 40% 98%;
|
||||||
|
--sidebar-primary: 351 85% 55%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 217.2 32.6% 17.5%;
|
||||||
|
--sidebar-accent-foreground: 210 40% 98%;
|
||||||
|
--sidebar-border: 217.2 32.6% 17.5%;
|
||||||
|
--sidebar-ring: 351 85% 52%;
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
|
|||||||
@@ -1,13 +1,72 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { COMPANY_NAME } from "@/lib/constants"
|
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
|
interface CompanyData {
|
||||||
|
companyName: string
|
||||||
|
companyEmail: string
|
||||||
|
companyPhone: string
|
||||||
|
companyWebsite: string
|
||||||
|
companyAddress: string
|
||||||
|
}
|
||||||
|
|
||||||
export function CompanySettingsForm() {
|
export function CompanySettingsForm() {
|
||||||
|
const [data, setData] = useState<CompanyData>({
|
||||||
|
companyName: "",
|
||||||
|
companyEmail: "",
|
||||||
|
companyPhone: "",
|
||||||
|
companyWebsite: "",
|
||||||
|
companyAddress: "",
|
||||||
|
})
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function load() {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/settings/company")
|
||||||
|
if (res.ok) {
|
||||||
|
const json = await res.json()
|
||||||
|
setData(json)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
load()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
setSaving(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/settings/company", {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
toast.success("Company settings saved")
|
||||||
|
} else {
|
||||||
|
toast.error("Failed to save company settings")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error("Failed to save company settings")
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(field: keyof CompanyData, value: string) {
|
||||||
|
setData((prev) => ({ ...prev, [field]: value }))
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -20,27 +79,29 @@ export function CompanySettingsForm() {
|
|||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="company-name">Company Name</Label>
|
<Label htmlFor="company-name">Company Name</Label>
|
||||||
<Input id="company-name" defaultValue={COMPANY_NAME} />
|
<Input id="company-name" disabled={loading} value={data.companyName} onChange={(e) => update("companyName", e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="company-email">Company Email</Label>
|
<Label htmlFor="company-email">Company Email</Label>
|
||||||
<Input id="company-email" type="email" defaultValue="info@coastalit.com" />
|
<Input id="company-email" type="email" disabled={loading} value={data.companyEmail} onChange={(e) => update("companyEmail", e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="company-phone">Phone</Label>
|
<Label htmlFor="company-phone">Phone</Label>
|
||||||
<Input id="company-phone" defaultValue="(555) 123-4567" />
|
<Input id="company-phone" disabled={loading} value={data.companyPhone} onChange={(e) => update("companyPhone", e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="company-website">Website</Label>
|
<Label htmlFor="company-website">Website</Label>
|
||||||
<Input id="company-website" defaultValue="https://coastalit.com" />
|
<Input id="company-website" disabled={loading} value={data.companyWebsite} onChange={(e) => update("companyWebsite", e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 sm:col-span-2">
|
<div className="space-y-2 sm:col-span-2">
|
||||||
<Label htmlFor="company-address">Address</Label>
|
<Label htmlFor="company-address">Address</Label>
|
||||||
<Input id="company-address" defaultValue="123 Business Ave, Suite 100, San Francisco, CA 94105" />
|
<Input id="company-address" disabled={loading} value={data.companyAddress} onChange={(e) => update("companyAddress", e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end pt-4">
|
<div className="flex justify-end pt-4">
|
||||||
<Button onClick={() => toast.success("Company settings saved")}>Save Changes</Button>
|
<Button onClick={handleSave} disabled={loading || saving}>
|
||||||
|
{saving ? "Saving..." : "Save Changes"}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
@@ -7,30 +8,76 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Sun, Moon, Monitor } from "lucide-react"
|
import { Sun, Moon, Monitor } from "lucide-react"
|
||||||
|
|
||||||
export function ThemeSettings() {
|
const COLOR_THEME_KEY = "crm-color-theme"
|
||||||
const { theme, setTheme } = useTheme()
|
|
||||||
|
|
||||||
const options = [
|
const modeOptions = [
|
||||||
{ value: "light", icon: Sun, label: "Light" },
|
{ value: "light", icon: Sun, label: "Light" },
|
||||||
{ value: "dark", icon: Moon, label: "Dark" },
|
{ value: "dark", icon: Moon, label: "Dark" },
|
||||||
{ value: "system", icon: Monitor, label: "System" },
|
{ value: "system", icon: Monitor, label: "System" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const themeOptions = [
|
||||||
|
{ value: "default", label: "Default", color: "bg-blue-600", ring: "ring-blue-600" },
|
||||||
|
{ value: "ocean", label: "Ocean", color: "bg-teal-500", ring: "ring-teal-500" },
|
||||||
|
{ value: "forest", label: "Forest", color: "bg-green-600", ring: "ring-green-600" },
|
||||||
|
{ value: "sunset", label: "Sunset", color: "bg-orange-500", ring: "ring-orange-500" },
|
||||||
|
{ value: "midnight", label: "Midnight", color: "bg-indigo-600", ring: "ring-indigo-600" },
|
||||||
|
{ value: "rose", label: "Rose", color: "bg-pink-600", ring: "ring-pink-600" },
|
||||||
|
{ value: "amber", label: "Amber", color: "bg-amber-500", ring: "ring-amber-500" },
|
||||||
|
{ value: "violet", label: "Violet", color: "bg-violet-600", ring: "ring-violet-600" },
|
||||||
|
{ value: "slate", label: "Slate", color: "bg-slate-500", ring: "ring-slate-500" },
|
||||||
|
{ value: "ruby", label: "Ruby", color: "bg-red-600", ring: "ring-red-600" },
|
||||||
|
]
|
||||||
|
|
||||||
|
function getStoredColorTheme(): string {
|
||||||
|
if (typeof window === "undefined") return "default"
|
||||||
|
return localStorage.getItem(COLOR_THEME_KEY) || "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyColorTheme(theme: string) {
|
||||||
|
document.documentElement.className = document.documentElement.className
|
||||||
|
.replace(/\b(default|ocean|forest|sunset|midnight|rose|amber|violet|slate|ruby)\b/g, "")
|
||||||
|
.trim()
|
||||||
|
if (theme !== "default") {
|
||||||
|
document.documentElement.classList.add(theme)
|
||||||
|
}
|
||||||
|
localStorage.setItem(COLOR_THEME_KEY, theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemeSettings() {
|
||||||
|
const { theme, setTheme } = useTheme()
|
||||||
|
const [colorTheme, setColorTheme] = useState("default")
|
||||||
|
const [mounted, setMounted] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
setColorTheme(getStoredColorTheme())
|
||||||
|
applyColorTheme(getStoredColorTheme())
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
function handleColorChange(value: string) {
|
||||||
|
setColorTheme(value)
|
||||||
|
applyColorTheme(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Theme Settings</CardTitle>
|
<CardTitle>Theme Mode</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Choose your preferred appearance for the dashboard.
|
Choose your preferred appearance mode.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<RadioGroup value={theme} onValueChange={setTheme} className="grid grid-cols-3 gap-4">
|
<RadioGroup value={theme || "dark"} onValueChange={setTheme} className="grid grid-cols-3 gap-4">
|
||||||
{options.map(({ value, icon: Icon, label }) => (
|
{modeOptions.map(({ value, icon: Icon, label }) => (
|
||||||
<div key={value}>
|
<div key={value}>
|
||||||
<RadioGroupItem value={value} id={value} className="peer sr-only" />
|
<RadioGroupItem value={value} id={`mode-${value}`} className="peer sr-only" />
|
||||||
<Label
|
<Label
|
||||||
htmlFor={value}
|
htmlFor={`mode-${value}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col items-center gap-3 rounded-lg border-2 p-4 hover:bg-accent cursor-pointer transition-all",
|
"flex flex-col items-center gap-3 rounded-lg border-2 p-4 hover:bg-accent cursor-pointer transition-all",
|
||||||
"peer-data-[state=checked]:border-primary peer-data-[state=checked]:bg-primary/5"
|
"peer-data-[state=checked]:border-primary peer-data-[state=checked]:bg-primary/5"
|
||||||
@@ -44,5 +91,34 @@ export function ThemeSettings() {
|
|||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Color Theme</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Select a color accent for the dashboard.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<RadioGroup value={colorTheme} onValueChange={handleColorChange} className="grid grid-cols-5 gap-4">
|
||||||
|
{themeOptions.map(({ value, label, color, ring }) => (
|
||||||
|
<div key={value}>
|
||||||
|
<RadioGroupItem value={value} id={`color-${value}`} className="peer sr-only" />
|
||||||
|
<Label
|
||||||
|
htmlFor={`color-${value}`}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col items-center gap-3 rounded-lg border-2 p-4 hover:bg-accent cursor-pointer transition-all",
|
||||||
|
"peer-data-[state=checked]:border-primary peer-data-[state=checked]:bg-primary/5"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={cn("h-8 w-8 rounded-full", color, ring, "ring-2 ring-offset-2 ring-offset-background")} />
|
||||||
|
<span className="text-sm font-medium">{label}</span>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@@ -12,7 +13,58 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select"
|
} from "@/components/ui/select"
|
||||||
|
|
||||||
|
interface Preferences {
|
||||||
|
timezone: string
|
||||||
|
dateFormat: string
|
||||||
|
itemsPerPage: number
|
||||||
|
}
|
||||||
|
|
||||||
export function UserPreferencesForm() {
|
export function UserPreferencesForm() {
|
||||||
|
const [prefs, setPrefs] = useState<Preferences>({
|
||||||
|
timezone: "america-los_angeles",
|
||||||
|
dateFormat: "mdy",
|
||||||
|
itemsPerPage: 20,
|
||||||
|
})
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function load() {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/settings/preferences")
|
||||||
|
if (res.ok) {
|
||||||
|
const json = await res.json()
|
||||||
|
setPrefs(json)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
load()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
setSaving(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/settings/preferences", {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(prefs),
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
toast.success("Preferences saved")
|
||||||
|
} else {
|
||||||
|
toast.error("Failed to save preferences")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error("Failed to save preferences")
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -25,7 +77,7 @@ export function UserPreferencesForm() {
|
|||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="timezone">Timezone</Label>
|
<Label htmlFor="timezone">Timezone</Label>
|
||||||
<Select defaultValue="america-los_angeles">
|
<Select disabled={loading} value={prefs.timezone} onValueChange={(v) => setPrefs((p) => ({ ...p, timezone: v }))}>
|
||||||
<SelectTrigger id="timezone">
|
<SelectTrigger id="timezone">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -39,7 +91,7 @@ export function UserPreferencesForm() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="date-format">Date Format</Label>
|
<Label htmlFor="date-format">Date Format</Label>
|
||||||
<Select defaultValue="mdy">
|
<Select disabled={loading} value={prefs.dateFormat} onValueChange={(v) => setPrefs((p) => ({ ...p, dateFormat: v }))}>
|
||||||
<SelectTrigger id="date-format">
|
<SelectTrigger id="date-format">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -52,7 +104,7 @@ export function UserPreferencesForm() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="items-per-page">Items Per Page</Label>
|
<Label htmlFor="items-per-page">Items Per Page</Label>
|
||||||
<Select defaultValue="20">
|
<Select disabled={loading} value={String(prefs.itemsPerPage)} onValueChange={(v) => setPrefs((p) => ({ ...p, itemsPerPage: parseInt(v) }))}>
|
||||||
<SelectTrigger id="items-per-page">
|
<SelectTrigger id="items-per-page">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -66,7 +118,9 @@ export function UserPreferencesForm() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end pt-4">
|
<div className="flex justify-end pt-4">
|
||||||
<Button onClick={() => toast.success("Preferences saved")}>Save Preferences</Button>
|
<Button onClick={handleSave} disabled={loading || saving}>
|
||||||
|
{saving ? "Saving..." : "Save Preferences"}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -4,5 +4,12 @@ import { ThemeProvider as NextThemesProvider } from "next-themes"
|
|||||||
import { type ThemeProviderProps } from "next-themes"
|
import { type ThemeProviderProps } from "next-themes"
|
||||||
|
|
||||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
return (
|
||||||
|
<NextThemesProvider
|
||||||
|
themes={["light", "dark", "system", "ocean", "forest", "sunset", "midnight"]}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</NextThemesProvider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user