Skip to content

Commit 6b69e9d

Browse files
committed
Better theme editing state management
1 parent 62abcb0 commit 6b69e9d

File tree

10 files changed

+284
-200
lines changed

10 files changed

+284
-200
lines changed

apps/web/components/admin/page-editor/index.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {
5252
SelectValue,
5353
} from "@/components/ui/select";
5454
import { ThemeWithDraftState } from "./theme-editor/theme-with-draft-state";
55+
import useThemes from "./use-themes";
5556

5657
const EditWidget = dynamic(() => import("./edit-widget"));
5758
const AddWidget = dynamic(() => import("./add-widget"));
@@ -112,6 +113,7 @@ export default function PageEditor({
112113
const { toast } = useToast();
113114
const [pages, setPages] = useState<Page[]>([]);
114115
const [loadingPages, setLoadingPages] = useState(true);
116+
const { theme: lastEditedTheme } = useThemes();
115117

116118
const router = useRouter();
117119
const debouncedSave = useCallback(
@@ -182,6 +184,12 @@ export default function PageEditor({
182184
}
183185
}, [draftTypefaces]);
184186

187+
useEffect(() => {
188+
if (lastEditedTheme) {
189+
setDraftTheme(lastEditedTheme);
190+
}
191+
}, [lastEditedTheme]);
192+
185193
const onItemClick = (widgetId: string) => {
186194
setLayout([...layout]);
187195
setSelectedWidget(widgetId);
@@ -841,7 +849,7 @@ export default function PageEditor({
841849
<Skeleton className="w-full h-10" />
842850
</div>
843851
)}
844-
{draftTypefaces.length > 0 && (
852+
{draftTypefaces.length > 0 && draftTheme && (
845853
<Template
846854
layout={layout}
847855
pageData={page.pageData || {}}

apps/web/components/admin/page-editor/theme-editor/index.tsx

Lines changed: 39 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
import React, { useState, useContext, useEffect, useCallback } from "react";
1+
import React, {
2+
useState,
3+
useContext,
4+
useEffect,
5+
useCallback,
6+
useRef,
7+
} from "react";
28
import { ExpandMoreRight } from "@courselit/icons";
3-
import { ColorSelector, Skeleton } from "@courselit/components-library";
9+
import { ColorSelector } from "@courselit/components-library";
410
import { capitalize, FetchBuilder, truncate } from "@courselit/utils";
511
import {
612
Columns3,
@@ -22,6 +28,8 @@ import StructureSelector from "./structure-selector";
2228
import { ThemeCard } from "./theme-card";
2329
import { Theme } from "@courselit/page-models";
2430
import { ThemeWithDraftState } from "./theme-with-draft-state";
31+
import useThemes from "../use-themes";
32+
import { ThemeCardSkeleton } from "./theme-card-skeleton";
2533

2634
type Section = {
2735
id: string;
@@ -120,12 +128,7 @@ function ThemeEditor({
120128
}: {
121129
onThemeChange: (theme: Theme) => void;
122130
}) {
123-
const [themes, setThemes] = useState<{
124-
system: Theme[];
125-
custom: Theme[];
126-
}>({ system: [], custom: [] });
127-
const [firstLoad, setFirstLoad] = useState(true);
128-
const [theme, setTheme] = useState<Theme | null>(null);
131+
const { themes, theme, setTheme, loadThemes, loaded } = useThemes();
129132
const [navigationStack, setNavigationStack] = useState<NavigationItem[]>(
130133
[],
131134
);
@@ -136,97 +139,28 @@ function ThemeEditor({
136139
const address = useContext(AddressContext);
137140
const { theme: currentTheme, setTheme: setCurrentTheme } =
138141
useContext(ThemeContext);
142+
const selectedThemeRef = useRef<HTMLDivElement>(null);
139143

140144
useEffect(() => {
141145
if (theme) {
142146
onThemeChange(theme);
143-
replaceThemeInThemesArray(theme);
144147
}
145148
}, [theme]);
146149

147-
const replaceThemeInThemesArray = useCallback((theme: Theme) => {
148-
setThemes((prev) => ({
149-
...prev,
150-
system: prev.system.map((t) => (t.id === theme.id ? theme : t)),
151-
custom: prev.custom.map((t) => (t.id === theme.id ? theme : t)),
152-
}));
153-
}, []);
154-
155-
const loadThemes = useCallback(async () => {
156-
setIsLoading(true);
157-
const query = `
158-
query {
159-
themes: getThemes {
160-
system {
161-
themeId
162-
name
163-
theme {
164-
colors
165-
typography
166-
interactives
167-
structure
168-
}
169-
draftTheme {
170-
colors
171-
typography
172-
interactives
173-
structure
174-
}
175-
}
176-
custom {
177-
themeId
178-
name
179-
theme {
180-
colors
181-
typography
182-
interactives
183-
structure
184-
}
185-
draftTheme {
186-
colors
187-
typography
188-
interactives
189-
structure
190-
}
191-
}
192-
}
193-
}
194-
`;
195-
const fetch = new FetchBuilder()
196-
.setUrl(`${address.backend}/api/graph`)
197-
.setPayload({ query })
198-
.setIsGraphQLEndpoint(true)
199-
.build();
200-
201-
try {
202-
const { themes } = await fetch.exec();
203-
if (themes) {
204-
setThemes({
205-
system: themes.system.map(transformServerTheme),
206-
custom: themes.custom.map(transformServerTheme),
207-
});
208-
}
209-
} catch (error) {
210-
console.error(error);
211-
} finally {
212-
setIsLoading(false);
213-
}
214-
}, [address.backend]);
215-
216-
useEffect(() => {
217-
loadThemes();
218-
}, [loadThemes]);
219-
220150
useEffect(() => {
221-
if (currentTheme?.id) {
222-
let theme = themes.system.find((t) => t.id === currentTheme.id);
223-
theme = themes.custom.find((t) => t.id === currentTheme.id);
224-
if (theme && firstLoad) {
225-
setTheme(theme);
226-
setFirstLoad(false);
227-
}
151+
if (loaded) {
152+
setIsLoading(false);
153+
// Scroll to selected theme after a short delay to ensure DOM is ready
154+
setTimeout(() => {
155+
if (selectedThemeRef.current) {
156+
selectedThemeRef.current.scrollIntoView({
157+
behavior: "smooth",
158+
block: "center",
159+
});
160+
}
161+
}, 100);
228162
}
229-
}, [themes, currentTheme]);
163+
}, [loaded, theme?.id]);
230164

231165
const navigateTo = useCallback((item: NavigationItem) => {
232166
setNavigationStack((prev) => [...prev, item]);
@@ -290,7 +224,6 @@ function ThemeEditor({
290224
const updatedThemeNew =
291225
transformServerTheme(updatedTheme);
292226
setTheme(updatedThemeNew);
293-
setCurrentTheme(updatedThemeNew);
294227
loadThemes();
295228
}
296229
}
@@ -373,34 +306,15 @@ function ThemeEditor({
373306
</div>
374307
<div className="space-y-2">
375308
{isLoading
376-
? // Skeleton for system themes
377-
Array(3)
309+
? Array(3)
378310
.fill(0)
379311
.map((_, index) => (
380-
<div
381-
key={index}
382-
className="w-full flex items-center justify-between px-3 py-3 rounded-md"
383-
>
384-
<div className="flex items-center gap-3">
385-
<div className="flex gap-1">
386-
{Array(5)
387-
.fill(0)
388-
.map((_, i) => (
389-
<Skeleton
390-
key={i}
391-
className="w-4 h-4 rounded-full"
392-
/>
393-
))}
394-
</div>
395-
<Skeleton className="h-4 w-32" />
396-
</div>
397-
<Skeleton className="h-4 w-4" />
398-
</div>
312+
<ThemeCardSkeleton key={index} />
399313
))
400314
: themes.system.map((themeItem) => (
401315
<ThemeCard
402316
key={themeItem.id}
403-
name={truncate(themeItem.name, 20)}
317+
name={truncate(themeItem.name, 30)}
404318
palette={colorOrder
405319
.map(
406320
(key) =>
@@ -416,6 +330,11 @@ function ThemeEditor({
416330
}}
417331
showUseButton={true}
418332
className="cursor-pointer"
333+
ref={
334+
themeItem.id === theme?.id
335+
? selectedThemeRef
336+
: null
337+
}
419338
onClick={() => {
420339
setTheme(themeItem);
421340
setNavigationStack([
@@ -434,29 +353,10 @@ function ThemeEditor({
434353
</div>
435354
<div className="space-y-2">
436355
{isLoading ? (
437-
// Skeleton for custom themes
438356
Array(2)
439357
.fill(0)
440358
.map((_, index) => (
441-
<div
442-
key={index}
443-
className="w-full flex items-center justify-between px-3 py-3 rounded-md"
444-
>
445-
<div className="flex items-center gap-3">
446-
<div className="flex gap-1">
447-
{Array(5)
448-
.fill(0)
449-
.map((_, i) => (
450-
<Skeleton
451-
key={i}
452-
className="w-4 h-4 rounded-full"
453-
/>
454-
))}
455-
</div>
456-
<Skeleton className="h-4 w-32" />
457-
</div>
458-
<Skeleton className="h-4 w-4" />
459-
</div>
359+
<ThemeCardSkeleton key={index} />
460360
))
461361
) : themes.custom.length === 0 ? (
462362
<div className="text-muted-foreground text-sm">
@@ -466,7 +366,7 @@ function ThemeEditor({
466366
themes.custom.map((themeItem) => (
467367
<ThemeCard
468368
key={themeItem.id}
469-
name={truncate(themeItem.name, 20)}
369+
name={truncate(themeItem.name, 30)}
470370
palette={colorOrder
471371
.map(
472372
(key) =>
@@ -483,6 +383,11 @@ function ThemeEditor({
483383
}}
484384
showUseButton={true}
485385
className="cursor-pointer"
386+
ref={
387+
themeItem.id === theme?.id
388+
? selectedThemeRef
389+
: null
390+
}
486391
onClick={() => {
487392
setTheme(themeItem);
488393
setNavigationStack([

apps/web/components/admin/page-editor/theme-editor/interactive-selector.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Input } from "@/components/ui/input";
1010
import { Label } from "@/components/ui/label";
1111
import { cn } from "@/lib/shadcn-utils";
1212
import { Info } from "lucide-react";
13-
import { Theme } from "@courselit/common-models";
13+
import { ThemeStyle } from "@courselit/page-models";
1414
import {
1515
paddingOptions,
1616
borderWidthOptions,
@@ -38,8 +38,8 @@ import {
3838
interface InteractiveSelectorProps {
3939
title: string;
4040
type: "button" | "link" | "card" | "input";
41-
theme: Theme;
42-
onChange: (theme: Theme) => void;
41+
theme: ThemeStyle;
42+
onChange: (theme: ThemeStyle) => void;
4343
}
4444

4545
const interactiveDisplayNames: Record<string, string> = {

apps/web/components/admin/page-editor/theme-editor/structure-selector.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ import {
77
SelectValue,
88
} from "@/components/ui/select";
99
import { Label } from "@/components/ui/label";
10-
import { Theme } from "@courselit/common-models";
1110
import { paddingOptions } from "./tailwind-to-human-readable";
1211
import { Separator } from "@components/ui/separator";
12+
import { ThemeStyle } from "@courselit/page-models";
1313

1414
interface StructureSelectorProps {
1515
title: string;
1616
type: "page" | "section";
17-
theme: Theme;
18-
onChange: (theme: Theme) => void;
17+
theme: ThemeStyle;
18+
onChange: (theme: ThemeStyle) => void;
1919
}
2020

2121
const pageWidthOptions = [
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React from "react";
2+
import { Card, CardContent, CardFooter } from "@/components/ui/card";
3+
import { Skeleton } from "@/components/ui/skeleton";
4+
import { cn } from "@/lib/shadcn-utils";
5+
6+
interface ThemeCardSkeletonProps {
7+
className?: string;
8+
}
9+
10+
export function ThemeCardSkeleton({ className = "" }: ThemeCardSkeletonProps) {
11+
return (
12+
<Card
13+
className={cn(
14+
"w-full px-0 py-0 bg-background flex flex-col border border-muted",
15+
className,
16+
)}
17+
>
18+
<CardContent className="pb-2 pt-2 px-2">
19+
<div className="flex items-center justify-between min-h-[24px]">
20+
<Skeleton className="h-5 w-24" />
21+
</div>
22+
</CardContent>
23+
<CardFooter className="flex items-center justify-between pt-0 pb-2 px-2">
24+
<div className="flex gap-2">
25+
{[...Array(5)].map((_, i) => (
26+
<Skeleton key={i} className="w-4 h-4 rounded-full" />
27+
))}
28+
</div>
29+
<Skeleton className="h-8 w-[56px] rounded-md" />
30+
</CardFooter>
31+
</Card>
32+
);
33+
}

0 commit comments

Comments
 (0)