mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
fix: building working, (lots of bugs)
This commit is contained in:
@@ -1,50 +0,0 @@
|
||||
import TabbedContainer from '../components/TabbedContainer';
|
||||
import Settings from './SettingsPage/Settings';
|
||||
import logo from '@/resources/icons/betterseqta-dark-full.png';
|
||||
import logoDark from '@/resources/icons/betterseqta-light-full.png';
|
||||
import { SettingsContextProvider } from '../SettingsContext';
|
||||
import Shortcuts from './SettingsPage/Shortcuts';
|
||||
import Picker from '../components/Picker';
|
||||
import Themes from './SettingsPage/Themes';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import { memo } from 'react';
|
||||
|
||||
import browser from 'webextension-polyfill';
|
||||
|
||||
interface SettingsPage {
|
||||
standalone: boolean;
|
||||
}
|
||||
|
||||
const SettingsPage = ({ standalone }: SettingsPage) => {
|
||||
const tabs = [
|
||||
{
|
||||
title: 'Settings',
|
||||
content: <Settings />
|
||||
},
|
||||
{
|
||||
title: 'Shortcuts',
|
||||
content: <Shortcuts />
|
||||
},
|
||||
{
|
||||
title: 'Themes',
|
||||
content: <Themes />
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsContextProvider>
|
||||
<ToastContainer stacked toastStyle={{ borderRadius: '16px' }} draggable theme={document.body.classList.contains('dark') ? 'dark' : 'light'} />
|
||||
<div className={`flex flex-col w-[384px] shadow-2xl gap-2 bg-white ${ standalone ? 'h-[600px]' : 'h-[100vh] rounded-xl' } overflow-clip dark:bg-zinc-800 dark:text-white`}>
|
||||
<div className="grid border-b border-b-zinc-200/40 place-items-center">
|
||||
<img src={logo} className="w-4/5 dark:hidden" />
|
||||
<img src={logoDark} className="hidden w-4/5 dark:block" />
|
||||
<button onClick={() => browser.runtime.sendMessage({ type: 'currentTab', info: 'OpenChangelog' })} className="absolute w-8 h-8 text-lg rounded-xl font-IconFamily top-1 right-1 bg-zinc-100 dark:bg-zinc-700"></button>
|
||||
</div>
|
||||
<Picker />
|
||||
<TabbedContainer tabs={tabs} animations={false} />
|
||||
</div>
|
||||
</SettingsContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SettingsPage);
|
||||
@@ -1,19 +0,0 @@
|
||||
const About = () => {
|
||||
return (
|
||||
<div className="flex flex-col overflow-y-scroll divide-y divide-zinc-100/50 dark:divide-zinc-700/50">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold">About</h2>
|
||||
<p className="py-2">BetterSEQTA+ is a branch of BetterSEQTA which was originally developed by Nulkem. It was discontinued. So BetterSEQTA+ has come in to fill in that gap!</p>
|
||||
<p className="py-2">We are currently working on fixing bugs and adding new features. If you want to request a feature or report a bug, you can do so on
|
||||
<a className="pl-1 text-blue-500 underline hover:text-blue-600" href="https://github.com/BetterSEQTA/BetterSEQTA-plus" target="_blank">Github</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="pt-2 text-lg font-bold">Credits</h2>
|
||||
<p className="py-2">Nulkem for the original extension, OG-RandomTechChannel, Crazypersonalph, and the current maintainer SethBurkart123</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
||||
@@ -1,160 +0,0 @@
|
||||
import Switch from '../../components/Switch';
|
||||
import Slider from '../../components/Slider';
|
||||
import PickerSwatch from '../../components/PickerSwatch';
|
||||
import Select from '../../components/Select';
|
||||
|
||||
import { SettingsList } from '../../types/SettingsProps';
|
||||
import { useSettingsContext } from '../../SettingsContext';
|
||||
|
||||
import browser from 'webextension-polyfill';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
const Settings: React.FC = () => {
|
||||
const { settingsState, setSettingsState } = useSettingsContext();
|
||||
|
||||
const handleDevModeToggle = useCallback(() => {
|
||||
const secretSequence = 'dev';
|
||||
let typedSequence = '';
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
typedSequence += event.key.toLowerCase();
|
||||
|
||||
if (typedSequence.includes(secretSequence)) {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
setSettingsState(prevState => ({
|
||||
...prevState,
|
||||
devMode: !prevState.devMode
|
||||
}));
|
||||
|
||||
alert(`Dev mode is now ${!settingsState.devMode ? 'enabled' : 'disabled'}`);
|
||||
}
|
||||
|
||||
// Clear the sequence after 2 seconds of inactivity
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
typedSequence = '';
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
// Cleanup function to remove the event listener
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [setSettingsState, settingsState.devMode]);
|
||||
|
||||
const handleSettingChange = useCallback((key: string, value: boolean | string | number) => {
|
||||
setSettingsState(prevState => ({
|
||||
...prevState,
|
||||
[key]: value,
|
||||
}));
|
||||
}, [setSettingsState]);
|
||||
|
||||
const settings: SettingsList[] = [
|
||||
{
|
||||
title: "Transparency Effects",
|
||||
description: "Enables transparency effects on certain elements such as blur. (May impact battery life)",
|
||||
modifyElement: <Switch state={settingsState.transparencyEffects} onChange={(isOn: boolean) => handleSettingChange('transparencyEffects', isOn)} />
|
||||
},
|
||||
{
|
||||
title: "Animated Background",
|
||||
description: "Adds an animated background to BetterSEQTA. (May impact battery life)",
|
||||
modifyElement: <Switch state={settingsState.animatedBackground} onChange={(isOn: boolean) => handleSettingChange('animatedBackground', isOn)} />
|
||||
},
|
||||
{
|
||||
title: "Animated Background Speed",
|
||||
description: "Controls the speed of the animated background.",
|
||||
modifyElement: <Slider state={parseInt(settingsState.animatedBackgroundSpeed)} onChange={(value: number) => handleSettingChange('animatedBackgroundSpeed', value)} />
|
||||
},
|
||||
{
|
||||
title: "Custom Theme Colour",
|
||||
description: "Customise the overall theme colour of SEQTA Learn.",
|
||||
modifyElement: <PickerSwatch />
|
||||
},
|
||||
{
|
||||
title: "Edit Sidebar Layout",
|
||||
description: "Customise the sidebar layout.",
|
||||
modifyElement: <button onClick={() => browser.runtime.sendMessage({ type: 'currentTab', info: 'EditSidebar' })} className='px-4 py-1 text-[0.75rem] dark:bg-[#38373D] bg-[#DDDDDD] dark:text-white rounded-md'>Edit</button>
|
||||
},
|
||||
{
|
||||
title: "Animations",
|
||||
description: "Enables animations on certain pages.",
|
||||
modifyElement: <Switch state={settingsState.animations} onChange={(isOn: boolean) => handleSettingChange('animations', isOn)} />
|
||||
},
|
||||
{
|
||||
title: "Notification Collector",
|
||||
description: "Uncaps the 9+ limit for notifications, showing the real number.",
|
||||
modifyElement: <Switch state={settingsState.notificationCollector} onChange={(isOn: boolean) => handleSettingChange('notificationCollector', isOn)} />
|
||||
},
|
||||
{
|
||||
title: "Lesson Alerts",
|
||||
description: "Sends a native browser notification ~5 minutes prior to lessons.",
|
||||
modifyElement: <Switch state={settingsState.lessonAlerts} onChange={(isOn: boolean) => handleSettingChange('lessonAlerts', isOn)} />
|
||||
},
|
||||
{
|
||||
title: "12 Hour Time",
|
||||
description: "Prefer 12 hour time format for SEQTA",
|
||||
modifyElement: <Switch state={settingsState.timeFormat == "12"} onChange={(isOn: boolean) => handleSettingChange('timeFormat', isOn ? "12" : "24")} />
|
||||
},
|
||||
{
|
||||
title: "Default Page",
|
||||
description: "The page to load when SEQTA Learn is opened.",
|
||||
modifyElement: <Select state={settingsState.defaultPage} onChange={(value: string) => handleSettingChange('defaultPage', value)} options={[
|
||||
{ value: 'home', label: 'Home' },
|
||||
{ value: 'dashboard', label: 'Dashboard' },
|
||||
{ value: 'timetable', label: 'Timetable' },
|
||||
{ value: 'welcome', label: 'Welcome' },
|
||||
{ value: 'messages', label: 'Messages' },
|
||||
{ value: 'documents', label: 'Documents' },
|
||||
{ value: 'reports', label: 'Reports' },
|
||||
]} />
|
||||
},
|
||||
{
|
||||
title: "BetterSEQTA+",
|
||||
description: "Enables BetterSEQTA+ features",
|
||||
modifyElement: <Switch state={settingsState.betterSEQTAPlus} onChange={(isOn: boolean) => handleSettingChange('betterSEQTAPlus', isOn)} />
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col -mt-4 overflow-y-scroll divide-y divide-zinc-100 dark:divide-zinc-700">
|
||||
{settings.map((setting, index) => (
|
||||
<div className="flex items-center justify-between px-4 py-3" key={index}>
|
||||
<div className="pr-4">
|
||||
<h2 onClick={setting.title.includes('BetterSEQTA+') ? handleDevModeToggle : undefined} className="text-sm font-bold">{setting.title}</h2>
|
||||
<p className="text-xs">{setting.description}</p>
|
||||
</div>
|
||||
<div>
|
||||
{setting.modifyElement}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{settingsState.devMode && (
|
||||
<>
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<div className="pr-4">
|
||||
<h2 className="text-sm font-bold">Dev Mode</h2>
|
||||
<p className="text-xs">Enables dev mode</p>
|
||||
</div>
|
||||
<Switch state={settingsState.devMode} onChange={(isOn: boolean) => handleSettingChange('devMode', isOn)} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<div className="pr-4">
|
||||
<h2 className="text-sm font-bold">Sensitive Hider</h2>
|
||||
<p className="text-xs">Replace sensitive content with mock data</p>
|
||||
</div>
|
||||
<button onClick={() => browser.runtime.sendMessage({ type: 'currentTab', info: 'HideSensitive' })} className='px-4 py-1 text-[0.75rem] dark:bg-[#38373D] bg-[#DDDDDD] dark:text-white rounded-md'>Hide</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Settings);
|
||||
@@ -1,147 +0,0 @@
|
||||
import { memo, useCallback, useState } from "react";
|
||||
import Switch from "../../components/Switch";
|
||||
import { useSettingsContext } from "../../SettingsContext";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { CustomShortcut } from "../../types/AppProps";
|
||||
|
||||
function formatUrl(inputUrl: string) {
|
||||
const protocolRegex = /^(http:\/\/|https:\/\/|ftp:\/\/)/;
|
||||
return protocolRegex.test(inputUrl) ? inputUrl : `https://${inputUrl}`;
|
||||
}
|
||||
|
||||
const Shortcuts = memo(() => {
|
||||
const { settingsState, setSettingsState } = useSettingsContext();
|
||||
|
||||
const [newTitle, setNewTitle] = useState<string>("");
|
||||
const [isFormVisible, setFormVisible] = useState(false);
|
||||
const [newURL, setNewURL] = useState<string>("");
|
||||
|
||||
const switchChange = useCallback((shortcutName: string, isOn: boolean) => {
|
||||
setSettingsState((prevState) => {
|
||||
const updatedShortcuts = prevState.shortcuts.map((shortcut) =>
|
||||
shortcut.name === shortcutName ? { ...shortcut, enabled: isOn } : shortcut
|
||||
);
|
||||
return { ...prevState, shortcuts: updatedShortcuts };
|
||||
});
|
||||
}, [setSettingsState]);
|
||||
|
||||
const isValidTitle = useCallback((title: string) => title.trim() !== "", []);
|
||||
|
||||
const isValidURL = useCallback((url: string) => {
|
||||
const pattern = new RegExp("^(https?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\-]+)*(?::\\d+)?(/[\\w\\-./]*)*$", "i");
|
||||
return pattern.test(url);
|
||||
}, []);
|
||||
|
||||
const addNewCustomShortcut = useCallback(() => {
|
||||
if (isValidTitle(newTitle) && isValidURL(newURL)) {
|
||||
const newShortcut: CustomShortcut = { name: newTitle.trim(), url: formatUrl(newURL).trim(), icon: newTitle[0] };
|
||||
const updatedCustomShortcuts = [...settingsState.customshortcuts, newShortcut];
|
||||
setSettingsState({ ...settingsState, customshortcuts: updatedCustomShortcuts });
|
||||
|
||||
setNewTitle("");
|
||||
setNewURL("");
|
||||
|
||||
setFormVisible(false);
|
||||
} else {
|
||||
// Replace with a more user-friendly way to display errors
|
||||
alert("Please enter a valid title and URL.");
|
||||
}
|
||||
}, [newTitle, newURL, isValidTitle, isValidURL, setSettingsState]);
|
||||
|
||||
const deleteCustomShortcut = useCallback((index: number) => {
|
||||
setSettingsState((prevState) => ({
|
||||
...prevState,
|
||||
customshortcuts: prevState.customshortcuts.filter((_, i) => i !== index),
|
||||
}));
|
||||
}, [setSettingsState]);
|
||||
|
||||
const toggleForm = useCallback(() => {
|
||||
setFormVisible((isVisible) => !isVisible);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col divide-y divide-zinc-100 dark:divide-zinc-700">
|
||||
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={isFormVisible ? { opacity: 1, height: "auto" } : { opacity: 0, height: 0 }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ type: "spring", damping: 20 }}
|
||||
>
|
||||
{isFormVisible &&
|
||||
<div className="flex flex-col items-center mb-4">
|
||||
<motion.input
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="w-full p-2 rounded-md bg-zinc-100 dark:bg-zinc-700 focus:outline-none"
|
||||
type="text"
|
||||
placeholder="Shortcut Name"
|
||||
value={newTitle}
|
||||
onChange={(e) => setNewTitle(e.target.value)}
|
||||
/>
|
||||
<motion.input
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="w-full p-2 my-2 rounded-md bg-zinc-100 dark:bg-zinc-700 focus:outline-none"
|
||||
type="text"
|
||||
placeholder="URL eg. https://google.com"
|
||||
value={newURL}
|
||||
onChange={(e) => setNewURL(e.target.value)}
|
||||
/>
|
||||
<motion.button
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
className="w-full px-4 py-2 text-white bg-blue-500 rounded-md"
|
||||
onClick={ addNewCustomShortcut }
|
||||
>
|
||||
Add
|
||||
</motion.button>
|
||||
</div>
|
||||
}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
{!isFormVisible && (
|
||||
<button
|
||||
className="w-full px-4 py-2 mb-4 text-white bg-blue-500 rounded"
|
||||
onClick={toggleForm}
|
||||
>
|
||||
Add Custom Shortcut
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Shortcuts Section */}
|
||||
{settingsState.shortcuts ? (
|
||||
settingsState.shortcuts.map((shortcut, index) => shortcut.name && (
|
||||
<div className="flex items-center justify-between px-4 py-3" key={index}>
|
||||
{shortcut.name}
|
||||
<Switch state={shortcut.enabled} onChange={(isOn) => switchChange(shortcut.name, isOn)} />
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p>Loading shortcuts...</p>
|
||||
)}
|
||||
|
||||
{/* Custom Shortcuts Section */}
|
||||
{settingsState.customshortcuts ? (
|
||||
settingsState.customshortcuts.map((shortcut, index) => (
|
||||
<div className="flex items-center justify-between px-4 py-3" key={index}>
|
||||
{shortcut.name}
|
||||
<button onClick={() => deleteCustomShortcut(index)}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p>Loading custom shortcuts...</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default Shortcuts;
|
||||
@@ -1,30 +0,0 @@
|
||||
import { createRef, FC, useState } from 'react';
|
||||
import BackgroundSelector from '../../components/BackgroundSelector';
|
||||
import ThemeSelector from '../../components/ThemeSelector';
|
||||
import { memo } from 'react';
|
||||
|
||||
type ThemeSelectorRef = {
|
||||
disableTheme: () => void;
|
||||
};
|
||||
|
||||
const Themes: FC = () => {
|
||||
const [isEditMode, setIsEditMode] = useState<boolean>(false);
|
||||
const themeSelectorRef = createRef<ThemeSelectorRef>();
|
||||
|
||||
const disableTheme = async () => {
|
||||
themeSelectorRef?.current?.disableTheme();
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<button className="absolute top-12 z-20 right-0 p-2 text-[0.8rem] text-blue-500" onClick={() => setIsEditMode(!isEditMode)}>
|
||||
{isEditMode ? 'Done' : 'Edit'}
|
||||
</button>
|
||||
<BackgroundSelector disableTheme={disableTheme} isEditMode={isEditMode} />
|
||||
<ThemeSelector ref={themeSelectorRef} isEditMode={isEditMode} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Themes);
|
||||
@@ -1,335 +0,0 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import Header from '../components/store/header';
|
||||
import { Autoplay } from 'swiper/modules';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/pagination';
|
||||
import 'swiper/css/scrollbar';
|
||||
import 'swiper/css/autoplay';
|
||||
import SpinnerIcon from '../components/LoadingSpinner';
|
||||
import localforage from 'localforage';
|
||||
import { StoreDownloadTheme } from '../../seqta/ui/themes/downloadTheme';
|
||||
|
||||
const textVariants = {
|
||||
hidden: { opacity: 0, y: 60 },
|
||||
visible: { opacity: 1, y: 0, transition: {
|
||||
type: 'spring',
|
||||
bounce: 0,
|
||||
stiffness: 80,
|
||||
damping: 12
|
||||
} },
|
||||
};
|
||||
|
||||
const containerVariants = {
|
||||
hidden: {
|
||||
y: '100vh',
|
||||
},
|
||||
visible: {
|
||||
y: 0,
|
||||
transition: {
|
||||
staggerChildren: 0.05,
|
||||
type: 'spring',
|
||||
bounce: 0,
|
||||
stiffness: 400,
|
||||
damping: 50
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export type Theme = {
|
||||
name: string;
|
||||
description: string;
|
||||
coverImage: string;
|
||||
marqueeImage: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
type ThemesResponse = {
|
||||
themes: Theme[];
|
||||
}
|
||||
|
||||
export const DeleteDownloadedTheme = async (themeID: string) => {
|
||||
console.debug('DeleteDownloaded Theme:', themeID)
|
||||
await localforage.removeItem(themeID);
|
||||
|
||||
const availableThemesList = await localforage.getItem('availableThemes') as string[];
|
||||
const updatedThemesList = availableThemesList.filter(theme => theme !== themeID);
|
||||
|
||||
await localforage.setItem('availableThemes', updatedThemesList);
|
||||
}
|
||||
|
||||
const Store = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const swiperCover = useRef<any | null>(null);
|
||||
const [gridThemes, setGridThemes] = useState<Theme[]>([]);
|
||||
const [filteredThemes, setFilteredThemes] = useState<Theme[]>([]);
|
||||
const [coverThemes, setCoverThemes] = useState<Theme[]>([]);
|
||||
const [installingThemes, setInstallingThemes] = useState<string[]>([]);
|
||||
const [currentThemes, setCurrentThemes] = useState<string[]>([]);
|
||||
const [displayTheme, setDisplayTheme] = useState<Theme | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
const fetchThemes = async () => {
|
||||
try {
|
||||
const response = await fetch(`https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/store/themes.json?nocache=${(new Date()).getTime()}`, { cache: 'no-store' });
|
||||
const data: ThemesResponse = await response.json();
|
||||
setGridThemes(data.themes);
|
||||
// Select up to 3 random themes to display in coverThemes
|
||||
const shuffled = [...data.themes].sort(() => 0.5 - Math.random());
|
||||
setCoverThemes(shuffled.slice(0, 3));
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch themes', error);
|
||||
// Retry after 5 seconds
|
||||
setTimeout(fetchThemes, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
document.title = 'BetterSEQTA+ Store';
|
||||
|
||||
fetchThemes();
|
||||
const availableThemes = await localforage.getItem('availableThemes') as string[] | null;
|
||||
if (availableThemes) {
|
||||
setCurrentThemes(availableThemes)
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setFilteredThemes(gridThemes.filter(theme =>
|
||||
theme.name.toLowerCase().includes(searchTerm.toLowerCase()) || theme.description.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
));
|
||||
}, [searchTerm, gridThemes]);
|
||||
|
||||
const downloadTheme = (id: string) => {
|
||||
const themeContent = gridThemes.find(theme => theme.id === id);
|
||||
if (!themeContent) {
|
||||
alert('There was an error, The theme was not found!')
|
||||
return
|
||||
}
|
||||
|
||||
setInstallingThemes([...installingThemes, id]);
|
||||
|
||||
StoreDownloadTheme({ themeContent }).then(() => {
|
||||
setInstallingThemes(installingThemes.filter(theme => theme !== id));
|
||||
setCurrentThemes([...currentThemes, id]);
|
||||
});
|
||||
};
|
||||
|
||||
const removeTheme = async (id: string) => {
|
||||
const themeContent = gridThemes.find(theme => theme.id === id);
|
||||
if (!themeContent) {
|
||||
alert('There was an error, The theme was not found!')
|
||||
return
|
||||
}
|
||||
|
||||
setInstallingThemes([...installingThemes, id]);
|
||||
|
||||
DeleteDownloadedTheme(id).then(() => {
|
||||
setInstallingThemes(installingThemes.filter(theme => theme !== id));
|
||||
setCurrentThemes(currentThemes.filter(theme => theme !== id));
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-screen h-screen overflow-y-scroll pt-[4.25rem] bg-zinc-200/50 dark:bg-zinc-900 dark:text-white">
|
||||
|
||||
<Header searchTerm={searchTerm} setSearchTerm={setSearchTerm} />
|
||||
|
||||
{/* loader */}
|
||||
<div className={`flex items-center justify-center w-full h-full ${!loading && 'hidden'}`}>
|
||||
<SpinnerIcon className="w-16 h-16" />
|
||||
</div>
|
||||
|
||||
<div className={`px-24 py-12 ${loading && 'hidden'}`}>
|
||||
<div className={`relative w-full rounded-xl overflow-clip transition-opacity ${searchTerm == '' ? 'opacity-100' : 'opacity-0'}`}>
|
||||
<motion.div className='overflow-clip' animate={{
|
||||
height: searchTerm == '' ? 'auto' : '0px'
|
||||
}} transition={{
|
||||
type: 'spring',
|
||||
bounce: 0,
|
||||
duration: 1,
|
||||
stiffness: 200,
|
||||
damping: 30
|
||||
}}>
|
||||
<Swiper
|
||||
ref={swiperCover}
|
||||
spaceBetween={20}
|
||||
slidesPerView={1}
|
||||
loop={true}
|
||||
className='w-full aspect-[8/3]'
|
||||
modules={[Autoplay]}
|
||||
autoplay={{
|
||||
delay: 5000,
|
||||
stopOnLastSlide: false,
|
||||
disableOnInteraction: false,
|
||||
pauseOnMouseEnter: true
|
||||
}}
|
||||
>
|
||||
{ [...coverThemes, ...coverThemes].map((theme, index) => (
|
||||
<SwiperSlide className='relative cursor-pointer rounded-xl overflow-clip' onClick={() => setDisplayTheme(theme)} key={index}>
|
||||
<img
|
||||
src={theme.marqueeImage}
|
||||
alt="Theme Preview"
|
||||
className="object-cover w-full h-full"
|
||||
/>
|
||||
<div className='absolute bottom-0 left-0 p-8 z-[1]'>
|
||||
<h2 className='text-4xl font-bold text-white'>{theme.name}</h2>
|
||||
<p className='text-lg text-white'>{theme.description}</p>
|
||||
</div>
|
||||
|
||||
{/* shadow from the bottom of the image */}
|
||||
<div className='absolute bottom-0 left-0 w-full h-1/2 bg-gradient-to-t from-black/80 to-transparent' />
|
||||
</SwiperSlide>
|
||||
)) }
|
||||
</Swiper>
|
||||
</motion.div>
|
||||
|
||||
<div className={displayTheme ? 'pointer-events-auto' : 'pointer-events-none'}>
|
||||
<AnimatePresence>
|
||||
{displayTheme && (
|
||||
<motion.div
|
||||
className={`fixed inset-0 z-50 flex items-end justify-center bg-black bg-opacity-70`}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={() => setDisplayTheme(null)}
|
||||
>
|
||||
<motion.div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="w-full max-w-xl h-[95%] p-4 bg-white rounded-t-2xl dark:bg-zinc-800 overflow-scroll"
|
||||
exit={{ y: "100vh" }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<motion.div className="relative h-auto">
|
||||
<motion.button
|
||||
className="absolute top-0 right-0 p-2 text-xl font-bold text-gray-600 font-IconFamily dark:text-gray-200"
|
||||
onClick={() => setDisplayTheme(null)}
|
||||
variants={textVariants}
|
||||
>
|
||||
{'\ued8a'}
|
||||
</motion.button>
|
||||
<motion.h2 className="mb-4 text-2xl font-bold" variants={textVariants}>
|
||||
{displayTheme.name}
|
||||
</motion.h2>
|
||||
<motion.img src={displayTheme.marqueeImage} alt="Theme Cover" className="object-cover w-full mb-4 rounded-md" variants={textVariants} />
|
||||
<motion.p className="mb-4 text-gray-700 dark:text-gray-300" variants={textVariants}>
|
||||
{displayTheme.description}
|
||||
</motion.p>
|
||||
{
|
||||
currentThemes.includes(displayTheme.id) ?
|
||||
<motion.button
|
||||
variants={textVariants}
|
||||
onClick={() => removeTheme(displayTheme.id)}
|
||||
className="flex px-4 py-2 mt-4 ml-auto rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600/50 hover:bg-zinc-200 focus:outline-none focus:ring-2 focus:ring-zinc-800 focus:ring-offset-2">
|
||||
{ installingThemes.includes(displayTheme.id) ?
|
||||
<>
|
||||
<SpinnerIcon className="w-4 h-4 mr-2" />
|
||||
Removing...
|
||||
</> :
|
||||
<> Remove </>
|
||||
}
|
||||
</motion.button> :
|
||||
<motion.button
|
||||
variants={textVariants}
|
||||
onClick={() => downloadTheme(displayTheme.id)}
|
||||
className="flex px-4 py-2 mt-4 ml-auto rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600/50 hover:bg-zinc-200 focus:outline-none focus:ring-2 focus:ring-zinc-800 focus:ring-offset-2">
|
||||
{ installingThemes.includes(displayTheme.id) ?
|
||||
<>
|
||||
<SpinnerIcon className="w-4 h-4 mr-2" />
|
||||
Installing...
|
||||
</> :
|
||||
<> Install </>
|
||||
}
|
||||
</motion.button>
|
||||
}
|
||||
|
||||
<motion.div className="my-8 border-b border-zinc-200 dark:border-zinc-700" variants={textVariants} />
|
||||
|
||||
<motion.h3 className="mb-4 text-lg font-bold" variants={textVariants}>
|
||||
Similar Themes
|
||||
</motion.h3>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{gridThemes.filter(theme => theme.id !== displayTheme.id).sort((a, b) => a.name.localeCompare(displayTheme.name) - b.name.localeCompare(displayTheme.name)).map((theme, index) => (
|
||||
<motion.div key={index} onClick={() => { setDisplayTheme(null); setDisplayTheme(theme); }} className='w-full cursor-pointer' variants={textVariants}>
|
||||
<div className="bg-gray-50 w-full transition-all hover:scale-105 duration-500 relative group group/card flex flex-col hover:shadow-2xl dark:hover:shadow-white/[0.1] hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] h-auto rounded-xl overflow-clip border">
|
||||
<div className="absolute z-10 mb-1 text-xl font-bold text-white transition-all duration-500 group-hover:-translate-y-0.5 bottom-1 left-3">
|
||||
{theme.name}
|
||||
</div>
|
||||
<div className='absolute bottom-0 z-0 w-full h-3/4 bg-gradient-to-t from-black/80 to-transparent' />
|
||||
<img src={theme.coverImage} alt="Theme Preview" className="object-cover w-full h-48" />
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* pagination */}
|
||||
<div className='absolute z-10 flex gap-2 bottom-2 right-2'>
|
||||
|
||||
<button onClick={ () => {swiperCover.current?.swiper.slidePrev() }} className='flex items-center justify-center w-8 h-8 text-white bg-black bg-opacity-50 rounded-full dark:bg-zinc-800 dark:bg-opacity-50'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m15.75 19.5-7.5-7.5 7.5-7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button onClick={ () => {swiperCover.current?.swiper.slideNext() }} className='flex items-center justify-center w-8 h-8 text-white bg-black bg-opacity-50 rounded-full dark:bg-zinc-800 dark:bg-opacity-50'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 py-12 mx-auto sm:grid-cols-2 lg:grid-cols-3">
|
||||
{filteredThemes.map((theme, index) => (
|
||||
<div onClick={() => setDisplayTheme(theme)} key={index} className='w-full cursor-pointer'>
|
||||
<div className="bg-gray-50 w-full transition-all hover:scale-105 duration-500 relative group group/card flex flex-col hover:shadow-2xl dark:hover:shadow-white/[0.1] hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] h-auto rounded-xl overflow-clip border">
|
||||
<div className="absolute z-10 mb-1 text-xl font-bold text-white transition-all duration-500 group-hover:-translate-y-0.5 bottom-1 left-3">
|
||||
{theme.name}
|
||||
</div>
|
||||
<div className='absolute bottom-0 z-0 w-full h-3/4 bg-gradient-to-t from-black/80 to-transparent' />
|
||||
<div
|
||||
className='w-full'>
|
||||
<img src={theme.coverImage} alt="Theme Preview" className="object-cover w-full h-48 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<a href="https://betterseqta.gitbook.io/betterseqta-docs" className='w-full cursor-pointer'>
|
||||
<div className="bg-zinc-50 h-48 w-full transition-all hover:scale-105 duration-500 relative justify-center items-center group group/card flex flex-col hover:shadow-2xl dark:hover:shadow-white/[0.1] hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] rounded-xl overflow-clip border">
|
||||
<div className="text-2xl font-IconFamily">{'\uecb3'}</div>
|
||||
<div className="text-xl font-bold text-center transition-all duration-500 dark:text-white">
|
||||
Got a Theme Idea?
|
||||
<p className="text-lg font-light subtitle">Transform it into a stunning theme!</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{filteredThemes.length == 0 && !loading && (
|
||||
<div className="flex flex-col items-center justify-center w-full text-center h-96">
|
||||
<h1 className="mt-4 text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-5xl">That doesnt exist! 😭😭😭</h1>
|
||||
<p className="mt-6 text-lg leading-7 text-zinc-600 dark:text-zinc-300">Sorry, we couldn’t find the theme you’re looking for. Maybe... you could create it?</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Store;
|
||||
@@ -1,341 +0,0 @@
|
||||
import CodeEditor from '../components/CodeEditor';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ColorPicker from 'react-best-gradient-color-picker';
|
||||
import Accordion from '../components/Accordian';
|
||||
import Switch from '../components/Switch';
|
||||
import { sendThemeUpdate } from '../hooks/ThemeManagment';
|
||||
import { MoonIcon, PlusIcon, SunIcon, XMarkIcon } from '@heroicons/react/24/outline';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { CustomTheme, CustomThemeBase64 } from '../types/CustomThemes';
|
||||
import browser from 'webextension-polyfill';
|
||||
|
||||
function ThemeCreator() {
|
||||
// default settings for new themes
|
||||
const [theme, setTheme] = useState<CustomTheme>({
|
||||
id: uuidv4(),
|
||||
name: '',
|
||||
description: '',
|
||||
defaultColour: '',
|
||||
CanChangeColour: true,
|
||||
allowBackgrounds: true,
|
||||
CustomCSS: '',
|
||||
CustomImages: [],
|
||||
coverImage: null,
|
||||
isEditable: true,
|
||||
hideThemeName: false,
|
||||
forceDark: undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const getTheme = async (themeID: string) => {
|
||||
const theme = await browser.runtime.sendMessage({
|
||||
type: 'currentTab',
|
||||
info: 'GetTheme',
|
||||
body: {
|
||||
themeID: themeID,
|
||||
}
|
||||
}) as CustomThemeBase64 | undefined;
|
||||
|
||||
if (theme) {
|
||||
// base64toblob to convert it to a blob url
|
||||
const CustomImages = theme.CustomImages.map((image) => {
|
||||
const base64Index = image.url.indexOf(',') + 1;
|
||||
const imageBase64 = image.url.substring(base64Index);
|
||||
|
||||
// Convert base64 to blob
|
||||
const byteCharacters = atob(imageBase64);
|
||||
const byteNumbers = new Uint8Array(byteCharacters.length);
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||
}
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
const blob = new Blob([byteArray], { type: 'image/png' });
|
||||
|
||||
return {
|
||||
id: image.id,
|
||||
blob: blob,
|
||||
variableName: image.variableName,
|
||||
};
|
||||
});
|
||||
|
||||
const coverImageBase64 = theme.coverImage;
|
||||
let coverImageBlob = null;
|
||||
|
||||
if (coverImageBase64) {
|
||||
const base64Index = coverImageBase64.indexOf(',') + 1;
|
||||
const imageBase64 = coverImageBase64.substring(base64Index);
|
||||
|
||||
// Convert base64 to blob
|
||||
const byteCharacters = atob(imageBase64);
|
||||
const byteNumbers = new Uint8Array(byteCharacters.length);
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||
}
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
coverImageBlob = new Blob([byteArray], { type: 'image/png' });
|
||||
}
|
||||
|
||||
setTheme({
|
||||
...theme,
|
||||
CustomImages,
|
||||
coverImage: coverImageBlob
|
||||
});
|
||||
|
||||
sendThemeUpdate({
|
||||
...theme,
|
||||
CustomImages: CustomImages,
|
||||
}, false, true);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// get ThemeID from URL params
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const themeID = urlParams.get('themeID');
|
||||
|
||||
if (themeID) {
|
||||
getTheme(themeID);
|
||||
}
|
||||
|
||||
}, []);
|
||||
|
||||
const generateImageId = () => {
|
||||
return Math.random().toString(36).substr(2, 9);
|
||||
};
|
||||
|
||||
const handleImageUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
event.target.value = '';
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
const imageBlob = await fetch(reader.result as string).then(res => res.blob());
|
||||
const imageId = generateImageId();
|
||||
const variableName = `custom-image-${theme.CustomImages.length}`;
|
||||
const updatedTheme = {
|
||||
...theme,
|
||||
CustomImages: [...theme.CustomImages, { id: imageId, blob: imageBlob, variableName }],
|
||||
};
|
||||
setTheme(updatedTheme);
|
||||
sendThemeUpdate(updatedTheme, false, true);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveImage = (imageId: string) => {
|
||||
const updatedTheme = {
|
||||
...theme,
|
||||
CustomImages: theme.CustomImages.filter((image) => image.id !== imageId),
|
||||
};
|
||||
setTheme(updatedTheme);
|
||||
};
|
||||
|
||||
const handleImageVariableChange = (imageId: string, variableName: string) => {
|
||||
const updatedTheme = {
|
||||
...theme,
|
||||
CustomImages: theme.CustomImages.map((image) =>
|
||||
image.id === imageId ? { ...image, variableName } : image
|
||||
),
|
||||
};
|
||||
setTheme(updatedTheme);
|
||||
};
|
||||
|
||||
function CodeUpdate(value: string) {
|
||||
setTheme((prevTheme) => ({
|
||||
...prevTheme,
|
||||
CustomCSS: value,
|
||||
}));
|
||||
}
|
||||
|
||||
const saveTheme = async () => {
|
||||
sendThemeUpdate(theme, true)
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
sendThemeUpdate(theme);
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<div className='w-full min-h-[100vh] bg-zinc-100 dark:bg-zinc-800 dark:text-white transition duration-30'>
|
||||
<div className='flex flex-col p-2'>
|
||||
<h1 className='text-xl font-semibold'>Theme Creator</h1>
|
||||
<a href="https://betterseqta.gitbook.io/betterseqta-docs" target="_blank" className='text-sm font-light text-zinc-500 dark:text-zinc-400'>
|
||||
<span className="no-underline font-IconFamily pr-0.5">{'\ueb44'}</span>
|
||||
<span className="underline">
|
||||
Need help? Check out the docs!
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div>
|
||||
<div className='pb-2 text-sm'>Theme Name</div>
|
||||
<input
|
||||
id='themeName'
|
||||
type='text'
|
||||
placeholder='What is your theme called?'
|
||||
value={theme.name}
|
||||
onChange={e => setTheme({ ...theme, name: e.target.value })}
|
||||
className='w-full p-2 mb-4 transition-all duration-300 rounded-lg focus:outline-none ring-0 focus:ring-1 ring-zinc-100 dark:ring-zinc-700 dark:bg-zinc-900 dark:text-white' />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className='pb-2 text-sm'>Description <span className='italic font-light opacity-80'>(optional)</span></div>
|
||||
<textarea
|
||||
id='themeDescription'
|
||||
placeholder="Don't worry, this one's optional!"
|
||||
value={theme.description}
|
||||
onChange={e => setTheme({ ...theme, description: e.target.value })}
|
||||
className='w-full p-2 rounded-lg focus:outline-none ring-0 focus:ring-1 ring-zinc-100 dark:ring-zinc-700 dark:bg-zinc-900 dark:text-white' />
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<p className='pr-2 text-sm font-semibold'>Cover Image <span className='italic font-light opacity-80'>(optional)</span></p>
|
||||
<p className='pb-3 text-sm'>Upload an image to use as the cover image for your theme. <span className='italic font-light'>Recommended resolution: 640x128</span></p>
|
||||
|
||||
<div className="relative flex justify-center w-full gap-1 overflow-hidden transition rounded-lg aspect-theme group place-items-center bg-zinc-100 dark:bg-zinc-900">
|
||||
<PlusIcon className={`transition pointer-events-none z-30 ${ theme.coverImage ? 'opacity-0 group-hover:opacity-100' : ''}`} height={18} />
|
||||
<span className={`dark:text-white pointer-events-none z-30 transition ${ theme.coverImage ? 'opacity-0 group-hover:opacity-100' : ''}`}>{theme.coverImage ? 'Change' : 'Add'} cover image</span>
|
||||
<input type="file" accept='image/*' onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
e.target.value = '';
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
const imageBlob = await fetch(reader.result as string).then(res => res.blob());
|
||||
setTheme({ ...theme, coverImage: imageBlob });
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}} className="absolute inset-0 z-10 w-full h-full opacity-0 cursor-pointer" />
|
||||
{
|
||||
!theme.hideThemeName && theme.coverImage ?
|
||||
<div className="absolute z-30 transition-opacity opacity-100 pointer-events-none group-hover:opacity-0">{theme.name}</div> : <></>
|
||||
}
|
||||
{
|
||||
theme.coverImage &&
|
||||
<>
|
||||
<div className="absolute z-20 w-full h-full transition-opacity opacity-0 pointer-events-none group-hover:opacity-100 bg-black/20"></div>
|
||||
<img src={URL.createObjectURL(theme.coverImage as Blob)} alt='Cover Image' className="absolute z-0 object-cover w-full h-full rounded" />
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between pt-4'>
|
||||
<div>
|
||||
<div className='pr-2 text-sm font-semibold'>Hide Name</div>
|
||||
<div className='pr-2 text-[11px]'>Useful when your cover image contains text</div>
|
||||
</div>
|
||||
<Switch state={theme.hideThemeName} onChange={value => setTheme({ ...theme, hideThemeName: value })} />
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* <div className='flex items-center justify-between'>
|
||||
<div>
|
||||
<div className='pr-2 text-sm font-semibold'>Custom Theme Colour</div>
|
||||
<div className='pr-2 text-[11px]'>Allow users to change the theme colour</div>
|
||||
</div>
|
||||
<Switch state={theme.CanChangeColour} onChange={value => setTheme({ ...theme, CanChangeColour: value })} />
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between pt-4'>
|
||||
<div>
|
||||
<div className='pr-2 text-sm font-semibold'>Custom Backgrounds</div>
|
||||
<div className='pr-2 text-[11px]'>Allow users to set image and video backgrounds</div>
|
||||
</div>
|
||||
<Switch state={theme.allowBackgrounds} onChange={value => setTheme({ ...theme, allowBackgrounds: value })} />
|
||||
</div>
|
||||
|
||||
<Divider /> */}
|
||||
|
||||
<div className='flex items-center justify-between'>
|
||||
<div>
|
||||
<div className='pr-2 text-sm font-semibold'>Force Theme</div>
|
||||
<div className='pr-2 text-[11px]'>Force users to use either dark or light mode</div>
|
||||
</div>
|
||||
|
||||
<Switch state={theme.forceDark == undefined ? false : true} onChange={value => setTheme({ ...theme, forceDark: value ? false : undefined })} />
|
||||
</div>
|
||||
|
||||
{ theme.forceDark != undefined &&
|
||||
<div className='flex items-center justify-between pt-4'>
|
||||
<div>
|
||||
<div className='pr-2 text-sm font-semibold'>Force {theme.forceDark ? 'Dark' : 'Light'} Mode</div>
|
||||
<div className='pr-2 text-[11px]'>Force users to use {theme.forceDark ? 'dark' : 'light'} mode</div>
|
||||
</div>
|
||||
<button className='flex items-center justify-center p-2 transition rounded-lg bg-zinc-100 dark:bg-zinc-700' onClick={() => setTheme({ ...theme, forceDark: !theme.forceDark })}>
|
||||
{theme.forceDark ? <MoonIcon className='w-6 h-6' /> : <SunIcon className='w-6 h-6' />}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<Divider />
|
||||
|
||||
<Accordion defaultOpened title='Default Theme Colour'>
|
||||
<div className='p-2 mt-2 bg-white rounded-lg w-fit dark:bg-zinc-900'>
|
||||
<ColorPicker
|
||||
width={278}
|
||||
disableDarkMode={true}
|
||||
hideInputs={true}
|
||||
value={theme.defaultColour}
|
||||
onChange={(color: string) => setTheme({ ...theme, defaultColour: color })} />
|
||||
</div>
|
||||
</Accordion>
|
||||
|
||||
<Divider />
|
||||
|
||||
<h2 className='pb-1 text-lg font-semibold'>Custom Images</h2>
|
||||
<p className='pb-3 text-sm'>Upload images to include them in your theme via CSS variables (gifs supported).</p>
|
||||
|
||||
{theme.CustomImages.map((image) => (
|
||||
<div key={image.id} className="flex items-center h-16 py-2 mb-4 bg-white rounded-lg shadow-lg dark:bg-zinc-900">
|
||||
<div className="flex-1 h-full ">
|
||||
<img src={URL.createObjectURL(image.blob)} alt={image.variableName} className="object-contain h-full rounded" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={image.variableName}
|
||||
onChange={(e) => handleImageVariableChange(image.id, e.target.value)}
|
||||
placeholder="CSS Variable Name"
|
||||
className="flex-grow flex-[3] w-full p-2 transition-all duration-300 rounded-lg focus:outline-none ring-0 focus:ring-1 ring-zinc-100 dark:ring-zinc-700 dark:bg-zinc-800/50 dark:text-white"
|
||||
/>
|
||||
<button onClick={() => handleRemoveImage(image.id)} className="p-2 ml-1 transition dark:text-white">
|
||||
<XMarkIcon height={20} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="relative flex justify-center w-full h-8 gap-1 overflow-hidden transition rounded-lg place-items-center bg-zinc-100 dark:bg-zinc-900">
|
||||
<PlusIcon height={18} />
|
||||
<span className='dark:text-white'>Add image</span>
|
||||
<input type="file" accept='image/*' onChange={handleImageUpload} className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" />
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Accordion defaultOpened title='Custom CSS'>
|
||||
<CodeEditor
|
||||
className='mt-2'
|
||||
height='800px'
|
||||
value={theme.CustomCSS}
|
||||
setValue={CodeUpdate} />
|
||||
</Accordion>
|
||||
|
||||
<Divider />
|
||||
|
||||
<button disabled={ theme.name === '' } onClick={saveTheme} className='w-full px-4 py-2 text-white transition bg-blue-500 rounded-lg dark:disabled:bg-zinc-700 disabled:bg-zinc-100 disabled:cursor-not-allowed dark:text-white'>
|
||||
Save theme
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Divider() {
|
||||
return <div className='w-full h-0.5 my-4 bg-zinc-200 dark:bg-zinc-700'></div>;
|
||||
}
|
||||
|
||||
export default ThemeCreator;
|
||||
Reference in New Issue
Block a user