feat: framer motion settings

This commit is contained in:
rift 2023-12-15 21:36:00 -06:00
parent 83f1083d75
commit 90a858d1e3
16 changed files with 461 additions and 3 deletions

7
framer-motion.d.ts vendored Normal file
View file

@ -0,0 +1,7 @@
import * as React from "preact/compat";
declare module "framer-motion" {
export interface AnimatePresenceProps {
children?: React.ReactNode;
}
}

View file

@ -10,6 +10,8 @@
"dependencies": { "dependencies": {
"@titaniumnetwork-dev/ultraviolet": "^2.0.0", "@titaniumnetwork-dev/ultraviolet": "^2.0.0",
"@tomphttp/bare-server-node": "^2.0.1", "@tomphttp/bare-server-node": "^2.0.1",
"classnames": "^2.3.2",
"framer-motion": "^10.16.16",
"i18next": "^23.7.9", "i18next": "^23.7.9",
"i18next-browser-languagedetector": "^7.2.0", "i18next-browser-languagedetector": "^7.2.0",
"million": "^2.6.4", "million": "^2.6.4",

42
pnpm-lock.yaml generated
View file

@ -11,6 +11,12 @@ dependencies:
'@tomphttp/bare-server-node': '@tomphttp/bare-server-node':
specifier: ^2.0.1 specifier: ^2.0.1
version: 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: i18next:
specifier: ^23.7.9 specifier: ^23.7.9
version: 23.7.10 version: 23.7.10
@ -359,6 +365,20 @@ packages:
'@babel/helper-validator-identifier': 7.22.20 '@babel/helper-validator-identifier': 7.22.20
to-fast-properties: 2.0.0 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: /@esbuild/android-arm64@0.19.9:
resolution: {integrity: sha512-q4cR+6ZD0938R19MyEW3jEsMzbb/1rulLXiNAJQADD/XYp7pT+rOS5JGxvpRW8dFDEfjW4wLgC/3FXIw4zYglQ==} resolution: {integrity: sha512-q4cR+6ZD0938R19MyEW3jEsMzbb/1rulLXiNAJQADD/XYp7pT+rOS5JGxvpRW8dFDEfjW4wLgC/3FXIw4zYglQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -1220,6 +1240,10 @@ packages:
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3
/classnames@2.3.2:
resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==}
dev: false
/cliui@8.0.1: /cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -1817,6 +1841,23 @@ packages:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
dev: true 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: /fs-extra@11.2.0:
resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==}
engines: {node: '>=14.14'} engines: {node: '>=14.14'}
@ -3252,7 +3293,6 @@ packages:
/tslib@2.6.2: /tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
dev: true
/tsutils@3.21.0(typescript@5.3.3): /tsutils@3.21.0(typescript@5.3.3):
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}

View file

@ -6,6 +6,7 @@ import { Home } from "./pages/Home";
import { NotFound } from "./pages/_404.jsx"; import { NotFound } from "./pages/_404.jsx";
import { DiscordPage } from "./pages/discord.jsx"; import { DiscordPage } from "./pages/discord.jsx";
import { ProxyFrame } from "./ProxyFrame.js"; import { ProxyFrame } from "./ProxyFrame.js";
import { Settings } from "./pages/Settings/index.js";
import "./style.css"; import "./style.css";
import "./themes/main.css"; import "./themes/main.css";
@ -18,6 +19,7 @@ export function App() {
<Route path="/" component={Home} /> <Route path="/" component={Home} />
<Route path="/discord" component={DiscordPage} /> <Route path="/discord" component={DiscordPage} />
<Route path="/proxyframe/:id" component={ProxyFrame} /> <Route path="/proxyframe/:id" component={ProxyFrame} />
<Route path="/settings" component={Settings} />
<Route default component={NotFound} /> <Route default component={NotFound} />
</Router> </Router>
</LocationProvider> </LocationProvider>

View file

@ -17,5 +17,13 @@
"sub": "Would you like to open this via proxy?", "sub": "Would you like to open this via proxy?",
"button1": "Open Normally", "button1": "Open Normally",
"button2": "Use Proxy" "button2": "Use Proxy"
},
"settings": {
"tabs": {
"proxy": "Proxy",
"tab": "Tab",
"custom": "Customization",
"misc": "Misc"
}
} }
} }

View file

