feat: add assets
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m37s

This commit is contained in:
Hewston Fox
2026-03-21 02:56:15 +02:00
parent d620f06ab8
commit a804f3c280
49 changed files with 21548 additions and 97 deletions

View File

@@ -0,0 +1,11 @@
@layer base {
.rootLayout {
position: relative;
height: 100%;
width: 100%;
background-image: url("./assets/main-bg.svg");
background-size: auto 101%;
background-position: center;
}
}

View File

@@ -0,0 +1,23 @@
import type { ReactNode } from "react";
import Header from "./components/Header";
import Navigation from "./components/Navigation";
import classes from "./RootLayout.module.css";
type Props = {
hideControls?: boolean;
children?: ReactNode;
};
export default function RootLayout({ children, hideControls }: Props) {
return (
<div className={classes.rootLayout}>
{!hideControls && <Header />}
<main className="h-full overflow-y-auto overflow-x-hidden pt-[calc(var(--header-total)+16px)] pb-[calc(var(--navigation-total)+16px)]">
{children}
</main>
{!hideControls && <Navigation />}
</div>
);
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View File

@@ -11,7 +11,7 @@
bottom: 0;
left: 0;
z-index: 10;
max-width: 320px;
max-width: 500px;
margin: auto;
display: flex;
align-items: flex-end;
@@ -27,6 +27,7 @@
align-items: center;
justify-content: flex-start;
padding-bottom: var(--navigation-padding);
cursor: pointer;
}
.safeContent {
@@ -52,13 +53,10 @@
-webkit-text-stroke: 1px #331b01;
text-shadow: 0px 2px 2px #0000008c;
line-height: 1;
}
.labelInactive {
color: white;
}
.labelActive {
.active .label {
color: #ffbd42;
}
@@ -89,6 +87,7 @@
align-items: center;
justify-content: flex-start;
padding-right: 20px;
cursor: pointer;
}
.menuBarContent {

View File

@@ -1,4 +1,4 @@
import { useMatchRoute, useNavigate } from "@tanstack/react-router";
import { type AnyRoute, useMatchRoute, useNavigate } from "@tanstack/react-router";
import { useTranslation } from "react-i18next";
import { AnimatePresence } from "motion/react";
@@ -22,6 +22,15 @@ import menuIcon from "./assets/menu.svg";
import rouletteIcon from "./assets/roulette.svg";
import tasksIcon from "./assets/tasks.svg";
import earningsIcon from "./assets/earnings.svg";
import tg from "@/tg";
const ANIMATION_DURATION = 0.2;
const SPRING_ANIMATION = {
type: "spring",
stiffness: 400,
damping: 20,
duration: ANIMATION_DURATION,
} as const;
const BAR_HEIGHT = 64;
const ACTIVE_BAR_HEIGHT = 74;
@@ -52,40 +61,40 @@ type BarProps = {
function NavBar({ labelKey, icon, active, entranceDelay, onClick }: BarProps) {
const { t } = useTranslation();
const isInitial = useRef(true);
const isInitial = useRef(true);
useEffect(() => {
isInitial.current = false;
}, []);
const height = active ? ACTIVE_BAR_HEIGHT : BAR_HEIGHT;
return (
<GlassSurface
className={classes.bar}
initial={{ translateY: ACTIVE_BAR_HEIGHT + OFFSCREEN_BAR_OFFSET }}
animate={{
translateY: OFFSCREEN_BAR_OFFSET,
height: (active ? ACTIVE_BAR_HEIGHT : BAR_HEIGHT) + OFFSCREEN_BAR_OFFSET,
}}
transition={{
type: "spring" as const,
stiffness: 400,
damping: 20,
delay: isInitial.current ? entranceDelay : 0,
className={[classes.bar, active && classes.active]}
variants={{
hidden: {
translateY: ACTIVE_BAR_HEIGHT + OFFSCREEN_BAR_OFFSET,
height,
},
visible: {
translateY: OFFSCREEN_BAR_OFFSET,
height: height + OFFSCREEN_BAR_OFFSET,
transition: { ...SPRING_ANIMATION, delay: entranceDelay },
},
ready: {
translateY: OFFSCREEN_BAR_OFFSET,
height: height + OFFSCREEN_BAR_OFFSET,
transition: SPRING_ANIMATION,
},
}}
initial="hidden"
animate={isInitial.current ? "visible" : "ready"}
whileTap={{ scale: 0.95 }}
onClick={onClick}
>
<div className={classes.safeContent}>
<img src={icon} className={classes.icon} alt="" />
<span
className={
active
? `${classes.label} ${classes.labelActive}`
: `${classes.label} ${classes.labelInactive}`
}
>
{t(labelKey)}
</span>
<span className={classes.label}>{t(labelKey)}</span>
</div>
</GlassSurface>
);
@@ -94,38 +103,55 @@ function NavBar({ labelKey, icon, active, entranceDelay, onClick }: BarProps) {
type MenuBarProps = {
labelKey: string;
icon: string;
active: boolean;
delay: number;
onClick: () => void;
};
const MENU_SPRING = { type: "spring" as const, stiffness: 400, damping: 20 };
const MENU_BAR_WIDTH = 94;
const ACTIVE_MENU_BAR_WIDTH = 104;
const OFFSCREEN_MENU_BAR_OFFSET = 20;
function MenuBar({ labelKey, icon, delay, onClick }: MenuBarProps) {
function MenuBar({ labelKey, icon, delay, active, onClick }: MenuBarProps) {
const { t } = useTranslation();
const [isReady, setIsReady] = useState(false);
const width = active ? ACTIVE_MENU_BAR_WIDTH : MENU_BAR_WIDTH;
return (
<GlassSurface
className={classes.menuBar}
className={[classes.menuBar, active && classes.active]}
variants={{
hidden: { translateX: ACTIVE_BAR_HEIGHT + OFFSCREEN_BAR_OFFSET },
hidden: {
translateX: width + OFFSCREEN_MENU_BAR_OFFSET,
width,
},
visible: {
translateX: OFFSCREEN_BAR_OFFSET,
transition: { ...MENU_SPRING, delay: delay },
translateX: OFFSCREEN_MENU_BAR_OFFSET,
transition: { ...SPRING_ANIMATION, delay: delay },
width,
},
ready: {
translateX: OFFSCREEN_MENU_BAR_OFFSET,
transition: SPRING_ANIMATION,
width,
},
exit: {
translateX: ACTIVE_BAR_HEIGHT + OFFSCREEN_BAR_OFFSET + 10,
transition: { ...MENU_SPRING, delay: delay },
translateX: width + OFFSCREEN_MENU_BAR_OFFSET + 10,
transition: { ...SPRING_ANIMATION, delay: delay },
width,
},
}}
onAnimationComplete={(variant) => variant === "visible" && setIsReady(true)}
initial="hidden"
animate="visible"
animate={isReady ? "ready" : "visible"}
exit="exit"
whileTap={{ scale: 0.95 }}
onClick={onClick}
>
<div className={classes.menuBarContent}>
<img src={icon} className={classes.icon} alt="" />
<span className={`${classes.label} ${classes.labelInactive}`}>{t(labelKey)}</span>
<span className={classes.label}>{t(labelKey)}</span>
</div>
</GlassSurface>
);
@@ -134,12 +160,12 @@ function MenuBar({ labelKey, icon, delay, onClick }: MenuBarProps) {
export default function Navigation() {
const matchRoute = useMatchRoute();
const navigate = useNavigate();
const [menuOpen, setMenuOpen] = useState(false);
const [menuOpen, setMenuOpen] = useState<number>(0);
const navRef = useRef<HTMLDivElement>(null);
const handleOutsideClick = useCallback((e: MouseEvent) => {
if (navRef.current && !navRef.current.contains(e.target as Node)) {
setMenuOpen(false);
setMenuOpen(0);
}
}, []);
@@ -150,6 +176,16 @@ export default function Navigation() {
}
}, [menuOpen, handleOutsideClick]);
const navigateRoute = async (route: AnyRoute, wait = false) => {
tg.hapticFeedback.click();
const redirection = navigate({ to: route.to });
if (wait) {
await new Promise((resolve) => setTimeout(resolve, 1000 * ANIMATION_DURATION - 100));
await redirection;
}
setMenuOpen(0);
};
return (
<div ref={navRef}>
<div className={classes.menuOverlay}>
@@ -157,21 +193,19 @@ export default function Navigation() {
{menuOpen &&
MENU_ITEMS.map((item) => (
<MenuBar
key={item.key}
key={`${item.key} ${menuOpen}`}
active={Boolean(matchRoute({ to: item.route.to }))}
labelKey={item.key}
icon={item.icon}
delay={item.delay}
onClick={() => {
setMenuOpen(false);
navigate({ to: item.route.to });
}}
onClick={() => navigateRoute(item.route, true)}
/>
))}
</AnimatePresence>
</div>
<nav className={classes.navigation}>
{NAV_ITEMS.map((item, index) => {
const active = item.isMenu ? menuOpen : Boolean(matchRoute({ to: item.route!.to }));
const active = Boolean(item.isMenu ? menuOpen : matchRoute({ to: item.route!.to }));
return (
<NavBar
key={item.key}
@@ -181,13 +215,10 @@ export default function Navigation() {
entranceDelay={HORIZONTAL_ENTRANCE_DELAYS[index]}
onClick={
item.isMenu
? () => setMenuOpen((v) => !v)
? () => setMenuOpen((v) => (v ? 0 : Math.random()))
: active
? () => {}
: () => {
setMenuOpen(false);
navigate({ to: item.route!.to });
}
: () => navigateRoute(item.route!)
}
/>
);

View File

Before

Width:  |  Height:  |  Size: 269 KiB

After

Width:  |  Height:  |  Size: 269 KiB

View File

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

View File

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 200 KiB

View File

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 146 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -0,0 +1 @@
export { default } from "./RootLayout";