diff --git a/framer-motion.d.ts b/framer-motion.d.ts
new file mode 100644
index 0000000..b62fed5
--- /dev/null
+++ b/framer-motion.d.ts
@@ -0,0 +1,7 @@
+import * as React from "preact/compat";
+
+declare module "framer-motion" {
+ export interface AnimatePresenceProps {
+ children?: React.ReactNode;
+ }
+}
diff --git a/package.json b/package.json
index 5b7c585..61cc777 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,8 @@
"dependencies": {
"@titaniumnetwork-dev/ultraviolet": "^2.0.0",
"@tomphttp/bare-server-node": "^2.0.1",
+ "classnames": "^2.3.2",
+ "framer-motion": "^10.16.16",
"i18next": "^23.7.9",
"i18next-browser-languagedetector": "^7.2.0",
"million": "^2.6.4",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5be8fc4..2e2fbf8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,6 +11,12 @@ dependencies:
'@tomphttp/bare-server-node':
specifier: ^2.0.1
version: 2.0.1
+ classnames:
+ specifier: ^2.3.2
+ version: 2.3.2
+ framer-motion:
+ specifier: ^10.16.16
+ version: 10.16.16(react@18.2.0)
i18next:
specifier: ^23.7.9
version: 23.7.10
@@ -359,6 +365,20 @@ packages:
'@babel/helper-validator-identifier': 7.22.20
to-fast-properties: 2.0.0
+ /@emotion/is-prop-valid@0.8.8:
+ resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==}
+ requiresBuild: true
+ dependencies:
+ '@emotion/memoize': 0.7.4
+ dev: false
+ optional: true
+
+ /@emotion/memoize@0.7.4:
+ resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==}
+ requiresBuild: true
+ dev: false
+ optional: true
+
/@esbuild/android-arm64@0.19.9:
resolution: {integrity: sha512-q4cR+6ZD0938R19MyEW3jEsMzbb/1rulLXiNAJQADD/XYp7pT+rOS5JGxvpRW8dFDEfjW4wLgC/3FXIw4zYglQ==}
engines: {node: '>=12'}
@@ -1220,6 +1240,10 @@ packages:
optionalDependencies:
fsevents: 2.3.3
+ /classnames@2.3.2:
+ resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==}
+ dev: false
+
/cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
@@ -1817,6 +1841,23 @@ packages:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
dev: true
+ /framer-motion@10.16.16(react@18.2.0):
+ resolution: {integrity: sha512-je6j91rd7NmUX7L1XHouwJ4v3R+SO4umso2LUcgOct3rHZ0PajZ80ETYZTajzEXEl9DlKyzjyt4AvGQ+lrebOw==}
+ peerDependencies:
+ react: ^18.0.0
+ react-dom: ^18.0.0
+ peerDependenciesMeta:
+ react:
+ optional: true
+ react-dom:
+ optional: true
+ dependencies:
+ react: 18.2.0
+ tslib: 2.6.2
+ optionalDependencies:
+ '@emotion/is-prop-valid': 0.8.8
+ dev: false
+
/fs-extra@11.2.0:
resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==}
engines: {node: '>=14.14'}
@@ -3252,7 +3293,6 @@ packages:
/tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
- dev: true
/tsutils@3.21.0(typescript@5.3.3):
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
diff --git a/src/index.tsx b/src/index.tsx
index fea262d..8a2bb2d 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -6,6 +6,7 @@ import { Home } from "./pages/Home";
import { NotFound } from "./pages/_404.jsx";
import { DiscordPage } from "./pages/discord.jsx";
import { ProxyFrame } from "./ProxyFrame.js";
+import { Settings } from "./pages/Settings/index.js";
import "./style.css";
import "./themes/main.css";
@@ -18,6 +19,7 @@ export function App() {
+
diff --git a/src/locales/en.json b/src/locales/en.json
index ff1ab71..e4a48ad 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -17,5 +17,13 @@
"sub": "Would you like to open this via proxy?",
"button1": "Open Normally",
"button2": "Use Proxy"
+ },
+ "settings": {
+ "tabs": {
+ "proxy": "Proxy",
+ "tab": "Tab",
+ "custom": "Customization",
+ "misc": "Misc"
+ }
}
}
diff --git a/src/locales/ja.json b/src/locales/ja.json
index 44c336e..5793763 100644
--- a/src/locales/ja.json
+++ b/src/locales/ja.json
@@ -17,5 +17,13 @@
"sub": "プロキシ経由で開きますか?",
"button1": "通常通り開く",
"button2": "プロキシを使用する"
+ },
+ "settings": {
+ "tabs": {
+ "proxy": "プロキシ",
+ "tab": "タブ",
+ "custom": "カスタマイズ",
+ "misc": "その他"
+ }
}
}
diff --git a/src/pages/Settings/Customization.tsx b/src/pages/Settings/Customization.tsx
new file mode 100644
index 0000000..fd061de
--- /dev/null
+++ b/src/pages/Settings/Customization.tsx
@@ -0,0 +1,19 @@
+import { motion } from "framer-motion";
+import { tabContentVariant, settingsPageVariant } from "./Variants";
+
+const Customization = ({ id, active }) => (
+
+
+ Customization
+
+
+);
+
+export default Customization;
diff --git a/src/pages/Settings/Misc.tsx b/src/pages/Settings/Misc.tsx
new file mode 100644
index 0000000..4529582
--- /dev/null
+++ b/src/pages/Settings/Misc.tsx
@@ -0,0 +1,19 @@
+import { motion } from "framer-motion";
+import { tabContentVariant, settingsPageVariant } from "./Variants";
+
+const Misc = ({ id, active }) => (
+
+
+ Misc settings
+
+
+);
+
+export default Misc;
diff --git a/src/pages/Settings/Proxy.tsx b/src/pages/Settings/Proxy.tsx
new file mode 100644
index 0000000..ee7f068
--- /dev/null
+++ b/src/pages/Settings/Proxy.tsx
@@ -0,0 +1,19 @@
+import { motion } from "framer-motion";
+import { tabContentVariant, settingsPageVariant } from "./Variants";
+
+const Proxy = ({ id, active }) => (
+
+
+ Porxy
+
+
+);
+
+export default Proxy;
diff --git a/src/pages/Settings/TabComponent.tsx b/src/pages/Settings/TabComponent.tsx
new file mode 100644
index 0000000..aeacd19
--- /dev/null
+++ b/src/pages/Settings/TabComponent.tsx
@@ -0,0 +1,106 @@
+import { useState, useEffect } from "preact/hooks";
+import cn from "classnames";
+import { motion } from "framer-motion";
+import "./styles.css";
+import { useTranslation } from "react-i18next";
+
+const tabVariant = {
+ active: {
+ width: "55%",
+ transition: {
+ type: "tween",
+ duration: 0.4
+ }
+ },
+ inactive: {
+ width: "15%",
+ transition: {
+ type: "tween",
+ duration: 0.4
+ }
+ }
+};
+
+const tabTextVariant = {
+ active: {
+ opacity: 1,
+ x: 0,
+ display: "block",
+ transition: {
+ type: "tween",
+ duration: 0.3,
+ delay: 0.3
+ }
+ },
+ inactive: {
+ opacity: 0,
+ x: -30,
+ transition: {
+ type: "tween",
+ duration: 0.3,
+ delay: 0.1
+ },
+ transitionEnd: { display: "none" }
+ }
+};
+
+const TabComponent = ({ tabs, defaultIndex = 0 }) => {
+ const [activeTabIndex, setActiveTabIndex] = useState(defaultIndex);
+
+ useEffect(() => {
+ document.documentElement.style.setProperty(
+ "--active-color",
+ tabs[activeTabIndex].color
+ );
+ }, [activeTabIndex, tabs]);
+
+ // Default to a tab based on the URL hash value
+ useEffect(() => {
+ const tabFromHash = tabs.findIndex(
+ (tab) => `#${tab.id}` === window.location.hash
+ );
+ setActiveTabIndex(tabFromHash !== -1 ? tabFromHash : defaultIndex);
+ }, [tabs, defaultIndex]);
+
+ const onTabClick = (index) => {
+ setActiveTabIndex(index);
+ };
+
+ const { t } = useTranslation()
+
+ return (
+
+
+
+
+ {tabs.map((tab, index) => (
+
+ ))}
+
+
+
+ );
+};
+
+export default TabComponent;
diff --git a/src/pages/Settings/TabSettings.tsx b/src/pages/Settings/TabSettings.tsx
new file mode 100644
index 0000000..bb5d31e
--- /dev/null
+++ b/src/pages/Settings/TabSettings.tsx
@@ -0,0 +1,19 @@
+import { motion } from "framer-motion";
+import { tabContentVariant, settingsPageVariant } from "./Variants";
+
+const TabSettings = ({ id, active }) => (
+
+
+ Tab settings
+
+
+);
+
+export default TabSettings;
diff --git a/src/pages/Settings/Variants.tsx b/src/pages/Settings/Variants.tsx
new file mode 100644
index 0000000..0a29d6a
--- /dev/null
+++ b/src/pages/Settings/Variants.tsx
@@ -0,0 +1,30 @@
+const tabContentVariant = {
+ active: {
+ display: "block",
+ transition: {
+ staggerChildren: 0.2
+ }
+ },
+ inactive: {
+ display: "none"
+ }
+};
+
+const settingsPageVariant = {
+ active: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ duration: 0.5
+ }
+ },
+ inactive: {
+ opacity: 0,
+ y: 10,
+ transition: {
+ duration: 0.5
+ }
+ }
+};
+
+export { settingsPageVariant, tabContentVariant };
diff --git a/src/pages/Settings/index.tsx b/src/pages/Settings/index.tsx
new file mode 100644
index 0000000..13b634f
--- /dev/null
+++ b/src/pages/Settings/index.tsx
@@ -0,0 +1,11 @@
+import TabComponent from "./TabComponent";
+import { HeaderRoute } from "../../components/HeaderRoute";
+import tabs from "./tabs";
+
+export function Settings() {
+ return (
+
+
+
+ );
+}
diff --git a/src/pages/Settings/styles.css b/src/pages/Settings/styles.css
new file mode 100644
index 0000000..68e761c
--- /dev/null
+++ b/src/pages/Settings/styles.css
@@ -0,0 +1,128 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ --white: #fff;
+ --black: #333;
+ --active-color: #f1f1f1;
+ --border-radius: 40px;
+}
+
+body {
+ -webkit-font-smoothing: antialiased;
+ font-family: Arial, Helvetica, sans-serif;
+ background: var(--active-color);
+ transition: background 1.5s ease;
+}
+
+img {
+ max-width: 100%;
+ vertical-align: middle;
+}
+
+.tabs-component {
+ width: 100%;
+ height: 100%;
+ margin: auto;
+ padding: 40px;
+ border-radius: var(--border-radius);
+}
+
+.tab-links {
+ padding: 0;
+ margin: 0 auto 20px;
+ list-style: none;
+ max-width: 400px;
+ display: flex;
+ justify-content: space-between;
+}
+
+.tab {
+ position: relative;
+}
+
+.tab a {
+ text-decoration: none;
+ color: var(--black);
+}
+
+.tab::before {
+ content: "";
+ width: 100%;
+ height: 100%;
+ opacity: 0.2;
+ position: absolute;
+ border-radius: var(--border-radius);
+ background: none;
+ transition: background 0.5s ease;
+}
+
+.tab svg {
+ height: 30px;
+ width: 30px;
+ min-width: 30px;
+ fill: var(--black);
+ transition: fill 0.5s ease;
+}
+
+.tab.active::before {
+ background: var(--active-color);
+}
+
+.tab span {
+ font-weight: 700;
+ margin-left: 10px;
+ transition: color 0.5s ease;
+}
+
+.tab.active span {
+ color: var(--active-color);
+}
+
+.tab.active svg {
+ fill: var(--active-color);
+}
+
+.tab a {
+ padding: 16px;
+ display: flex;
+ align-items: center;
+ font-size: 20px;
+ overflow: hidden;
+ position: relative;
+}
+
+.cards {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ margin-top: 40px;
+}
+
+.content-card {
+ width: 48%;
+ margin-bottom: 26px;
+}
+
+.content-card .info::after {
+ content: "";
+ display: block;
+ width: 100%;
+ height: 3px;
+ bottom: -5px;
+ background: var(--active-color);
+ opacity: 0.5;
+}
+
+.content-card img {
+ border-radius: 6px;
+}
+
+.content-card h3 {
+ margin: 0 0 5px;
+}
+
+.content-card .info {
+ padding: 10px 0;
+}
diff --git a/src/pages/Settings/tabs.tsx b/src/pages/Settings/tabs.tsx
new file mode 100644
index 0000000..59f786c
--- /dev/null
+++ b/src/pages/Settings/tabs.tsx
@@ -0,0 +1,42 @@
+import Proxy from "./Proxy";
+import TabSettings from "./TabSettings";
+import Misc from "./Misc";
+import Customization from "./Customization";
+
+import { GoBrowser } from "react-icons/go";
+import { AiOutlineLaptop } from "react-icons/ai";
+import { FaPalette } from "react-icons/fa";
+import { FaGear } from "react-icons/fa6";
+
+const tabs = [
+ {
+ title: "settings.tabs.proxy",
+ id: "proxy",
+ icon: ,
+ color: "#5d5dff",
+ content: Proxy
+ },
+ {
+ title: "settings.tabs.tab",
+ id: "tab",
+ icon: ,
+ color: "#67bb67",
+ content: TabSettings
+ },
+ {
+ title: "settings.tabs.custom",
+ id: "custom",
+ icon: ,
+ color: "#63a7c7",
+ content: Customization
+ },
+ {
+ title: "settings.tabs.misc",
+ id: "misc",
+ icon: ,
+ color: "#f56868",
+ content: Misc
+ }
+];
+
+export default tabs;
diff --git a/tsconfig.json b/tsconfig.json
index b207a94..6f88ae5 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -8,8 +8,6 @@
"jsxImportSource": "preact",
"skipLibCheck": true,
"paths": {
- "react": ["./node_modules/preact/compat/"],
- "react-dom": ["./node_modules/preact/compat/"]
}
}
}