diff --git a/interface/src/App.css b/interface/src/App.css new file mode 100644 index 00000000..e69de29b diff --git a/interface/src/App.tsx b/interface/src/App.tsx new file mode 100644 index 00000000..ea173fd1 --- /dev/null +++ b/interface/src/App.tsx @@ -0,0 +1,72 @@ +// 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({ + 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: + }, + { + title: 'Shortcuts', + content: + }, + { + title: 'About', + content: + } + ]; + + {/*
*/} + return ( +
+
+ + +
+ +
+ ); +}; + +export default App; \ No newline at end of file diff --git a/interface/src/assets/betterseqta-dark-full.png b/interface/src/assets/betterseqta-dark-full.png new file mode 100644 index 00000000..f2c77f49 Binary files /dev/null and b/interface/src/assets/betterseqta-dark-full.png differ diff --git a/interface/src/assets/betterseqta-light-full.png b/interface/src/assets/betterseqta-light-full.png new file mode 100644 index 00000000..6bd12477 Binary files /dev/null and b/interface/src/assets/betterseqta-light-full.png differ diff --git a/interface/src/assets/react.svg b/interface/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/interface/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/interface/src/components/ColorPicker.tsx b/interface/src/components/ColorPicker.tsx new file mode 100644 index 00000000..84bd298d --- /dev/null +++ b/interface/src/components/ColorPicker.tsx @@ -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(false); + const ref = useRef(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 ( +
+ + {showPicker && ( +
+ +
+ )} +
+ ); +}; + +export default Picker; diff --git a/interface/src/components/Slider.css b/interface/src/components/Slider.css new file mode 100644 index 00000000..ee7b1c0b --- /dev/null +++ b/interface/src/components/Slider.css @@ -0,0 +1,21 @@ +.range-slider{ + margin: 20px; + appearance: none; + outline: none; + width: 150px; + height: 3px; + border-radius: 5px; + background-color: #ccc; +} + +.range-slide::-webkit-slider-runnable-track{ + background-color: #4BD663 !important; +} +.range-slider::-webkit-slider-thumb { + background: #fafafa; + appearance: none; + box-shadow: 1px 2px 26px 1px #bdbdbd; + width: 15px; + height: 15px; + border-radius: 50%; +} \ No newline at end of file diff --git a/interface/src/components/Slider.tsx b/interface/src/components/Slider.tsx new file mode 100644 index 00000000..664d357d --- /dev/null +++ b/interface/src/components/Slider.tsx @@ -0,0 +1,35 @@ +import React, { useState } from 'react'; +import "./Slider.css"; + +interface Slider { + onValueChange: (value: number) => void; +} + +const Slider: React.FC = ({ onValueChange }) => { + const [sliderValue, setSliderValue] = useState(0); + + const handleInputChange = (event: React.ChangeEvent) => { + const value = parseInt(event.target.value, 10); + setSliderValue(value); + }; + + const handleMouseUp = () => { + onValueChange(sliderValue); + }; + + return ( +
+ +
+ ); +}; + +export default Slider; \ No newline at end of file diff --git a/interface/src/components/Switch.css b/interface/src/components/Switch.css new file mode 100644 index 00000000..d7e84449 --- /dev/null +++ b/interface/src/components/Switch.css @@ -0,0 +1,3 @@ +.switch[data-ison="true"] { + background-color: #30D259; +} \ No newline at end of file diff --git a/interface/src/components/Switch.tsx b/interface/src/components/Switch.tsx new file mode 100644 index 00000000..62dacfec --- /dev/null +++ b/interface/src/components/Switch.tsx @@ -0,0 +1,36 @@ +import { motion } from "framer-motion"; +import "./Switch.css"; + +interface SwitchProps { + onChange: (isOn: boolean) => void; + state: boolean; +} + +export default function Switch(props: SwitchProps) { + const toggleSwitch = () => { + const newIsOn = !props.state; + props.onChange(newIsOn); + }; + + return ( +
+ +
+ ); +} + +const spring = { + type: "spring", + stiffness: 700, + damping: 30 +}; \ No newline at end of file diff --git a/interface/src/components/TabbedContainer.tsx b/interface/src/components/TabbedContainer.tsx new file mode 100644 index 00000000..91df24dd --- /dev/null +++ b/interface/src/components/TabbedContainer.tsx @@ -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 = ({ tabs, themeColor }) => { + const [activeTab, setActiveTab] = useState(0); + const [hoveredTab, setHoveredTab] = useState(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 ( +
+
+
+ + {tabs.map((tab, index) => ( + + ))} +
+
+
+ +
+ {tabs.map((tab, index) => ( +
+ {tab.content} +
+ ))} +
+
+
+
+ ); +}; + +export default TabbedContainer; diff --git a/interface/src/index.css b/interface/src/index.css new file mode 100644 index 00000000..b2d789e1 --- /dev/null +++ b/interface/src/index.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} \ No newline at end of file diff --git a/interface/src/main.tsx b/interface/src/main.tsx new file mode 100644 index 00000000..be55245f --- /dev/null +++ b/interface/src/main.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './index.css' + +const root = ReactDOM.createRoot(document.getElementById('ExtensionPopup')!); + +root.render( + + + , +) diff --git a/interface/src/pages/About.tsx b/interface/src/pages/About.tsx new file mode 100644 index 00000000..ffeca441 --- /dev/null +++ b/interface/src/pages/About.tsx @@ -0,0 +1,20 @@ +const About: React.FC = () => { + + return ( +
+
+

About

+

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!

+

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 + Github. +

+
+
+

Credits

+

Nulkem for the original extension, OG-RandomTechChannel, Crazypersonalph, and the current maintainer SethBurkart123

+
+
+ ); +}; + +export default About; diff --git a/interface/src/pages/Settings.tsx b/interface/src/pages/Settings.tsx new file mode 100644 index 00000000..71a13851 --- /dev/null +++ b/interface/src/pages/Settings.tsx @@ -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 = ({ settingsState, switchChange, colorChange }) => { + const settings: ISetting[] = [ + { + title: "Notification Collector", + description: "Uncaps the 9+ limit for notifications, showing the real number.", + modifyElement: switchChange('notificationCollector', isOn)} /> + }, + { + title: "Lesson Alerts", + description: "Sends a native browser notification ~5 minutes prior to lessons.", + modifyElement: switchChange('lessonAlerts', isOn)} /> + }, + { + title: "Animated Background", + description: "Adds an animated background to BetterSEQTA. (May impact battery life)", + modifyElement: switchChange('animatedBackground', isOn)} /> + }, + { + title: "Animated Background Speed", + description: "Controls the speed of the animated background.", + modifyElement: switchChange('animatedBackgroundSpeed', isOn)} /> + }, + { + title: "Custom Theme Colour", + description: "Customise the overall theme colour of SEQTA Learn.", + modifyElement: colorChange(color)} /> + }, + { + title: "BetterSEQTA+", + description: "Unlocks premium features.", + modifyElement: switchChange('betterSEQTAPlus', isOn)} /> + } + ]; + + return ( +
+ {settings.map((setting, index) => ( +
+
+

{setting.title}

+

{setting.description}

+
+
+ {setting.modifyElement} +
+
+ ))} +
+ ); +}; + +export default Settings; diff --git a/interface/src/pages/Shortcuts.tsx b/interface/src/pages/Shortcuts.tsx new file mode 100644 index 00000000..7400df6a --- /dev/null +++ b/interface/src/pages/Shortcuts.tsx @@ -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: switchChange('youtube', isOn)} /> + }, + { + title: "Outlook", + link: "https://outlook.office.com/mail/inbox", + modifyElement: switchChange('outlook', isOn)} /> + }, + { + title: "Office", + link: "https://www.office.com/", + modifyElement: switchChange('office', isOn)} /> + }, + { + title: "Spotify", + link: "https://www.spotify.com/", + modifyElement: switchChange('spotify', isOn)} /> + }, + { + title: "Google", + link: "https://www.google.com/", + modifyElement: switchChange('google', isOn)} /> + }, + { + title: "DuckDuckGo", + link: "https://duckduckgo.com/", + modifyElement: switchChange('duckduckgo', isOn)} /> + }, + { + title: "Cool Math Games", + link: "https://www.coolmathgames.com/", + modifyElement: switchChange('coolmathgames', isOn)} /> + }, + { + title: "SACE", + link: "https://www.sace.sa.edu.au/", + modifyElement: switchChange('sace', isOn)} /> + }, + { + title: "Google Scholar", + link: "https://scholar.google.com/", + modifyElement: switchChange('googlescholar', isOn)} /> + }, + { + title: "Gmail", + link: "https://mail.google.com/", + modifyElement: switchChange('gmail', isOn)} /> + }, + { + title: "Netflix", + link: "https://www.netflix.com/", + modifyElement: switchChange('netflix', isOn)} /> + } + ]; + + return ( +
+ {DefaultShortcuts.map((shortcut, index) => ( +
+ {shortcut.title} + {shortcut.modifyElement} +
+ ))} +
+ ); +} diff --git a/interface/src/vite-env.d.ts b/interface/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/interface/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/inject/iframe.css b/src/inject/iframe.css index 3675acb6..cb4516ae 100644 --- a/src/inject/iframe.css +++ b/src/inject/iframe.css @@ -51,6 +51,10 @@ table th { background-color: transparent; } +::-webkit-scrollbar-thumb { + border-radius: 10rem !important; +} + ::-webkit-scrollbar-corner { background: none; } diff --git a/src/inject/injected.css b/src/inject/injected.css index 5d943129..6622755a 100644 --- a/src/inject/injected.css +++ b/src/inject/injected.css @@ -1,28 +1,3 @@ -/* // This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . */ -/* // This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . */ - @import url("https://fonts.googleapis.com/css?family=Rubik:300,400,500,600"); @import "./injected/popup.css"; @@ -368,9 +343,6 @@ ol:has(.MessageList__avatar___2wxyb svg) { -webkit-box-shadow: 0px 5px 16px 6px rgba(0, 0, 0, 0.2) !important; box-shadow: 0px 0px 4px 2px rgba(0, 0, 0, 0.2) !important; } -#main .timetablepage { - padding-top: 10px; -} #main .timetablepage .quickbar { border: none;