diff --git a/admin/src/api/themes.ts b/admin/src/api/themes.ts index 3390e55..e3658af 100644 --- a/admin/src/api/themes.ts +++ b/admin/src/api/themes.ts @@ -29,6 +29,11 @@ export interface ThemeSettings { particleSymbol?: string particleColor?: string particleGlow?: string + particleTargetCount?: number + particleUpdateInterval?: number + particleDurationMin?: number + particleDurationMax?: number + particleInitialDelayMax?: number } export interface Theme { @@ -251,4 +256,9 @@ export const DEFAULT_THEME_SETTINGS: ThemeSettings = { particleSymbol: '❄', particleColor: '#ffffff', particleGlow: 'rgba(255, 255, 255, 0.8)', + particleTargetCount: 200, + particleUpdateInterval: 1000, + particleDurationMin: 7, + particleDurationMax: 10, + particleInitialDelayMax: 10, } diff --git a/admin/src/components/ThemeEditorDialog.tsx b/admin/src/components/ThemeEditorDialog.tsx index 8862a90..f84670c 100644 --- a/admin/src/components/ThemeEditorDialog.tsx +++ b/admin/src/components/ThemeEditorDialog.tsx @@ -218,7 +218,7 @@ export function ThemeEditorDialog({ setColors((prev) => ({ ...prev, [key]: value })) } - const updateSetting = (key: keyof ThemeSettings, value: string | boolean) => { + const updateSetting = (key: keyof ThemeSettings, value: string | boolean | number) => { setSettings((prev) => ({ ...prev, [key]: value })) } @@ -594,6 +594,120 @@ export function ThemeEditorDialog({ description="Цвет свечения частиц. По умолчанию используется Text Glow цвет" /> +
+

Animation Settings (Настройки анимации)

+
+
+ + { + const value = parseInt(e.target.value, 10) + if (!isNaN(value) && value > 0) { + updateSetting('particleTargetCount', value) + } + }} + /> +

+ Целевое количество снежинок на экране +

+
+
+ + { + const value = parseInt(e.target.value, 10) + if (!isNaN(value) && value > 0) { + updateSetting('particleUpdateInterval', value) + } + }} + /> +

+ Интервал обновления частиц в миллисекундах +

+
+
+ + { + const value = parseFloat(e.target.value) + if (!isNaN(value) && value > 0) { + updateSetting('particleDurationMin', value) + } + }} + /> +

+ Минимальная длительность анимации в секундах +

+
+
+ + { + const value = parseFloat(e.target.value) + if (!isNaN(value) && value > 0) { + updateSetting('particleDurationMax', value) + } + }} + /> +

+ Максимальная длительность анимации в секундах +

+
+
+ + { + const value = parseFloat(e.target.value) + if (!isNaN(value) && value >= 0) { + updateSetting('particleInitialDelayMax', value) + } + }} + /> +

+ Максимальная начальная задержка для первой партии частиц в секундах +

