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/"] } } }