major updates to popup dev, manifest fix
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,75 @@
|
|||||||
|
// App.tsx
|
||||||
|
import { useState } from 'react';
|
||||||
|
import TabbedContainer from './components/TabbedContainer';
|
||||||
|
import Settings from './pages/Settings';
|
||||||
|
import logo from './assets/betterseqta-dark-full.png';
|
||||||
|
import logoDark from './assets/betterseqta-light-full.png';
|
||||||
|
import Shortcuts from './pages/Shortcuts';
|
||||||
|
import About from './pages/About';
|
||||||
|
|
||||||
|
export interface SettingsState {
|
||||||
|
notificationCollector: boolean;
|
||||||
|
lessonAlerts: boolean;
|
||||||
|
animatedBackground: boolean;
|
||||||
|
animatedBackgroundSpeed: boolean;
|
||||||
|
customThemeColor: string;
|
||||||
|
betterSEQTAPlus: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const App: React.FC = () => {
|
||||||
|
const [settingsState, setSettingsState] = useState<SettingsState>({
|
||||||
|
notificationCollector: false,
|
||||||
|
lessonAlerts: false,
|
||||||
|
animatedBackground: false,
|
||||||
|
animatedBackgroundSpeed: false,
|
||||||
|
customThemeColor: "#db6969",
|
||||||
|
betterSEQTAPlus: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handler for Switches
|
||||||
|
const switchChange = (key: string, isOn: boolean) => {
|
||||||
|
setSettingsState({
|
||||||
|
...settingsState,
|
||||||
|
[key]: isOn,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handler for ColorPicker
|
||||||
|
const colorChange = (color: string) => {
|
||||||
|
setSettingsState({
|
||||||
|
...settingsState,
|
||||||
|
customThemeColor: color,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
title: 'Settings',
|
||||||
|
content: <Settings settingsState={settingsState} switchChange={switchChange} colorChange={colorChange} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Shortcuts',
|
||||||
|
content: <Shortcuts />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'About',
|
||||||
|
content: <About />
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center w-screen h-screen pt-4 overflow-hidden" style={{ background: settingsState.customThemeColor }}>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-[24rem] shadow-2xl gap-2 bg-white rounded-xl h-4/6 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" />
|
||||||
|
</div>
|
||||||
|
<TabbedContainer themeColor={settingsState.customThemeColor} tabs={tabs} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,45 @@
|
|||||||
|
// TODO: Create types for ColorPicker
|
||||||
|
// @ts-expect-error No typescript declarations available
|
||||||
|
import ColorPicker from 'react-best-gradient-color-picker';
|
||||||
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
|
||||||
|
interface ColorPickerProps {
|
||||||
|
color: string;
|
||||||
|
onChange: (color: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Picker = ({ color, onChange }: ColorPickerProps) => {
|
||||||
|
const [showPicker, setShowPicker] = useState<boolean>(false);
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent): void => {
|
||||||
|
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||||
|
setShowPicker(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (showPicker) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [showPicker]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowPicker(!showPicker)}
|
||||||
|
style={{ background: color }}
|
||||||
|
className="w-16 h-8 rounded-md"
|
||||||
|
></button>
|
||||||
|
{showPicker && (
|
||||||
|
<div ref={ref} className="fixed top-0 left-0 z-50 p-4 bg-white border rounded-lg shadow-lg border-zinc-00">
|
||||||
|
<ColorPicker value={color} onChange={onChange} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Picker;
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
.switch[data-ison="true"] {
|
.switch[data-ison="true"] {
|
||||||
justify-content: end;
|
|
||||||
background-color: #30D259;
|
background-color: #30D259;
|
||||||
}
|
}
|
||||||
@@ -1,31 +1,30 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import "./Switch.css";
|
import "./Switch.css";
|
||||||
|
|
||||||
interface SwitchProps {
|
interface SwitchProps {
|
||||||
onChange: (isOn: boolean) => void;
|
onChange: (isOn: boolean) => void;
|
||||||
|
state: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Switch(props: SwitchProps) {
|
export default function Switch(props: SwitchProps) {
|
||||||
const [isOn, setIsOn] = useState(false);
|
|
||||||
|
|
||||||
const toggleSwitch = () => {
|
const toggleSwitch = () => {
|
||||||
const newIsOn = !isOn;
|
const newIsOn = !props.state;
|
||||||
setIsOn(newIsOn);
|
|
||||||
props.onChange(newIsOn);
|
props.onChange(newIsOn);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex w-14 p-1 cursor-pointer rounded-full dark:bg-[#38373D] bg-[#DDDDDD] switch"
|
className="flex w-14 p-1 cursor-pointer rounded-full dark:bg-[#38373D] bg-[#DDDDDD] switch"
|
||||||
data-isOn={isOn}
|
data-isOn={props.state}
|
||||||
onClick={toggleSwitch}
|
onClick={toggleSwitch}
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|
||||||
className="w-6 h-6 bg-white dark:bg-[#FEFEFE] rounded-full drop-shadow-md"
|
className="w-6 h-6 bg-white dark:bg-[#FEFEFE] rounded-full drop-shadow-md"
|
||||||
layout
|
initial={{ x: props.state ? 0 : 0 }}
|
||||||
|
animate={{ x: props.state ? 24 : 0 }}
|
||||||
transition={spring}
|
transition={spring}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
interface Tab {
|
||||||
|
title: string;
|
||||||
|
content: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TabbedContainerProps {
|
||||||
|
tabs: Tab[];
|
||||||
|
themeColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TabbedContainer: React.FC<TabbedContainerProps> = ({ tabs, themeColor }) => {
|
||||||
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
const [hoveredTab, setHoveredTab] = useState<number | null>(null);
|
||||||
|
const [tabWidth, setTabWidth] = useState(0);
|
||||||
|
const [position, setPosition] = useState(0);
|
||||||
|
const positionRef = useRef(position);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newPosition = -activeTab * 100;
|
||||||
|
setPosition(newPosition);
|
||||||
|
positionRef.current = newPosition;
|
||||||
|
}, [activeTab]);
|
||||||
|
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
|
const springTransition = { type: 'spring', stiffness: 250, damping: 25 };
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
// @ts-expect-error for some reason its giving an error in TS but it works...
|
||||||
|
const width = containerRef.current.getBoundingClientRect().width;
|
||||||
|
setTabWidth(width / tabs.length);
|
||||||
|
}
|
||||||
|
}, [tabs.length]);
|
||||||
|
|
||||||
|
const calcXPos = (index: number | null) => {
|
||||||
|
if (index !== null) {
|
||||||
|
return tabWidth * index;
|
||||||
|
}
|
||||||
|
return tabWidth * activeTab;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full px-4 overflow-y-scroll overflow-x-clip">
|
||||||
|
<div ref={containerRef} className="sticky top-0 z-10 text-[0.875rem] mb-2 pb-2 bg-white">
|
||||||
|
<div className="relative flex">
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-0 left-0 z-0 h-full rounded-full opacity-40"
|
||||||
|
style={{ width: `${tabWidth}px`, background: themeColor }}
|
||||||
|
initial={false}
|
||||||
|
animate={{ x: calcXPos(hoveredTab) }}
|
||||||
|
transition={springTransition}
|
||||||
|
/>
|
||||||
|
{tabs.map((tab, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
className="relative z-10 flex-1 px-4 py-2"
|
||||||
|
onClick={() => setActiveTab(index)}
|
||||||
|
onMouseEnter={() => setHoveredTab(index)}
|
||||||
|
onMouseLeave={() => setHoveredTab(null)}
|
||||||
|
>
|
||||||
|
{tab.title}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<motion.div
|
||||||
|
initial={false}
|
||||||
|
animate={{ x: `${position}%` }}
|
||||||
|
transition={springTransition}
|
||||||
|
>
|
||||||
|
<div className="absolute flex w-full" style={{ left: `${-position}%` }}>
|
||||||
|
{tabs.map((tab, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`w-full ${activeTab === index ? '' : 'hidden'}`}
|
||||||
|
>
|
||||||
|
{tab.content}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TabbedContainer;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
const About: React.FC = () => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col overflow-y-scroll divide-y divide-zinc-100">
|
||||||
|
<h2>About</h2>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default About;
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import Switch from '../components/Switch';
|
||||||
|
import ColorPicker from '../components/ColorPicker';
|
||||||
|
import { SettingsState } from '../App';
|
||||||
|
|
||||||
|
interface ISetting {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
modifyElement: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsProps {
|
||||||
|
settingsState: SettingsState;
|
||||||
|
switchChange: (key: string, isOn: boolean) => void;
|
||||||
|
colorChange: (color: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Settings: React.FC<SettingsProps> = ({ settingsState, switchChange, colorChange }) => {
|
||||||
|
const settings: ISetting[] = [
|
||||||
|
{
|
||||||
|
title: "Notification Collector",
|
||||||
|
description: "Uncaps the 9+ limit for notifications, showing the real number.",
|
||||||
|
modifyElement: <Switch state={settingsState.notificationCollector} onChange={(isOn: boolean) => switchChange('notificationCollector', isOn)} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Lesson Alerts",
|
||||||
|
description: "Sends a native browser notification ~5 minutes prior to lessons.",
|
||||||
|
modifyElement: <Switch state={settingsState.lessonAlerts} onChange={(isOn: boolean) => switchChange('lessonAlerts', isOn)} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Animated Background",
|
||||||
|
description: "Adds an animated background to BetterSEQTA. (May impact battery life)",
|
||||||
|
modifyElement: <Switch state={settingsState.animatedBackground} onChange={(isOn: boolean) => switchChange('animatedBackground', isOn)} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Animated Background Speed",
|
||||||
|
description: "Controls the speed of the animated background.",
|
||||||
|
modifyElement: <Switch state={settingsState.animatedBackgroundSpeed} onChange={(isOn: boolean) => switchChange('animatedBackgroundSpeed', isOn)} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Custom Theme Colour",
|
||||||
|
description: "Customise the overall theme colour of SEQTA Learn.",
|
||||||
|
modifyElement: <ColorPicker color={settingsState.customThemeColor} onChange={(color: string) => colorChange(color)} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "BetterSEQTA+",
|
||||||
|
description: "Unlocks premium features.",
|
||||||
|
modifyElement: <Switch state={settingsState.betterSEQTAPlus} onChange={(isOn: boolean) => switchChange('betterSEQTAPlus', isOn)} />
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col overflow-y-scroll divide-y divide-zinc-100">
|
||||||
|
{settings.map((setting, index) => (
|
||||||
|
<div className="flex items-center justify-between px-4 py-3" key={index}>
|
||||||
|
<div className="pr-4">
|
||||||
|
<h2 className="text-sm font-bold">{setting.title}</h2>
|
||||||
|
<p className="text-xs">{setting.description}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{setting.modifyElement}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Settings;
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import Switch from "../components/Switch";
|
||||||
|
|
||||||
|
export default function Shortcuts() {
|
||||||
|
const [shortcutState, setShortcutState] = useState({
|
||||||
|
youtube: false,
|
||||||
|
outlook: false,
|
||||||
|
office: false,
|
||||||
|
spotify: false,
|
||||||
|
google: false,
|
||||||
|
duckduckgo: false,
|
||||||
|
coolmathgames: false,
|
||||||
|
sace: false,
|
||||||
|
googlescholar: false,
|
||||||
|
gmail: false,
|
||||||
|
netflix: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handler for Switches
|
||||||
|
const switchChange = (key: string, isOn: boolean) => {
|
||||||
|
setShortcutState({
|
||||||
|
...shortcutState,
|
||||||
|
[key]: isOn,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const DefaultShortcuts = [
|
||||||
|
{
|
||||||
|
title: "YouTube",
|
||||||
|
link: "https://youtube.com",
|
||||||
|
modifyElement: <Switch state={shortcutState.youtube} onChange={(isOn: boolean) => switchChange('youtube', isOn)} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Outlook",
|
||||||
|
link: "https://outlook.office.com/mail/inbox",
|
||||||
|
modifyElement: <Switch state={shortcutState.outlook} onChange={(isOn: boolean) => switchChange('outlook', isOn)} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Office",
|
||||||
|
link: "https://www.office.com/",
|
||||||
|
modifyElement: <Switch state={shortcutState.office} onChange={(isOn: boolean) => switchChange('office', isOn)} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Spotify",
|
||||||
|
link: "https://www.spotify.com/",
|
||||||
|
modifyElement: <Switch state={shortcutState.spotify} onChange={(isOn: boolean) => switchChange('spotify', isOn)} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Google",
|
||||||
|
link: "https://www.google.com/",
|
||||||
|
modifyElement: <Switch state={shortcutState.google} onChange={(isOn: boolean) => switchChange('google', isOn)} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "DuckDuckGo",
|
||||||
|
link: "https://duckduckgo.com/",
|
||||||
|
modifyElement: <Switch state={shortcutState.duckduckgo} onChange={(isOn: boolean) => switchChange('duckduckgo', isOn)} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Cool Math Games",
|
||||||
|
link: "https://www.coolmathgames.com/",
|
||||||
|
modifyElement: <Switch state={shortcutState.coolmathgames} onChange={(isOn: boolean) => switchChange('coolmathgames', isOn)} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "SACE",
|
||||||
|
link: "https://www.sace.sa.edu.au/",
|
||||||
|
modifyElement: <Switch state={shortcutState.sace} onChange={(isOn: boolean) => switchChange('sace', isOn)} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Google Scholar",
|
||||||
|
link: "https://scholar.google.com/",
|
||||||
|
modifyElement: <Switch state={shortcutState.googlescholar} onChange={(isOn: boolean) => switchChange('googlescholar', isOn)} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Gmail",
|
||||||
|
link: "https://mail.google.com/",
|
||||||
|
modifyElement: <Switch state={shortcutState.gmail} onChange={(isOn: boolean) => switchChange('gmail', isOn)} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Netflix",
|
||||||
|
link: "https://www.netflix.com/",
|
||||||
|
modifyElement: <Switch state={shortcutState.netflix} onChange={(isOn: boolean) => switchChange('netflix', isOn)} />
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col divide-y divide-zinc-100">
|
||||||
|
{DefaultShortcuts.map((shortcut, index) => (
|
||||||
|
<div className="flex items-center justify-between px-4 py-3" key={index}>
|
||||||
|
{shortcut.title}
|
||||||
|
{shortcut.modifyElement}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
darkMode: "class",
|
||||||
|
theme: {
|
||||||
|
fontSize: {
|
||||||
|
'xs': '.65rem',
|
||||||
|
'sm': '.775rem',
|
||||||
|
'base': '0.65rem', // 16px
|
||||||
|
'md': '0.65rem', // 16px
|
||||||
|
'lg': '1rem', // 18px
|
||||||
|
'xl': '1.25rem', // 20px
|
||||||
|
'2xl': '1.5rem', // 24px
|
||||||
|
'3xl': '1.875rem', // 30px
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
|
||||||
@@ -5,7 +5,7 @@ import react from '@vitejs/plugin-react'
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
build: {
|
build: {
|
||||||
outDir: '../../public/popup-dist',
|
//outDir: '../../public/popup-dist',
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
assetFileNames: 'client/rsc/[ext]/[name][extname]',
|
assetFileNames: 'client/rsc/[ext]/[name][extname]',
|
||||||
@@ -20,8 +20,7 @@
|
|||||||
"permissions": ["tabs", "notifications", "storage"],
|
"permissions": ["tabs", "notifications", "storage"],
|
||||||
"host_permissions": ["https://newsapi.org/", "*://*/*"],
|
"host_permissions": ["https://newsapi.org/", "*://*/*"],
|
||||||
"background": {
|
"background": {
|
||||||
"content_scripts": "background.js",
|
"service_worker": "background.js"
|
||||||
"persistent": false
|
|
||||||
},
|
},
|
||||||
"optional_permissions": ["declarativeContent"],
|
"optional_permissions": ["declarativeContent"],
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
import './App.css'
|
|
||||||
import Switch from './components/Switch'
|
|
||||||
import logo from './assets/betterseqta-dark-full.png'
|
|
||||||
import logoDark from './assets/betterseqta-light-full.png'
|
|
||||||
import ColorPicker from './components/ColorPicker'
|
|
||||||
|
|
||||||
const switchChange = (isOn: boolean) => {
|
|
||||||
console.log(isOn)
|
|
||||||
}
|
|
||||||
|
|
||||||
const settings = [
|
|
||||||
{
|
|
||||||
title: "Notification Collector",
|
|
||||||
description: "Uncaps the 9+ limit for notifications, showing the real number.",
|
|
||||||
modifyElement: <Switch onChange={switchChange} />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Lesson Alerts",
|
|
||||||
description: "Sends a native browser notification ~5 minutes prior to lessons.",
|
|
||||||
modifyElement: <Switch onChange={switchChange} />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Animated Background",
|
|
||||||
description: "Adds an animated background to BetterSEQTA. (May impact battery life)",
|
|
||||||
modifyElement: <Switch onChange={switchChange} />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Animated Background Speed",
|
|
||||||
description: "Controls the speed of the animated background.",
|
|
||||||
modifyElement: <Switch onChange={switchChange} />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Custom Theme Colour",
|
|
||||||
description: "Customise the overall theme colour of SEQTA Learn.",
|
|
||||||
modifyElement: <ColorPicker />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "BetterSEQTA+",
|
|
||||||
description: "Unlocks premium features.",
|
|
||||||
modifyElement: <Switch onChange={switchChange} />
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-2 dark:bg-zinc-800">
|
|
||||||
<div className="grid border-b border-b-zinc-200 place-items-center h-1/2">
|
|
||||||
<img src={logo} className="w-4/5 dark:hidden" />
|
|
||||||
<img src={logoDark} className="hidden w-4/5 dark:block" />
|
|
||||||
</div>
|
|
||||||
{settings.map((setting, index) => (
|
|
||||||
<div className="flex justify-between px-4 place-items-center" key={index}>
|
|
||||||
<div className="dark:text-white">
|
|
||||||
<h2 className="text-sm font-bold">{setting.title}</h2>
|
|
||||||
<p className="text-xs">{setting.description}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{setting.modifyElement}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
// TODO: Create types for ColorPicker
|
|
||||||
// @ts-expect-error No typescript declarations available
|
|
||||||
import ColorPicker from 'react-best-gradient-color-picker';
|
|
||||||
import { useState, useRef, useEffect } from 'react';
|
|
||||||
|
|
||||||
const Picker = (): JSX.Element => {
|
|
||||||
const [color, setColor] = useState<string>('rgba(255,20,255,1)');
|
|
||||||
const [showPicker, setShowPicker] = useState<boolean>(false);
|
|
||||||
const pickerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent): void => {
|
|
||||||
if (pickerRef.current && !pickerRef.current.contains(event.target as Node)) {
|
|
||||||
setShowPicker(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
};
|
|
||||||
}, [pickerRef]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative" ref={pickerRef}>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowPicker(!showPicker)}
|
|
||||||
style={{
|
|
||||||
'background': color
|
|
||||||
}}
|
|
||||||
className="w-16 h-8 rounded-md"
|
|
||||||
></button>
|
|
||||||
<div className="absolute top-0 right-0 z-10 p-2 bg-white border rounded-lg shadow-2xl border-zinc-200">
|
|
||||||
{ showPicker &&
|
|
||||||
<ColorPicker value={color} onChange={setColor} />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Picker;
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
export default {
|
|
||||||
content: [
|
|
||||||
"./index.html",
|
|
||||||
"./src/**/*.{js,ts,jsx,tsx}",
|
|
||||||
],
|
|
||||||
darkMode: "class",
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
"background-primary-dark": "",
|
|
||||||
"background-primary-light": "",
|
|
||||||
"background-secondary-dark": "",
|
|
||||||
"background-secondary-light": "",
|
|
||||||
"forground-primary-dark": "",
|
|
||||||
"forground-primary-light": "",
|
|
||||||
"forground-secondary-dark": "",
|
|
||||||
"forground-secondary-light": ""
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
}
|
|
||||||
|
|
||||||