+
+
+
diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts index dfe6a84..80a3d5f 100644 --- a/backend/prisma/seed.ts +++ b/backend/prisma/seed.ts @@ -287,6 +287,11 @@ async function main() { particleSymbol: '❄', particleColor: '#ffffff', particleGlow: 'rgba(255, 215, 0, 0.8)', + particleTargetCount: 200, + particleUpdateInterval: 1000, + particleDurationMin: 7, + particleDurationMax: 10, + particleInitialDelayMax: 10, }, }, { @@ -322,6 +327,11 @@ async function main() { particleSymbol: '🌸', particleColor: '#2d3748', particleGlow: 'rgba(47, 128, 237, 0.6)', + particleTargetCount: 200, + particleUpdateInterval: 1000, + particleDurationMin: 7, + particleDurationMax: 10, + particleInitialDelayMax: 10, }, }, { @@ -357,6 +367,11 @@ async function main() { particleSymbol: '🎉', particleColor: '#ffffff', particleGlow: 'rgba(255, 87, 108, 0.8)', + particleTargetCount: 200, + particleUpdateInterval: 1000, + particleDurationMin: 7, + particleDurationMax: 10, + particleInitialDelayMax: 10, }, }, { @@ -392,6 +407,11 @@ async function main() { particleSymbol: '✨', particleColor: '#e0e0e0', particleGlow: 'rgba(100, 255, 218, 0.6)', + particleTargetCount: 200, + particleUpdateInterval: 1000, + particleDurationMin: 7, + particleDurationMax: 10, + particleInitialDelayMax: 10, }, }, ]; diff --git a/backend/src/admin/themes/dto/create-theme.dto.ts b/backend/src/admin/themes/dto/create-theme.dto.ts index 91ba427..d7c8eea 100644 --- a/backend/src/admin/themes/dto/create-theme.dto.ts +++ b/backend/src/admin/themes/dto/create-theme.dto.ts @@ -1,6 +1,7 @@ import { IsString, IsBoolean, + IsNumber, IsOptional, IsObject, ValidateNested, @@ -85,6 +86,31 @@ export class ThemeSettingsDto { @IsString() @IsOptional() particleGlow?: string; + + @IsNumber() + @IsOptional() + @Type(() => Number) + particleTargetCount?: number; + + @IsNumber() + @IsOptional() + @Type(() => Number) + particleUpdateInterval?: number; + + @IsNumber() + @IsOptional() + @Type(() => Number) + particleDurationMin?: number; + + @IsNumber() + @IsOptional() + @Type(() => Number) + particleDurationMax?: number; + + @IsNumber() + @IsOptional() + @Type(() => Number) + particleInitialDelayMax?: number; } export class CreateThemeDto { diff --git a/src/components/Answer.css b/src/components/Answer.css index ecaccda..52ba2a9 100644 --- a/src/components/Answer.css +++ b/src/components/Answer.css @@ -13,7 +13,7 @@ min-height: 0; position: relative; overflow-y: auto; - max-height: clamp(120px, 20vh, 250px); + height: 100%; width: 100%; /* Firefox scrollbar */ scrollbar-width: thin; @@ -27,7 +27,6 @@ align-items: center; justify-content: space-between; gap: clamp(8px, 1.5vw, 15px); - max-height: clamp(100px, 30vh, 200px); } } diff --git a/src/components/GameManagementModal.jsx b/src/components/GameManagementModal.jsx index 3914586..808dfd8 100644 --- a/src/components/GameManagementModal.jsx +++ b/src/components/GameManagementModal.jsx @@ -74,7 +74,8 @@ const GameManagementModal = ({ { text: '', points: 10 }, ]) const [jsonError, setJsonError] = useState('') - const [showPackImport, setShowPackImport] = useState(false) + const [showCreateQuestionModal, setShowCreateQuestionModal] = useState(false) + const [showPackImportModal, setShowPackImportModal] = useState(false) const [selectedPack, setSelectedPack] = useState(null) const [packQuestions, setPackQuestions] = useState([]) const [selectedQuestionIndices, setSelectedQuestionIndices] = useState(new Set()) @@ -193,10 +194,12 @@ const GameManagementModal = ({ setQuestionText(question.text) setAnswers([...question.answers]) setJsonError('') + setShowCreateQuestionModal(true) } const handleCancelEditQuestion = () => { resetQuestionForm() + setShowCreateQuestionModal(false) } const handleAnswerChange = (index, field, value) => { @@ -263,6 +266,7 @@ const GameManagementModal = ({ onUpdateQuestions(updatedQuestions) resetQuestionForm() + setShowCreateQuestionModal(false) } const handleDeleteQuestion = (questionId) => { @@ -395,18 +399,21 @@ const GameManagementModal = ({ const handleExportJson = () => { try { // Нормализуем вопросы, удаляя поле question если оно есть - const normalizedQuestions = questions.map(q => { - const { question, ...questionWithoutQuestion } = q - return { - ...questionWithoutQuestion, - text: q.text || '', - answers: q.answers.map(a => ({ - id: a.id, - text: a.text, - points: a.points - })) - } - }) + // Если вопросов нет, экспортируем пустой массив + const normalizedQuestions = questions.length > 0 + ? questions.map(q => { + const { question, ...questionWithoutQuestion } = q + return { + ...questionWithoutQuestion, + text: q.text || '', + answers: q.answers.map(a => ({ + id: a.id, + text: a.text, + points: a.points + })) + } + }) + : [] const jsonString = JSON.stringify(normalizedQuestions, null, 2) const blob = new Blob([jsonString], { type: 'application/json' }) const url = URL.createObjectURL(blob) @@ -423,49 +430,6 @@ const GameManagementModal = ({ } } - const handleDownloadTemplate = () => { - const template = [ - { - text: 'Назовите самый популярный вид спорта в мире', - answers: [ - { text: 'Футбол', points: 100 }, - { text: 'Баскетбол', points: 80 }, - { text: 'Теннис', points: 60 }, - { text: 'Хоккей', points: 40 }, - { text: 'Волейбол', points: 20 }, - { text: 'Бокс', points: 10 }, - ] - }, - { - text: 'Что люди обычно берут с собой на пляж?', - answers: [ - { text: 'Полотенце', points: 100 }, - { text: 'Крем от солнца', points: 80 }, - { text: 'Очки', points: 60 }, - { text: 'Зонт', points: 40 }, - { text: 'Книга', points: 20 }, - { text: 'Еда', points: 10 }, - ] - } - ] - - try { - const jsonString = JSON.stringify(template, null, 2) - const blob = new Blob([jsonString], { type: 'application/json' }) - const url = URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = url - link.download = 'template_questions.json' - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - URL.revokeObjectURL(url) - setJsonError('') - } catch (error) { - setJsonError('Ошибка при скачивании шаблона: ' + error.message) - } - } - const handleImportJson = () => { const input = document.createElement('input') input.type = 'file' @@ -630,7 +594,7 @@ const GameManagementModal = ({ setSelectedQuestionIndices(new Set()) setSearchQuery('') - setShowPackImport(false) + setShowPackImportModal(false) setJsonError('') alert(`Импортировано ${copiedQuestions.length} вопросов`) } @@ -1094,11 +1058,22 @@ const GameManagementModal = ({
+ {availablePacks.length > 0 && ( + + )} - {availablePacks.length > 0 && ( - - )}
{jsonError && (
{jsonError}
)} - {showPackImport && availablePacks.length > 0 && ( -
-

Импорт вопросов из пака

- - - {packQuestions.length > 0 && ( -
- {/* Поиск */} -
- setSearchQuery(e.target.value)} - placeholder="🔍 Поиск вопросов..." - className="pack-search-input" - /> -
- -
-
- Выберите вопросы для импорта: -
- {filteredPackQuestions.length > 0 && ( - <> - {areAllVisibleSelected() ? ( - - ) : ( - - )} - - )} -
-
- -
- -
- {filteredPackQuestions.length === 0 ? ( -
- {searchQuery ? 'Вопросы не найдены' : 'Нет вопросов в паке'} -
- ) : ( - filteredPackQuestions.map((q, filteredIdx) => { - const originalIndex = packQuestions.findIndex(pq => pq === q) - return ( -
- handleToggleQuestion(originalIndex)} - /> -
- {q.text || ''} - - {q.answers?.length || 0} ответов - -
- -
- ) - }) - )} -
-
- )} - - {/* Модальное окно просмотра вопроса */} - {viewingQuestion && ( -
-
e.stopPropagation()}> -
-

Просмотр вопроса

- -
-
-
- {viewingQuestion.text || ''} -
- - {showAnswers && ( -
- {viewingQuestion.answers?.map((answer, idx) => ( -
- {answer.text} - {answer.points} очков -
- ))} -
- )} -
-
-
- )} -
- )} - -
- setQuestionText(e.target.value)} - placeholder="Введите текст вопроса" - className="questions-modal-input" - /> - -
-
- Ответы: - -
- - {answers.map((answer, index) => ( -
- handleAnswerChange(index, 'text', e.target.value)} - placeholder={`Ответ ${index + 1}`} - className="questions-modal-answer-input" - /> - handleAnswerChange(index, 'points', e.target.value)} - className="questions-modal-points-input" - min="0" - /> - {answers.length > 1 && ( - - )} -
- ))} -
- -
- - {editingQuestion && ( - - )} -
-
-
-

+

Вопросы ({questions.length})

{questions.length === 0 ? ( @@ -1430,6 +1190,249 @@ const GameManagementModal = ({ )}
+ + {/* Модалка создания вопроса */} + {showCreateQuestionModal && ( +
{ + if (e.target === e.currentTarget) { + handleCancelEditQuestion() + } + }}> +
e.stopPropagation()}> +
+

{editingQuestion ? 'Редактировать вопрос' : 'Добавить вопрос'}

+ +
+ + {jsonError && ( +
{jsonError}
+ )} + +
+ setQuestionText(e.target.value)} + placeholder="Введите текст вопроса" + className="questions-modal-input" + /> + +
+
+ Ответы: + +
+ + {answers.map((answer, index) => ( +
+ handleAnswerChange(index, 'text', e.target.value)} + placeholder={`Ответ ${index + 1}`} + className="questions-modal-answer-input" + /> + handleAnswerChange(index, 'points', e.target.value)} + className="questions-modal-points-input" + min="0" + /> + {answers.length > 1 && ( + + )} +
+ ))} +
+ +
+ + +
+
+
+
+ )} + + {/* Модалка импорта из пака */} + {showPackImportModal && availablePacks.length > 0 && ( +
{ + if (e.target === e.currentTarget) { + setShowPackImportModal(false) + } + }}> +
e.stopPropagation()}> +
+

