diff --git a/app/themes/layout.tsx b/app/themes/layout.tsx
new file mode 100644
index 0000000..1e81d5b
--- /dev/null
+++ b/app/themes/layout.tsx
@@ -0,0 +1,11 @@
+import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar';
+import { AppSidebar } from '@/components/app-sidebar';
+
+export default function PickerLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+ );
+}
diff --git a/app/themes/page.tsx b/app/themes/page.tsx
new file mode 100644
index 0000000..6aa8486
--- /dev/null
+++ b/app/themes/page.tsx
@@ -0,0 +1,20 @@
+import { getUserThemesList, ThemeData } from '@/lib/services/theme-service';
+import ThemeCreator from '@/components/picker/theme-creator';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { Suspense } from 'react';
+import ThemeManagerClient from '@/components/themes/theme-manager-client';
+
+export const metadata = {
+ title: 'My Themes',
+ description: 'Create, customize, and manage your themes',
+};
+
+export default async function ThemesPage() {
+ const themes = await getUserThemesList();
+
+ return (
+ Loading your themes...}>
+
+
+ );
+}
diff --git a/components/page-header.tsx b/components/page-header.tsx
new file mode 100644
index 0000000..950056f
--- /dev/null
+++ b/components/page-header.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+interface PageHeaderProps {
+ heading: string;
+ text?: string;
+ children?: React.ReactNode;
+}
+
+export function PageHeader({ heading, text, children }: PageHeaderProps) {
+ return (
+
+
{heading}
+ {text &&
{text}
}
+ {children}
+
+ );
+}
diff --git a/components/picker/theme-creator.tsx b/components/picker/theme-creator.tsx
index b37f96a..1b732d8 100644
--- a/components/picker/theme-creator.tsx
+++ b/components/picker/theme-creator.tsx
@@ -1,7 +1,7 @@
'use client';
import { useState, useEffect, useRef, useCallback } from 'react';
-import { Copy, Check } from 'lucide-react';
+import { Copy, Check, Save } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import { Toaster } from '@/components/ui/sonner';
@@ -30,6 +30,17 @@ import { ModeToggle } from '../mode-toggle';
import { SidebarTrigger } from '../ui/sidebar';
import { ScrollArea } from '../ui/scroll-area';
import { FontOption, applyFontToDocument, fontOptions } from '@/lib/picker/font-utils';
+import { saveTheme, getUserThemesList, ThemeData } from '@/lib/services/theme-service';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+} from '@/components/ui/dialog';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
// Type for theme color key
type ThemeColorKey = keyof typeof defaultTheme.light;
@@ -53,6 +64,32 @@ export default function ThemeCreator() {
const [forceEditorUpdate, setForceEditorUpdate] = useState(0);
const [importModalOpen, setImportModalOpen] = useState(false);
+ // Add state for saving to database
+ const [saveDialogOpen, setSaveDialogOpen] = useState(false);
+ const [themeName, setThemeName] = useState('My Custom Theme');
+ const [isDefault, setIsDefault] = useState(false);
+ const [isSaving, setIsSaving] = useState(false);
+ const [isThemeSaved, setIsThemeSaved] = useState(false);
+
+ // Check if theme is already saved
+ const checkIfThemeIsSaved = useCallback(async () => {
+ try {
+ const userThemes = await getUserThemesList();
+ // Compare current theme with saved themes
+ // This is a simple check - you might want a more sophisticated comparison
+ const themeFound = userThemes.some(
+ savedTheme =>
+ JSON.stringify(savedTheme.light) === JSON.stringify(themeColorsRef.current.light) &&
+ JSON.stringify(savedTheme.dark) === JSON.stringify(themeColorsRef.current.dark)
+ );
+ setIsThemeSaved(themeFound);
+ return themeFound;
+ } catch (error) {
+ console.error('Error checking if theme is saved:', error);
+ return false;
+ }
+ }, []);
+
// Handle hydration mismatch
useEffect(() => {
setMounted(true);
@@ -75,7 +112,21 @@ export default function ThemeCreator() {
if (defaultFont) {
applyFontToDocument(defaultFont);
}
- }, [mounted, currentTheme, currentFont]);
+
+ // Check if theme is already saved
+ checkIfThemeIsSaved();
+ }, [mounted, currentTheme, currentFont, checkIfThemeIsSaved]);
+
+ // Check if theme is saved when it changes significantly
+ useEffect(() => {
+ if (!mounted) return;
+ // Debounce the check to avoid too many API calls
+ const timer = setTimeout(() => {
+ checkIfThemeIsSaved();
+ }, 1000);
+
+ return () => clearTimeout(timer);
+ }, [forceEditorUpdate, checkIfThemeIsSaved, mounted]);
// Update all hues with new value without re-rendering
const handleHueChange = useCallback(
@@ -224,6 +275,41 @@ export default function ThemeCreator() {
// Font will be applied by the FontSelector component through the FontProvider
}, []);
+ // Handle save to database
+ const handleSaveToDatabase = useCallback(async () => {
+ try {
+ // First check if theme is already saved
+ const alreadySaved = await checkIfThemeIsSaved();
+ if (alreadySaved) {
+ toast('This theme is already saved in your account');
+ return;
+ }
+
+ setIsSaving(true);
+
+ const themeData: ThemeData = {
+ name: themeName,
+ isDefault: isDefault,
+ light: themeColorsRef.current.light,
+ dark: themeColorsRef.current.dark,
+ };
+
+ await saveTheme(themeData);
+
+ // Close the dialog
+ setSaveDialogOpen(false);
+ setIsThemeSaved(true);
+
+ // Show success message
+ toast('Theme saved successfully');
+ } catch (error) {
+ console.error('Error saving theme:', error);
+ toast.error('Failed to save theme');
+ } finally {
+ setIsSaving(false);
+ }
+ }, [themeName, isDefault, checkIfThemeIsSaved]);
+
// Handle selecting a default theme
const handleSelectDefaultTheme = useCallback(
(themeName: string, theme: any) => {
@@ -296,6 +382,15 @@ export default function ThemeCreator() {
+
@@ -329,6 +424,48 @@ export default function ThemeCreator() {
onOpenChange={setImportModalOpen}
onImport={handleImportTheme}
/>
+
+
+
);
diff --git a/components/picker/theme-manager.tsx b/components/picker/theme-manager.tsx
new file mode 100644
index 0000000..f2d6383
--- /dev/null
+++ b/components/picker/theme-manager.tsx
@@ -0,0 +1,385 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Button } from '@/components/ui/button';
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Separator } from '@/components/ui/separator';
+import { ScrollArea } from '@/components/ui/scroll-area';
+import { toast } from 'sonner';
+import { Star, Trash2, Edit, Check, X, Plus } from 'lucide-react';
+import { saveTheme, updateTheme, removeTheme, ThemeData } from '@/lib/services/theme-service';
+import {
+ ThemeMode,
+ ThemeColors,
+ getActiveThemeMode,
+ applyThemeToDOM,
+ toCamelCase,
+} from '@/lib/picker/theme-utils';
+import { useTheme } from 'next-themes';
+
+interface ThemeManagerProps {
+ themes: ThemeData[];
+ currentTheme: Record;
+ onSelectTheme: (theme: ThemeData) => void;
+}
+
+export default function ThemeManager({ themes, currentTheme, onSelectTheme }: ThemeManagerProps) {
+ const [selectedTheme, setSelectedTheme] = useState(null);
+ const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
+ const [isSaveDialogOpen, setIsSaveDialogOpen] = useState(false);
+ const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
+ const [themeName, setThemeName] = useState('');
+ const [isDefault, setIsDefault] = useState(false);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [localThemes, setLocalThemes] = useState(themes);
+ const { theme: nextTheme } = useTheme();
+
+ // Update local themes when prop changes
+ useEffect(() => {
+ setLocalThemes(themes);
+ }, [themes]);
+
+ const handleSaveTheme = async () => {
+ if (!themeName.trim()) {
+ toast.error('Theme name required', {
+ description: 'Please enter a name for your theme',
+ });
+ return;
+ }
+
+ setIsSubmitting(true);
+ try {
+ const activeMode = getActiveThemeMode(nextTheme);
+
+ const themeToSave: ThemeData = {
+ name: themeName,
+ isDefault,
+ light: currentTheme.light as unknown as Record,
+ dark: currentTheme.dark as unknown as Record,
+ };
+
+ const savedTheme = await saveTheme(themeToSave);
+
+ if (savedTheme) {
+ setLocalThemes(prev => [...prev, savedTheme]);
+ toast.success('Theme saved', {
+ description: `"${themeName}" has been saved to your themes`,
+ });
+ setIsSaveDialogOpen(false);
+ setThemeName('');
+ setIsDefault(false);
+ } else {
+ toast.error('Error saving theme', {
+ description: 'There was a problem saving your theme. Please try again.',
+ });
+ }
+ } catch (error) {
+ toast.error('Error saving theme', {
+ description: 'There was a problem saving your theme. Please try again.',
+ });
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleUpdateTheme = async () => {
+ if (!selectedTheme || !selectedTheme.id) return;
+ if (!themeName.trim()) {
+ toast.error('Theme name required', {
+ description: 'Please enter a name for your theme',
+ });
+ return;
+ }
+
+ setIsSubmitting(true);
+ try {
+ const updates: Partial = {
+ name: themeName,
+ isDefault,
+ };
+
+ const updatedTheme = await updateTheme(selectedTheme.id, updates);
+
+ if (updatedTheme) {
+ setLocalThemes(prev => prev.map(t => (t.id === updatedTheme.id ? updatedTheme : t)));
+ toast.success('Theme updated', {
+ description: `"${themeName}" has been updated`,
+ });
+ setIsEditDialogOpen(false);
+ setSelectedTheme(null);
+ setThemeName('');
+ setIsDefault(false);
+ } else {
+ toast.error('Error updating theme', {
+ description: 'There was a problem updating your theme. Please try again.',
+ });
+ }
+ } catch (error) {
+ toast.error('Error updating theme', {
+ description: 'There was a problem updating your theme. Please try again.',
+ });
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleDeleteTheme = async () => {
+ if (!selectedTheme || !selectedTheme.id) return;
+
+ setIsSubmitting(true);
+ try {
+ const success = await removeTheme(selectedTheme.id);
+
+ if (success) {
+ setLocalThemes(prev => prev.filter(t => t.id !== selectedTheme.id));
+ toast.success('Theme deleted', {
+ description: `"${selectedTheme.name}" has been deleted`,
+ });
+ setIsDeleteDialogOpen(false);
+ setSelectedTheme(null);
+ } else {
+ toast.error('Error deleting theme', {
+ description: 'There was a problem deleting your theme. Please try again.',
+ });
+ }
+ } catch (error) {
+ toast.error('Error deleting theme', {
+ description: 'There was a problem deleting your theme. Please try again.',
+ });
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const openEditDialog = (theme: ThemeData) => {
+ setSelectedTheme(theme);
+ setThemeName(theme.name);
+ setIsDefault(theme.isDefault || false);
+ setIsEditDialogOpen(true);
+ };
+
+ const openDeleteDialog = (theme: ThemeData) => {
+ setSelectedTheme(theme);
+ setIsDeleteDialogOpen(true);
+ };
+
+ const openSaveDialog = () => {
+ setThemeName('My Custom Theme');
+ setIsDefault(false);
+ setIsSaveDialogOpen(true);
+ };
+
+ return (
+
+
+
My Themes
+
+
+
+
+
+ {localThemes.length === 0 ? (
+
+
+ No saved themes yet. Create and save your first theme!
+
+
+ ) : (
+
+ {localThemes.map(theme => (
+
+
+
+
{theme.name}
+ {theme.isDefault && (
+
+
+
+ )}
+
+
+ {theme.isDefault ? 'Default theme' : 'Custom theme'}
+
+
+
+
+
+ {/* Primary color swatch */}
+
+
+ {/* Secondary color swatch */}
+
+
+ {/* Background color swatch */}
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ {/* Save Theme Dialog */}
+
+
+ {/* Edit Theme Dialog */}
+
+
+ {/* Delete Theme Dialog */}
+
+
+ );
+}
diff --git a/components/themes/theme-manager-client.tsx b/components/themes/theme-manager-client.tsx
new file mode 100644
index 0000000..1c60330
--- /dev/null
+++ b/components/themes/theme-manager-client.tsx
@@ -0,0 +1,64 @@
+'use client';
+
+import { useState, useRef, useEffect } from 'react';
+import ThemeManager from '@/components/picker/theme-manager';
+import { ThemeData } from '@/lib/services/theme-service';
+import { useTheme } from 'next-themes';
+import { ThemeMode, getActiveThemeMode, applyThemeToDOM } from '@/lib/picker/theme-utils';
+import { defaultTheme } from '@/components/picker/defaults/defaultTheme';
+
+interface ThemeManagerClientProps {
+ initialThemes: ThemeData[];
+}
+
+export default function ThemeManagerClient({ initialThemes }: ThemeManagerClientProps) {
+ const [themes, setThemes] = useState(initialThemes);
+ const { theme: currentTheme, setTheme } = useTheme();
+
+ // Use a ref to store the current theme without triggering re-renders
+ const themeColorsRef = useRef>({
+ light: { ...defaultTheme.light },
+ dark: { ...defaultTheme.dark },
+ });
+
+ // Handle applying a saved theme
+ const handleSelectTheme = (theme: ThemeData) => {
+ // Update the theme colors reference
+ themeColorsRef.current = {
+ light: { ...theme.light },
+ dark: { ...theme.dark },
+ };
+
+ // Get the current active mode
+ const activeMode = getActiveThemeMode(currentTheme);
+
+ // Apply the active theme to DOM
+ applyThemeToDOM(themeColorsRef.current, activeMode, document.documentElement, false);
+
+ // For the inactive theme, we'll use data attributes
+ const inactiveMode: ThemeMode = activeMode === 'light' ? 'dark' : 'light';
+
+ // Store the inactive theme using data attributes
+ const inactiveTheme = theme[inactiveMode];
+ Object.entries(inactiveTheme).forEach(([key, value]) => {
+ // Handle data-attributes appropriately based on the key
+ const dataKey = key.replace(/-([a-z])/g, (_, p1) => p1.toUpperCase());
+ document.documentElement.style.setProperty(`--${key}`, `hsl(${value})`);
+ });
+ };
+
+ return (
+
+ );
+}
diff --git a/lib/db/migrations/0001_add_themes_table.sql b/lib/db/migrations/0001_add_themes_table.sql
new file mode 100644
index 0000000..443bd62
--- /dev/null
+++ b/lib/db/migrations/0001_add_themes_table.sql
@@ -0,0 +1,10 @@
+CREATE TABLE IF NOT EXISTS themes (
+ id SERIAL PRIMARY KEY,
+ user_id INTEGER NOT NULL REFERENCES users(id),
+ name VARCHAR(100) NOT NULL,
+ is_default BOOLEAN NOT NULL DEFAULT FALSE,
+ light_theme JSONB NOT NULL,
+ dark_theme JSONB NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMP NOT NULL DEFAULT NOW()
+);
\ No newline at end of file
diff --git a/lib/db/queries.ts b/lib/db/queries.ts
index 30431a3..f9d9f1e 100644
--- a/lib/db/queries.ts
+++ b/lib/db/queries.ts
@@ -1,6 +1,6 @@
import { desc, and, eq, isNull } from 'drizzle-orm';
import { db } from './drizzle';
-import { activityLogs, teamMembers, teams, users } from './schema';
+import { activityLogs, teamMembers, teams, themes, users } from './schema';
import { cookies } from 'next/headers';
import { verifyToken } from '@/lib/auth/session';
@@ -123,3 +123,96 @@ export async function getTeamForUser(userId: number) {
return result?.teamMembers[0]?.team || null;
}
+
+// Theme related queries
+export async function getUserThemes(userId: number) {
+ return await db
+ .select()
+ .from(themes)
+ .where(eq(themes.userId, userId))
+ .orderBy(desc(themes.updatedAt));
+}
+
+export async function getUserDefaultTheme(userId: number) {
+ const result = await db
+ .select()
+ .from(themes)
+ .where(and(eq(themes.userId, userId), eq(themes.isDefault, true)))
+ .limit(1);
+
+ return result.length > 0 ? result[0] : null;
+}
+
+export async function saveUserTheme(
+ userId: number,
+ themeName: string,
+ lightTheme: any,
+ darkTheme: any,
+ isDefault: boolean = false
+) {
+ // If this theme is set as default, update any existing default themes
+ if (isDefault) {
+ await db
+ .update(themes)
+ .set({ isDefault: false })
+ .where(and(eq(themes.userId, userId), eq(themes.isDefault, true)));
+ }
+
+ return await db
+ .insert(themes)
+ .values({
+ userId,
+ name: themeName,
+ lightTheme,
+ darkTheme,
+ isDefault,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ })
+ .returning();
+}
+
+export async function updateUserTheme(
+ themeId: number,
+ userId: number,
+ updates: {
+ name?: string;
+ lightTheme?: any;
+ darkTheme?: any;
+ isDefault?: boolean;
+ }
+) {
+ // If this theme is set as default, update any existing default themes
+ if (updates.isDefault) {
+ await db
+ .update(themes)
+ .set({ isDefault: false })
+ .where(and(eq(themes.userId, userId), eq(themes.isDefault, true)));
+ }
+
+ return await db
+ .update(themes)
+ .set({
+ ...updates,
+ updatedAt: new Date(),
+ })
+ .where(and(eq(themes.id, themeId), eq(themes.userId, userId)))
+ .returning();
+}
+
+export async function deleteUserTheme(themeId: number, userId: number) {
+ const themeToDelete = await db
+ .select()
+ .from(themes)
+ .where(and(eq(themes.id, themeId), eq(themes.userId, userId)))
+ .limit(1);
+
+ if (themeToDelete.length === 0) {
+ return null;
+ }
+
+ // Delete the theme
+ await db.delete(themes).where(and(eq(themes.id, themeId), eq(themes.userId, userId)));
+
+ return themeToDelete[0];
+}
diff --git a/lib/db/schema.ts b/lib/db/schema.ts
index f2915b6..ddb8664 100644
--- a/lib/db/schema.ts
+++ b/lib/db/schema.ts
@@ -1,4 +1,13 @@
-import { pgTable, serial, varchar, text, timestamp, integer } from 'drizzle-orm/pg-core';
+import {
+ pgTable,
+ serial,
+ varchar,
+ text,
+ timestamp,
+ integer,
+ json,
+ boolean,
+} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
export const users = pgTable('users', {
@@ -61,6 +70,19 @@ export const invitations = pgTable('invitations', {
status: varchar('status', { length: 20 }).notNull().default('pending'),
});
+export const themes = pgTable('themes', {
+ id: serial('id').primaryKey(),
+ userId: integer('user_id')
+ .notNull()
+ .references(() => users.id),
+ name: varchar('name', { length: 100 }).notNull(),
+ isDefault: boolean('is_default').notNull().default(false),
+ lightTheme: json('light_theme').notNull(),
+ darkTheme: json('dark_theme').notNull(),
+ createdAt: timestamp('created_at').notNull().defaultNow(),
+ updatedAt: timestamp('updated_at').notNull().defaultNow(),
+});
+
export const teamsRelations = relations(teams, ({ many }) => ({
teamMembers: many(teamMembers),
activityLogs: many(activityLogs),
@@ -70,6 +92,7 @@ export const teamsRelations = relations(teams, ({ many }) => ({
export const usersRelations = relations(users, ({ many }) => ({
teamMembers: many(teamMembers),
invitationsSent: many(invitations),
+ themes: many(themes),
}));
export const invitationsRelations = relations(invitations, ({ one }) => ({
@@ -105,6 +128,13 @@ export const activityLogsRelations = relations(activityLogs, ({ one }) => ({
}),
}));
+export const themesRelations = relations(themes, ({ one }) => ({
+ user: one(users, {
+ fields: [themes.userId],
+ references: [users.id],
+ }),
+}));
+
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Team = typeof teams.$inferSelect;
@@ -115,6 +145,8 @@ export type ActivityLog = typeof activityLogs.$inferSelect;
export type NewActivityLog = typeof activityLogs.$inferInsert;
export type Invitation = typeof invitations.$inferSelect;
export type NewInvitation = typeof invitations.$inferInsert;
+export type Theme = typeof themes.$inferSelect;
+export type NewTheme = typeof themes.$inferInsert;
export type TeamDataWithMembers = Team & {
teamMembers: (TeamMember & {
user: Pick;
diff --git a/lib/services/theme-service.ts b/lib/services/theme-service.ts
new file mode 100644
index 0000000..93aa7d1
--- /dev/null
+++ b/lib/services/theme-service.ts
@@ -0,0 +1,163 @@
+'use server';
+
+import { revalidatePath } from 'next/cache';
+import {
+ getUserThemes,
+ getUserDefaultTheme,
+ saveUserTheme,
+ updateUserTheme,
+ deleteUserTheme,
+} from '@/lib/db/queries';
+import { getUser } from '@/lib/db/queries';
+import { NewTheme, Theme } from '@/lib/db/schema';
+import { ThemeMode } from '@/lib/picker/theme-utils';
+
+export type ThemeData = {
+ id?: number;
+ name: string;
+ isDefault?: boolean;
+ light: Record;
+ dark: Record;
+};
+
+/**
+ * Get all themes for the current user
+ */
+export async function getUserThemesList(): Promise {
+ const user = await getUser();
+ if (!user) {
+ return [];
+ }
+
+ const themes = await getUserThemes(user.id);
+
+ return themes.map(theme => ({
+ id: theme.id,
+ name: theme.name,
+ isDefault: theme.isDefault,
+ light: theme.lightTheme as Record,
+ dark: theme.darkTheme as Record,
+ }));
+}
+
+/**
+ * Get the default theme for the current user
+ */
+export async function getCurrentUserDefaultTheme(): Promise {
+ const user = await getUser();
+ if (!user) {
+ return null;
+ }
+
+ const theme = await getUserDefaultTheme(user.id);
+ if (!theme) {
+ return null;
+ }
+
+ return {
+ id: theme.id,
+ name: theme.name,
+ isDefault: theme.isDefault,
+ light: theme.lightTheme as Record,
+ dark: theme.darkTheme as Record,
+ };
+}
+
+/**
+ * Save a theme for the current user
+ */
+export async function saveTheme(themeData: ThemeData): Promise {
+ const user = await getUser();
+ if (!user) {
+ return null;
+ }
+
+ const result = await saveUserTheme(
+ user.id,
+ themeData.name,
+ themeData.light,
+ themeData.dark,
+ themeData.isDefault || false
+ );
+
+ if (!result || result.length === 0) {
+ return null;
+ }
+
+ const savedTheme = result[0];
+
+ revalidatePath('/settings/themes');
+
+ return {
+ id: savedTheme.id,
+ name: savedTheme.name,
+ isDefault: savedTheme.isDefault,
+ light: savedTheme.lightTheme as Record,
+ dark: savedTheme.darkTheme as Record,
+ };
+}
+
+/**
+ * Update an existing theme
+ */
+export async function updateTheme(
+ themeId: number,
+ updates: Partial
+): Promise {
+ const user = await getUser();
+ if (!user) {
+ return null;
+ }
+
+ const updateData: any = {};
+
+ if (updates.name) {
+ updateData.name = updates.name;
+ }
+
+ if (updates.isDefault !== undefined) {
+ updateData.isDefault = updates.isDefault;
+ }
+
+ if (updates.light) {
+ updateData.lightTheme = updates.light;
+ }
+
+ if (updates.dark) {
+ updateData.darkTheme = updates.dark;
+ }
+
+ const result = await updateUserTheme(themeId, user.id, updateData);
+
+ if (!result || result.length === 0) {
+ return null;
+ }
+
+ const updatedTheme = result[0];
+
+ revalidatePath('/settings/themes');
+
+ return {
+ id: updatedTheme.id,
+ name: updatedTheme.name,
+ isDefault: updatedTheme.isDefault,
+ light: updatedTheme.lightTheme as Record,
+ dark: updatedTheme.darkTheme as Record,
+ };
+}
+
+/**
+ * Delete a theme
+ */
+export async function removeTheme(themeId: number): Promise {
+ const user = await getUser();
+ if (!user) {
+ return false;
+ }
+
+ const result = await deleteUserTheme(themeId, user.id);
+
+ revalidatePath('/settings/themes');
+
+ return !!result;
+}