fix: building working, (lots of bugs)

This commit is contained in:
sethburkart123
2024-09-02 21:46:48 +10:00
parent 99a3166fa4
commit 2f08d6ee08
107 changed files with 1113 additions and 37 deletions
-50
View File
@@ -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);
-335
View File
@@ -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 couldnt find the theme youre looking for. Maybe... you could create it?</p>
</div>
)}
</div>
</div>
);
};
export default Store;
-341
View File
@@ -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;