Импорт вопросов из пака

+ +
+ +
+ + + {packQuestions.length > 0 && ( +
+ {/* Поиск */} +
+ setSearchQuery(e.target.value)} + placeholder="🔍 Поиск вопросов..." + className="pack-search-input" + /> +
+ +
+
+ Выберите вопросы для импорта: +
+ {filteredPackQuestions.length > 0 && ( + <> + {areAllVisibleSelected() ? ( + + ) : ( + + )} + + )} +
+
+ +
+ +
+ {filteredPackQuestions.length === 0 ? ( +
+ {searchQuery ? 'Вопросы не найдены' : 'Нет вопросов в паке'} +
+ ) : ( + filteredPackQuestions.map((q, filteredIdx) => { + const originalIndex = packQuestions.findIndex(pq => pq === q) + return ( +
+ handleToggleQuestion(originalIndex)} + /> +
+ {q.text || ''} + + {q.answers?.length || 0} ответов + +
+ +
+ ) + }) + )} +
+
+ )} + + {/* Модальное окно просмотра вопроса */} + {viewingQuestion && ( +
+
e.stopPropagation()}> +
+

Просмотр вопроса

+ +
+
+
+ {viewingQuestion.text || ''} +
+ + {showAnswers && ( +
+ {viewingQuestion.answers?.map((answer, idx) => ( +
+ {answer.text} + {answer.points} очков +
+ ))} +
+ )} +
+
+
+ )} +
+
+
+ )} ) } diff --git a/src/components/Question.css b/src/components/Question.css index 01a6457..27d2437 100644 --- a/src/components/Question.css +++ b/src/components/Question.css @@ -127,7 +127,7 @@ .answers-grid { display: grid; grid-template-columns: repeat(2, 1fr); - grid-auto-rows: auto; + grid-auto-rows: minmax(auto, clamp(120px, 18vh, 200px)); column-gap: clamp(6px, 1.2vw, 12px); row-gap: clamp(6px, 0.8vh, 12px); flex: 1; @@ -152,3 +152,17 @@ } } +/* Для очень высоких экранов (ТВ и т.д.) */ +@media (min-height: 1080px) { + .answers-grid { + grid-auto-rows: minmax(auto, 180px); + } +} + +/* Для телефонов в портретной ориентации */ +@media (max-width: 768px) and (max-height: 900px) { + .answers-grid { + grid-auto-rows: minmax(auto, 150px); + } +} + diff --git a/src/components/QuestionsModal.css b/src/components/QuestionsModal.css index 40e55dd..44ceeb3 100644 --- a/src/components/QuestionsModal.css +++ b/src/components/QuestionsModal.css @@ -855,3 +855,168 @@ } } +/* Question Create Modal */ +.question-create-modal-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(5px); + display: flex; + justify-content: center; + align-items: center; + z-index: 2000; + padding: 20px; +} + +.question-create-modal-content { + background: rgba(20, 20, 30, 0.95); + backdrop-filter: blur(20px); + border-radius: 20px; + padding: clamp(15px, 3vh, 25px); + max-width: 800px; + width: 100%; + max-height: 90vh; + overflow-y: auto; + border: 2px solid rgba(255, 215, 0, 0.3); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + position: relative; +} + +.question-create-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: clamp(15px, 2vh, 20px); + padding-bottom: 15px; + border-bottom: 2px solid rgba(255, 215, 0, 0.2); +} + +.question-create-modal-header h3 { + color: var(--accent-primary); + font-size: clamp(1.3rem, 2.5vw, 1.8rem); + margin: 0; + text-shadow: 0 0 10px rgba(255, 215, 0, 0.5); +} + +.question-create-modal-close { + background: rgba(255, 107, 107, 0.2); + color: #ff6b6b; + border: 2px solid rgba(255, 107, 107, 0.5); + border-radius: 50%; + width: 40px; + height: 40px; + font-size: 2rem; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + flex-shrink: 0; +} + +.question-create-modal-close:hover { + background: rgba(255, 107, 107, 0.4); + border-color: #ff6b6b; + transform: scale(1.1); +} + +/* Pack Import Modal */ +.pack-import-modal-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(5px); + display: flex; + justify-content: center; + align-items: center; + z-index: 2000; + padding: 20px; +} + +.pack-import-modal-content { + background: rgba(20, 20, 30, 0.95); + backdrop-filter: blur(20px); + border-radius: 20px; + padding: clamp(15px, 3vh, 25px); + max-width: 900px; + width: 100%; + max-height: 90vh; + overflow-y: auto; + border: 2px solid rgba(255, 215, 0, 0.3); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + position: relative; +} + +.pack-import-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: clamp(15px, 2vh, 20px); + padding-bottom: 15px; + border-bottom: 2px solid rgba(255, 215, 0, 0.2); +} + +.pack-import-modal-header h3 { + color: var(--accent-primary); + font-size: clamp(1.3rem, 2.5vw, 1.8rem); + margin: 0; + text-shadow: 0 0 10px rgba(255, 215, 0, 0.5); +} + +.pack-import-modal-close { + background: rgba(255, 107, 107, 0.2); + color: #ff6b6b; + border: 2px solid rgba(255, 107, 107, 0.5); + border-radius: 50%; + width: 40px; + height: 40px; + font-size: 2rem; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + flex-shrink: 0; +} + +.pack-import-modal-close:hover { + background: rgba(255, 107, 107, 0.4); + border-color: #ff6b6b; + transform: scale(1.1); +} + +/* Add button style */ +.questions-modal-add-button { + flex: 1; + padding: 12px 20px; + background: linear-gradient(135deg, #4ecdc4 0%, #44a08d 100%); + color: var(--text-primary); + border: none; + border-radius: 12px; + font-size: 1rem; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; +} + +.questions-modal-add-button:hover { + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(78, 205, 196, 0.4); +} + +/* Pack import section inside modal */ +.pack-import-modal-content .pack-import-section { + margin: 0; + padding: 0; + border: none; + background: transparent; +} + diff --git a/src/components/Snowflakes.jsx b/src/components/Snowflakes.jsx index d24c48d..5ddbd76 100644 --- a/src/components/Snowflakes.jsx +++ b/src/components/Snowflakes.jsx @@ -1,15 +1,27 @@ import { useEffect, useState } from 'react' import { useTheme } from '../context/ThemeContext' -const TARGET_COUNT = 30 // Target number of snowflakes -const UPDATE_INTERVAL = 500 // Check every 500ms +// Default values for particle animation settings +const DEFAULT_TARGET_COUNT = 200 +const DEFAULT_UPDATE_INTERVAL = 1000 +const DEFAULT_DURATION_MIN = 7 +const DEFAULT_DURATION_MAX = 10 +const DEFAULT_INITIAL_DELAY_MAX = 10 -function createSnowflake(id, isInitial = false) { +function createSnowflake(id, options = {}) { + const { + durationMin = DEFAULT_DURATION_MIN, + durationMax = DEFAULT_DURATION_MAX, + initialDelayMax = DEFAULT_INITIAL_DELAY_MAX, + isInitial = false, + } = options + + const durationRange = durationMax - durationMin return { id: id || crypto.randomUUID(), left: Math.random() * 100, - duration: Math.random() * 3 + 7, // 7-10s - delay: isInitial ? Math.random() * 2 : 0, // Only delay initial batch + duration: Math.random() * durationRange + durationMin, + delay: isInitial ? Math.random() * initialDelayMax : 0, size: Math.random() * 10 + 10, // 10-20px opacity: Math.random() * 0.5 + 0.5, // 0.5-1 createdAt: Date.now(), @@ -40,19 +52,50 @@ const Snowflakes = ({ roomParticlesEnabled = null }) => { return currentThemeData?.settings?.particleSymbol || '❄' } + // Get particle animation settings from theme with defaults + const getParticleTargetCount = () => { + return currentThemeData?.settings?.particleTargetCount ?? DEFAULT_TARGET_COUNT + } + + const getParticleUpdateInterval = () => { + return currentThemeData?.settings?.particleUpdateInterval ?? DEFAULT_UPDATE_INTERVAL + } + + const getParticleDurationRange = () => { + return { + min: currentThemeData?.settings?.particleDurationMin ?? DEFAULT_DURATION_MIN, + max: currentThemeData?.settings?.particleDurationMax ?? DEFAULT_DURATION_MAX, + } + } + + const getParticleInitialDelayMax = () => { + return currentThemeData?.settings?.particleInitialDelayMax ?? DEFAULT_INITIAL_DELAY_MAX + } + const particlesEnabled = getParticlesEnabled() const particleSymbol = getParticleSymbol() + const targetCount = getParticleTargetCount() + const updateInterval = getParticleUpdateInterval() + const durationRange = getParticleDurationRange() + const initialDelayMax = getParticleInitialDelayMax() // Initialize snowflakes only if particles are enabled - // Also re-initialize when theme changes (particleSymbol might change) + // Also re-initialize when theme changes (particle settings might change) useEffect(() => { if (!particlesEnabled) { setSnowflakes([]) return } - const initial = Array.from({ length: TARGET_COUNT }, (_, i) => createSnowflake(i, true)) + const initial = Array.from({ length: targetCount }, (_, i) => + createSnowflake(i, { + durationMin: durationRange.min, + durationMax: durationRange.max, + initialDelayMax, + isInitial: true, + }) + ) setSnowflakes(initial) - }, [particlesEnabled, particleSymbol, currentThemeData]) + }, [particlesEnabled, particleSymbol, targetCount, durationRange.min, durationRange.max, initialDelayMax, currentThemeData]) // Update cycle - remove old snowflakes and add new ones useEffect(() => { @@ -73,16 +116,23 @@ const Snowflakes = ({ roomParticlesEnabled = null }) => { // Add new snowflakes if below target const newFlakes = [...filtered] - while (newFlakes.length < TARGET_COUNT) { - newFlakes.push(createSnowflake()) + while (newFlakes.length < targetCount) { + newFlakes.push( + createSnowflake(null, { + durationMin: durationRange.min, + durationMax: durationRange.max, + initialDelayMax, + isInitial: false, + }) + ) } return newFlakes }) - }, UPDATE_INTERVAL) + }, updateInterval) return () => clearInterval(interval) - }, [particlesEnabled]) + }, [particlesEnabled, targetCount, updateInterval, durationRange.min, durationRange.max, initialDelayMax]) // Don't render if particles are disabled if (!particlesEnabled) { diff --git a/src/context/ThemeContext.jsx b/src/context/ThemeContext.jsx index f897e94..c86b985 100644 --- a/src/context/ThemeContext.jsx +++ b/src/context/ThemeContext.jsx @@ -88,8 +88,22 @@ export const ThemeProvider = ({ children }) => { // Apply theme settings if (theme.settings) { Object.entries(theme.settings).forEach(([key, value]) => { - // Skip boolean values (like particlesEnabled) - they are handled separately - if (typeof value !== 'boolean' && value !== null && value !== undefined) { + // Skip boolean and number values - they are handled separately (numbers for JS, booleans for logic) + // Only string values are used as CSS variables (except particle animation numbers) + const isParticleNumber = [ + 'particleTargetCount', + 'particleUpdateInterval', + 'particleDurationMin', + 'particleDurationMax', + 'particleInitialDelayMax', + ].includes(key); + + if ( + typeof value === 'string' && + !isParticleNumber && + value !== null && + value !== undefined + ) { root.style.setProperty(`--${camelToKebab(key)}`, String(value)); } });