diff --git a/src/components/SavedThemes.tsx b/src/components/SavedThemes.tsx index 6662feb..323aa14 100644 --- a/src/components/SavedThemes.tsx +++ b/src/components/SavedThemes.tsx @@ -4,14 +4,13 @@ import { useState } from 'react' import ThemeCard from '@/components/ThemeCard' import { SavedTheme } from '@/lib/types/colors' import ThemePreviewSmall from './ThemePreviewSmall' -import { useThemes } from '@/hooks/useThemes' export function SavedThemesContent({ initialThemes, }: { initialThemes: SavedTheme[] }) { - const { themes } = useThemes(initialThemes) + const [themes, setThemes] = useState(initialThemes) const [selectedTheme, setSelectedTheme] = useState(themes[0]) const handlePreview = (theme: SavedTheme) => { diff --git a/src/components/ThemeCard.tsx b/src/components/ThemeCard.tsx index 3ecd5ba..2055790 100644 --- a/src/components/ThemeCard.tsx +++ b/src/components/ThemeCard.tsx @@ -1,20 +1,17 @@ // src/components/ThemeCard.tsx 'use client' -import React from 'react' +import React, { useState } from 'react' import { SavedTheme } from '@/lib/types/colors' import { Button } from '@/components/ui/button' import { Switch } from '@/components/ui/switch' +import { Edit, Download, Eye, Share2, Trash2, Loader2 } from 'lucide-react' import { - MoreVertical, - Edit, - Download, - Eye, - Share2, - Trash2, - Loader2, -} from 'lucide-react' -import { useThemes } from '@/hooks/useThemes' + updateThemePublicity, + deleteTheme, + downloadThemeVSIX, +} from '@/lib/db/themes' +import { useTransition } from 'react' interface ThemeCardProps { theme: SavedTheme @@ -22,40 +19,43 @@ interface ThemeCardProps { } const ThemeCard: React.FC = ({ theme, onPreview }) => { - const { - removeTheme, - updateThemePublicityOptimistic, - downloadTheme, - isThemePending, - isThemeDownloading, - } = useThemes([theme]) + const [isPublic, setIsPublic] = useState(theme.public) + const [isPending, startTransition] = useTransition() - const handleDelete = async () => { + const handleDelete = () => { if (window.confirm('Are you sure you want to delete this theme?')) { - await removeTheme(theme.id) + startTransition(async () => { + await deleteTheme(theme.id) + }) } } - const handlePublicityToggle = async () => { - await updateThemePublicityOptimistic(theme.id, !theme.public) + const handlePublicityToggle = (checked: boolean) => { + startTransition(async () => { + await updateThemePublicity(theme.id, checked) + setIsPublic(checked) + }) } - const handleDownload = async () => { - await downloadTheme(theme.id) + const handleDownload = () => { + startTransition(async () => { + const vsixBuffer = await downloadThemeVSIX(theme.id) + if (vsixBuffer) { + const blob = new Blob([vsixBuffer], { + type: 'application/octet-stream', + }) + const url = window.URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `${theme.name}.vsix` + document.body.appendChild(a) + a.click() + window.URL.revokeObjectURL(url) + a.remove() + } + }) } - const handleEdit = () => { - // Implement edit functionality here - console.log('Edit theme:', theme.id) - } - - const handleShare = () => { - // Implement share functionality here - console.log('Share theme:', theme.id) - } - - const isPending = isThemePending(theme.id) - return (
= ({ theme, onPreview }) => { transition: 'opacity 0.2s', }} className="rounded-lg shadow-md overflow-hidden" + onClick={() => onPreview(theme)} + aria-disabled={isPending} >
@@ -75,8 +77,8 @@ const ThemeCard: React.FC = ({ theme, onPreview }) => {
handlePublicityToggle(checked)} disabled={isPending} /> @@ -88,7 +90,7 @@ const ThemeCard: React.FC = ({ theme, onPreview }) => { - -
diff --git a/src/components/ThemePreview.tsx b/src/components/ThemePreview.tsx index 4d140d3..3760267 100644 --- a/src/components/ThemePreview.tsx +++ b/src/components/ThemePreview.tsx @@ -13,6 +13,7 @@ const ThemePreview: React.FC = () => { uiSaturation, syntaxSaturation, scheme, + isPublic, } = useTheme() const [selectedFile, setSelectedFile] = @@ -117,6 +118,7 @@ const ThemePreview: React.FC = () => { isDark: isDark, id: 0, name: 'Custom', + public: isPublic, baseHue: baseHue, uiSaturation: uiSaturation, syntaxSaturation: syntaxSaturation, diff --git a/src/components/ThemeSaver.tsx b/src/components/ThemeSaver.tsx index aaa939b..1920f2e 100644 --- a/src/components/ThemeSaver.tsx +++ b/src/components/ThemeSaver.tsx @@ -3,10 +3,25 @@ import { useTheme } from '@/contexts/ThemeContext' import { Input } from '@/components/ui/input' import { ActionButton } from '@/components/ActionButton' import { useUser } from '@clerk/nextjs' +import { Switch } from '@/components/ui/switch' const ThemeSaver: React.FC = () => { - const { saveCurrentTheme, updateCurrentTheme, currentThemeId, savedThemes } = - useTheme() + const { + saveCurrentTheme, + updateCurrentTheme, + currentThemeId, + savedThemes, + isPublic, + setIsPublic, + colors, + syntaxColors, + ansiColors, + isDark, + baseHue, + uiSaturation, + syntaxSaturation, + scheme, + } = useTheme() const [themeName, setThemeName] = useState('') const { user } = useUser() @@ -19,38 +34,74 @@ const ThemeSaver: React.FC = () => { ) if (currentTheme) { setThemeName(currentTheme.name) + setIsPublic(currentTheme.public) } } else { setThemeName('') + setIsPublic(false) } - }, [currentThemeId, savedThemes]) + }, [currentThemeId, savedThemes, setIsPublic]) const handleSave = () => { + if (!user) return + startTransition(async () => { - if (!user) return if (currentThemeId) { - await updateCurrentTheme(themeName.trim()) + await updateCurrentTheme(currentThemeId, { + name: themeName.trim(), + public: isPublic, + uiColors: colors, + syntaxColors: syntaxColors, + ansiColors: ansiColors, + isDark: isDark, + baseHue: baseHue, + uiSaturation: uiSaturation, + syntaxSaturation: syntaxSaturation, + scheme: scheme, + }) } else { - await saveCurrentTheme(themeName.trim(), user.id) + await saveCurrentTheme({ + name: themeName.trim(), + userId: user.id, + public: isPublic, + uiColors: colors, + syntaxColors: syntaxColors, + ansiColors: ansiColors, + isDark: isDark, + baseHue: baseHue, + uiSaturation: uiSaturation, + syntaxSaturation: syntaxSaturation, + scheme: scheme, + }) } }) } return ( -
-
- setThemeName(e.target.value)} - placeholder="Enter theme name" +
+
+
+ setThemeName(e.target.value)} + placeholder="Enter theme name" + /> +
+ +
+
+ +
-
) } diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx index 848a7d7..994f3fd 100644 --- a/src/contexts/ThemeContext.tsx +++ b/src/contexts/ThemeContext.tsx @@ -24,11 +24,10 @@ import { } from '@/lib/types/colors' import { saveTheme, - getThemeById, getThemesByUserId, deleteTheme, - updateTheme as updateThemeInDb, - updateThemePublicityInDb, + updateTheme, + updateThemePublicity, } from '@/lib/db/themes' import type { SavedTheme } from '@/lib/types/colors' import { useTheme as useNextTheme } from 'next-themes' @@ -74,16 +73,23 @@ interface ThemeContextType { schemeHues: number[] savedThemes: SavedTheme[] setSavedThemes: (themes: SavedTheme[]) => void - saveCurrentTheme: (name: string, userId: string) => Promise + saveCurrentTheme: ( + theme: Omit + ) => Promise loadTheme: (theme: SavedTheme) => void deleteTheme: (themeId: number) => Promise - loadSavedThemes: () => Promise + loadSavedThemes: (userId: string) => Promise + updateThemePublicity: (themeId: number, isPublic: boolean) => Promise currentThemeId: number | null setCurrentThemeId: (id: number | null) => void - updateCurrentTheme: (name: string) => Promise + updateCurrentTheme: ( + id: number, + theme: Partial> + ) => Promise isOnigasmInitialized: boolean setIsOnigasmInitialized: (value: boolean) => void - updateThemePublicity: (themeId: number, isPublic: boolean) => Promise + isPublic: boolean + setIsPublic: (value: boolean) => void } const ThemeContext = createContext(undefined) @@ -111,51 +117,49 @@ export const ThemeProvider: React.FC<{ const [savedThemes, setSavedThemes] = useState([]) const [currentThemeId, setCurrentThemeId] = useState(null) const [isOnigasmInitialized, setIsOnigasmInitialized] = useState(false) - const loadSavedThemes = useCallback(async () => { + const [isPublic, setIsPublic] = useState(false) + + const loadSavedThemes = useCallback(async (userId: string) => { if (userId) { const themes = await getThemesByUserId(userId) setSavedThemes(themes) - return themes } - }, [userId]) + }, []) + + const saveCurrentTheme = useCallback( + async (theme: Omit) => { + const savedTheme = await saveTheme(theme) + setSavedThemes((prev) => [...prev, savedTheme]) + }, + [] + ) + + const updateCurrentTheme = useCallback( + async ( + id: number, + theme: Partial> + ) => { + const updatedTheme = await updateTheme(id, theme) + setSavedThemes((prev) => + prev.map((theme) => + theme.id === updatedTheme.id + ? { ...theme, ...(updatedTheme as Partial) } + : theme + ) + ) + }, + [] + ) useEffect(() => { const loadUserThemes = async () => { - const themes = await loadSavedThemes() + if (userId) { + await loadSavedThemes(userId) + } } loadUserThemes() }, [userId, loadSavedThemes]) - const saveCurrentTheme = useCallback( - async (name: string, userId: string) => { - const themeToSave: Omit = { - name, - userId, - public: false, - uiColors: colors, - syntaxColors: syntaxColors, - ansiColors: ansiColors, - isDark, - baseHue, - uiSaturation, - syntaxSaturation, - scheme, - } - const savedTheme = await saveTheme(themeToSave) - setSavedThemes((prev) => [...prev, savedTheme]) - }, - [ - colors, - syntaxColors, - ansiColors, - isDark, - baseHue, - uiSaturation, - syntaxSaturation, - scheme, - ] - ) - const loadTheme = useCallback( (theme: SavedTheme) => { setIsDarkState(theme.isDark) @@ -174,6 +178,7 @@ export const ThemeProvider: React.FC<{ ] as ColorScheme) ) setCurrentThemeId(theme.id) + setIsPublic(theme.public) }, [setTheme] ) @@ -183,16 +188,23 @@ export const ThemeProvider: React.FC<{ setSavedThemes((prev) => prev.filter((theme) => theme.id !== themeId)) }, []) - const updateThemePublicity = useCallback( + const updateThemePublicityInContext = useCallback( async (themeId: number, isPublic: boolean) => { - const updatedTheme = await updateThemePublicityInDb(themeId, isPublic) - setSavedThemes((prev) => - prev.map((theme) => - theme.id === updatedTheme.id ? updatedTheme : theme + try { + await updateThemePublicity(themeId, isPublic) + setSavedThemes((prev) => + prev.map((theme) => + theme.id === themeId ? { ...theme, public: isPublic } : theme + ) ) - ) + if (currentThemeId === themeId) { + setIsPublic(isPublic) + } + } catch (error) { + console.error('Failed to update theme publicity:', error) + } }, - [] + [currentThemeId] ) const generateColors = useCallback( @@ -379,48 +391,7 @@ export const ThemeProvider: React.FC<{ regenerateAnsiColors() }, [colors.BG1, regenerateAnsiColors]) - const updateCurrentTheme = useCallback( - async (name: string) => { - if (!currentThemeId || !userId) return - - const themeToUpdate: Omit< - SavedTheme, - 'id' | 'createdAt' | 'updatedAt' | 'public' - > = { - name, - userId, - uiColors: colors, - syntaxColors: syntaxColors, - ansiColors: ansiColors, - isDark, - baseHue, - uiSaturation, - syntaxSaturation, - scheme, - } - - const updatedTheme = await updateThemeInDb(currentThemeId, themeToUpdate) - setSavedThemes((prev) => - prev.map((theme) => - theme.id === updatedTheme.id ? updatedTheme : theme - ) - ) - }, - [ - currentThemeId, - userId, - colors, - syntaxColors, - ansiColors, - isDark, - baseHue, - uiSaturation, - syntaxSaturation, - scheme, - ] - ) - - const value = { + const value: ThemeContextType = { isDark, baseHue, uiSaturation, @@ -454,7 +425,9 @@ export const ThemeProvider: React.FC<{ updateCurrentTheme, isOnigasmInitialized, setIsOnigasmInitialized, - updateThemePublicity, + updateThemePublicity: updateThemePublicityInContext, + isPublic, + setIsPublic, } return {children} diff --git a/src/hooks/useThemes.tsx b/src/hooks/useThemes.tsx deleted file mode 100644 index 73d7b5f..0000000 --- a/src/hooks/useThemes.tsx +++ /dev/null @@ -1,113 +0,0 @@ -'use client' - -import { useState, useCallback } from 'react' -import { SavedTheme } from '@/lib/types/colors' -import { - updateThemePublicity, - deleteThemeFromDb, - downloadThemeVSIX, -} from '@/lib/db/themes' - -export function useThemes(initialThemes: SavedTheme[]) { - const [themes, setThemes] = useState(initialThemes) - const [pendingThemes, setPendingThemes] = useState>(new Set()) - const [downloadingThemes, setDownloadingThemes] = useState>( - new Set() - ) - - const removeTheme = useCallback(async (themeId: number) => { - setPendingThemes((prev) => new Set(prev).add(themeId)) - try { - await deleteThemeFromDb(themeId) - setThemes((prevThemes) => - prevThemes.filter((theme) => theme.id !== themeId) - ) - } finally { - setPendingThemes((prev) => { - const newSet = new Set(prev) - newSet.delete(themeId) - return newSet - }) - } - }, []) - - const updateThemePublicityOptimistic = useCallback( - async (themeId: number, isPublic: boolean) => { - setPendingThemes((prev) => new Set(prev).add(themeId)) - setThemes((prevThemes) => - prevThemes.map((theme) => - theme.id === themeId ? { ...theme, public: isPublic } : theme - ) - ) - - try { - await updateThemePublicity(themeId, isPublic) - } catch (error) { - // Revert the optimistic update if the server request fails - setThemes((prevThemes) => - prevThemes.map((theme) => - theme.id === themeId ? { ...theme, public: !isPublic } : theme - ) - ) - } finally { - setPendingThemes((prev) => { - const newSet = new Set(prev) - newSet.delete(themeId) - return newSet - }) - } - }, - [] - ) - - const downloadTheme = useCallback( - async (themeId: number) => { - setDownloadingThemes((prev) => new Set(prev).add(themeId)) - try { - const vsixBuffer = await downloadThemeVSIX(themeId) - if (vsixBuffer) { - const theme = themes.find((t) => t.id === themeId) - if (theme) { - const blob = new Blob([vsixBuffer], { - type: 'application/octet-stream', - }) - const url = window.URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = `${theme.name}.vsix` - document.body.appendChild(a) - a.click() - window.URL.revokeObjectURL(url) - a.remove() - } - } - } finally { - setDownloadingThemes((prev) => { - const newSet = new Set(prev) - newSet.delete(themeId) - return newSet - }) - } - }, - [themes] - ) - - const isThemePending = useCallback( - (themeId: number) => pendingThemes.has(themeId), - [pendingThemes] - ) - - const isThemeDownloading = useCallback( - (themeId: number) => downloadingThemes.has(themeId), - [downloadingThemes] - ) - - return { - themes, - removeTheme, - updateThemePublicityOptimistic, - downloadTheme, - isThemePending, - isThemeDownloading, - } -} diff --git a/src/lib/db/themes.ts b/src/lib/db/themes.ts index 0c8b8f2..f880b4b 100644 --- a/src/lib/db/themes.ts +++ b/src/lib/db/themes.ts @@ -22,14 +22,14 @@ export async function updateThemePublicity(themeId: number, isPublic: boolean) { revalidatePath('/saved-themes') } -export async function deleteThemeFromDb(themeId: number) { +export async function deleteTheme(themeId: number) { await db.delete(ThemesTable).where(eq(ThemesTable.id, themeId)) revalidatePath('/saved-themes') } -export const saveTheme = async ( +export async function saveTheme( theme: Omit -) => { +) { const result = await db .insert(ThemesTable) .values({ @@ -37,10 +37,12 @@ export const saveTheme = async ( scheme: theme.scheme.toString(), // Convert enum to string if present }) .returning() - return parseSavedTheme(result[0]) + const savedTheme = parseSavedTheme(result[0]) + revalidatePath('/saved-themes') + return savedTheme } -export async function updateThemePublicityInDb( +async function updateThemePublicityInDb( themeId: number, isPublic: boolean ): Promise { @@ -53,10 +55,10 @@ export async function updateThemePublicityInDb( return parseSavedTheme(updatedTheme) } -export const updateTheme = async ( +export async function updateTheme( id: number, theme: Partial> -) => { +) { const updateData = { ...theme, updatedAt: new Date(), @@ -68,12 +70,12 @@ export const updateTheme = async ( .set(updateData) .where(eq(ThemesTable.id, id)) .returning() - return parseSavedTheme(result[0]) + const updatedTheme = parseSavedTheme(result[0]) + revalidatePath('/saved-themes') + return updatedTheme } -export const getThemesByUserId = async ( - userId: string -): Promise => { +export async function getThemesByUserId(userId: string): Promise { const results = await db .select() .from(ThemesTable) @@ -81,7 +83,7 @@ export const getThemesByUserId = async ( return results.map(parseSavedTheme) } -export const getThemeById = async (id: number): Promise => { +export async function getThemeById(id: number): Promise { const result = await db .select() .from(ThemesTable) @@ -90,6 +92,14 @@ export const getThemeById = async (id: number): Promise => { return result[0] ? parseSavedTheme(result[0]) : null } +export async function getPublicThemes(): Promise { + const results = await db + .select() + .from(ThemesTable) + .where(eq(ThemesTable.public, true)) + return results.map(parseSavedTheme) +} + function safeJsonParse(value: any) { if (typeof value === 'string') { try { @@ -102,14 +112,6 @@ function safeJsonParse(value: any) { return value } -export const getPublicThemes = async (): Promise => { - const results = await db - .select() - .from(ThemesTable) - .where(eq(ThemesTable.public, true)) - return results.map(parseSavedTheme) -} - function parseSavedTheme(rawTheme: any): SavedTheme { const parsedTheme = { ...rawTheme,