@ -17,5 +17,13 @@
"sub": "プロキシ経由で開きますか?", "sub": "プロキシ経由で開きますか?",
"button1": "通常通り開く", "button1": "通常通り開く",
"button2": "プロキシを使用する" "button2": "プロキシを使用する"
},
"settings": {
"tabs": {
"proxy": "プロキシ",
"tab": "タブ",
"custom": "カスタマイズ",
"misc": "その他"
}
} }
} }

View file

@ -0,0 +1,19 @@
import { motion } from "framer-motion";
import { tabContentVariant, settingsPageVariant } from "./Variants";
const Customization = ({ id, active }) => (
<motion.div
role="tabpanel"
id={id}
className="tab-content"
variants={tabContentVariant}
animate={active ? "active" : "inactive"}
initial="inactive"
>
<motion.div variants={settingsPageVariant} className="content-card">
<h1>Customization</h1>
</motion.div>
</motion.div>
);
export default Customization;

View file

@ -0,0 +1,19 @@
import { motion } from "framer-motion";
import { tabContentVariant, settingsPageVariant } from "./Variants";
const Misc = ({ id, active }) => (
<motion.div
role="tabpanel"
id={id}
className="tab-content"
variants={tabContentVariant}
animate={active ? "active" : "inactive"}
initial="inactive"
>
<motion.div variants={settingsPageVariant} className="content-card">
<h1>Misc settings</h1>
</motion.div>
</motion.div>
);
export default Misc;

View file

@ -0,0 +1,19 @@
import { motion } from "framer-motion";
import { tabContentVariant, settingsPageVariant } from "./Variants";
const Proxy = ({ id, active }) => (
<motion.div
role="tabpanel"
id={id}
className="tab-content"
variants={tabContentVariant}
animate={active ? "active" : "inactive"}
initial="inactive"
>
<motion.div variants={settingsPageVariant} className="content-card">
<h1>Porxy</h1>
</motion.div>
</motion.div>
);
export default Proxy;

View file

@ -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 (
<div class="flex flex-col items-center">
<div className="container h-full w-full">
<div className="tabs-component">
<ul className="tab-links" role="tablist">
{tabs.map((tab, index) => (
<motion.li
key={tab.id}
className={cn("tab", { active: activeTabIndex === index })}
role="presentation"
variants={tabVariant}
animate={activeTabIndex === index ? "active" : "inactive"}
>
<a href={`#${tab.id}`} onClick={() => onTabClick(index)}>
{tab.icon}
<motion.span variants={tabTextVariant}>
{t(tab.title)}
</motion.span>
</a>
</motion.li>
))}
</ul>
{tabs.map((tab, index) => (
<tab.content
key={tab.id}
id={`${tab.id}-content`}
active={activeTabIndex === index}
/>
))}
</div>
</div>
</div>
);
};
export default TabComponent;

View file

@ -0,0 +1,19 @@
import { motion } from "framer-motion";
import { tabContentVariant, settingsPageVariant } from "./Variants";
const TabSettings = ({ id, active }) => (
<motion.div
role="tabpanel"
id={id}
className="tab-content"
variants={tabContentVariant}
animate={active ? "active" : "inactive"}
initial="inactive"
>
<motion.div variants={settingsPageVariant} className="content-card">
<h1>Tab settings</h1>
</motion.div>
</motion.div>
);
export default TabSettings;

View file

@ -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 };

View file

@ -0,0 +1,11 @@
import TabComponent from "./TabComponent";
import { HeaderRoute } from "../../components/HeaderRoute";
import tabs from "./tabs";
export function Settings() {
return (
<HeaderRoute>
<TabComponent tabs={tabs} />
</HeaderRoute>
);
}

View file

@ -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;
}

View file

@ -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: <AiOutlineLaptop />,
color: "#5d5dff",
content: Proxy
},
{
title: "settings.tabs.tab",
id: "tab",
icon: <GoBrowser />,
color: "#67bb67",
content: TabSettings
},
{
title: "settings.tabs.custom",
id: "custom",
icon: <FaPalette />,
color: "#63a7c7",
content: Customization
},
{
title: "settings.tabs.misc",
id: "misc",
icon: <FaGear />,
color: "#f56868",
content: Misc
}
];
export default tabs;

View file

@ -8,8 +8,6 @@
"jsxImportSource": "preact", "jsxImportSource": "preact",
"skipLibCheck": true, "skipLibCheck": true,
"paths": { "paths": {
"react": ["./node_modules/preact/compat/"],
"react-dom": ["./node_modules/preact/compat/"]
} }
} }
} }