feat: add settings menu
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m40s
All checks were successful
Deploy to VPS (dist) / deploy (push) Successful in 1m40s
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import type { ReactNode } from "react";
|
||||
import Header from "./components/Header";
|
||||
import Navigation from "./components/Navigation";
|
||||
import { LiftLayer } from "@components/lift";
|
||||
import classes from "./RootLayout.module.css";
|
||||
|
||||
type Props = {
|
||||
@@ -16,6 +17,7 @@ export default function RootLayout({ children, hideControls }: Props) {
|
||||
<main className={classes.main}>{children}</main>
|
||||
|
||||
{!hideControls && <Navigation />}
|
||||
<LiftLayer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
z-index: 20;
|
||||
|
||||
padding-top: var(--header-padding);
|
||||
padding-left: var(--safe-left);
|
||||
|
||||
@@ -8,12 +8,51 @@
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.left {
|
||||
}
|
||||
|
||||
.avatar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.avatarBorder {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(180deg, #eed074 0%, #a5602c 100%);
|
||||
border: 1px solid #401e08;
|
||||
box-shadow:
|
||||
4px 0px 1px -2px #00000040,
|
||||
0px 2px 0px 0px #ffefd1 inset,
|
||||
0px -2px 0px 0px #7a451c inset;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.avatarInner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(180deg, #9e6025 0%, #feeea1 100%);
|
||||
box-shadow:
|
||||
0px -1px 0px 0px #ffffffa6 inset,
|
||||
0px 1px 0px 0px #00000040 inset;
|
||||
padding: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatarImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
import classes from "./Profile.module.css";
|
||||
import { motion } from "motion/react";
|
||||
import { Liftable } from "@components/lift";
|
||||
import { useTelegramUser } from "@/tg";
|
||||
|
||||
export default function Profile() {
|
||||
const user = useTelegramUser();
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ scale: 0, x: "-100vw" }}
|
||||
animate={{ scale: 1, x: 0 }}
|
||||
transition={{ duration: 0.3, type: "spring", bounce: 0.2 }}
|
||||
className={classes.profile}
|
||||
>
|
||||
<div className={classes.left} />
|
||||
<div className={classes.right}>
|
||||
<div className={classes.rightTop} />
|
||||
<div className={classes.rightBottom} />
|
||||
</div>
|
||||
</motion.div>
|
||||
<Liftable always>
|
||||
<motion.div
|
||||
initial={{ scale: 0, x: "-100vw" }}
|
||||
animate={{ scale: 1, x: 0 }}
|
||||
transition={{ duration: 0.3, type: "spring", bounce: 0.2 }}
|
||||
className={classes.profile}
|
||||
>
|
||||
<div className={classes.avatar}>
|
||||
<div className={classes.avatarBorder}>
|
||||
<div className={classes.avatarInner}>
|
||||
{user?.photoUrl && <img className={classes.avatarImage} src={user.photoUrl} alt="" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.right}>
|
||||
<div className={classes.rightTop} />
|
||||
<div className={classes.rightBottom} />
|
||||
</div>
|
||||
</motion.div>
|
||||
</Liftable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,49 +1,114 @@
|
||||
import { useState } from "react";
|
||||
import { lazy, Suspense } from "react";
|
||||
import { useMachine } from "@xstate/react";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
import tg from "@/tg";
|
||||
import { settingsMachine } from "./settingsMachine";
|
||||
import SettingsModal, { type SettingsModalId } from "./components/SettingsModal";
|
||||
|
||||
const FAQModal = lazy(() => import("./components/FAQModal"));
|
||||
const AccountModal = lazy(() => import("./components/AccountModal"));
|
||||
const SupportModal = lazy(() => import("./components/SupportModal"));
|
||||
const TransactionsHistoryModal = lazy(() => import("./components/TransactionsHistoryModal"));
|
||||
const LanguageModal = lazy(() => import("./components/LanguageModal"));
|
||||
|
||||
import classes from "./Settings.module.css";
|
||||
import { usePlaySound } from "@/audio";
|
||||
|
||||
export default function Settings() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const play = usePlaySound();
|
||||
|
||||
const [state, send] = useMachine(settingsMachine);
|
||||
|
||||
const isOpen = state.value !== "closed";
|
||||
|
||||
const toggle = () => {
|
||||
play("click");
|
||||
tg.hapticFeedback.click();
|
||||
setIsOpen((prev) => !prev);
|
||||
if (isOpen) {
|
||||
send({ type: "CLOSE" });
|
||||
} else {
|
||||
send({ type: "OPEN_SETTINGS" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
send({ type: "CLOSE" });
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
send({ type: "BACK" });
|
||||
};
|
||||
|
||||
const handleNavigate = (modal: SettingsModalId) => {
|
||||
const eventMap = {
|
||||
faq: "OPEN_FAQ",
|
||||
account: "OPEN_ACCOUNT",
|
||||
support: "OPEN_SUPPORT",
|
||||
transactionsHistory: "OPEN_TRANSACTIONS_HISTORY",
|
||||
language: "OPEN_LANGUAGE",
|
||||
} satisfies Record<SettingsModalId, string>;
|
||||
send({ type: eventMap[modal] as Parameters<typeof send>[0]["type"] });
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
initial={{ scale: 0, x: "50%", y: "-50%" }}
|
||||
animate={{ scale: 1, x: 0, y: 0 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
transition={{ duration: 0.1, type: "spring" }}
|
||||
className={classes.settings}
|
||||
onClick={toggle}
|
||||
>
|
||||
<div className={classes.bars}>
|
||||
{[0, 1, 2].map((i) => (
|
||||
<motion.span
|
||||
key={i}
|
||||
className={classes.bar}
|
||||
initial={{ scale: 0 }}
|
||||
animate={
|
||||
isOpen
|
||||
? i === 0
|
||||
? { scale: 1, rotate: 45, y: 12 }
|
||||
: i === 1
|
||||
? { scale: 1, opacity: 0, scaleX: 0 }
|
||||
: { scale: 1, rotate: -45, y: -12 }
|
||||
: { scale: 1, rotate: 0, y: 0, opacity: 1, scaleX: 1 }
|
||||
}
|
||||
transition={{
|
||||
scale: { delay: 0.05 * i + 0.1, duration: 0.15, type: "spring" },
|
||||
default: { duration: 0.1 },
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</motion.button>
|
||||
<>
|
||||
<motion.button
|
||||
initial={{ scale: 0, x: "50%", y: "-50%" }}
|
||||
animate={{ scale: 1, x: 0, y: 0 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
transition={{ duration: 0.1, type: "spring" }}
|
||||
className={classes.settings}
|
||||
onClick={toggle}
|
||||
>
|
||||
<div className={classes.bars}>
|
||||
{[0, 1, 2].map((i) => (
|
||||
<motion.span
|
||||
key={i}
|
||||
className={classes.bar}
|
||||
initial={{ scale: 0 }}
|
||||
animate={
|
||||
isOpen
|
||||
? i === 0
|
||||
? { scale: 1, rotate: 45, y: 12 }
|
||||
: i === 1
|
||||
? { scale: 1, opacity: 0, scaleX: 0 }
|
||||
: { scale: 1, rotate: -45, y: -12 }
|
||||
: { scale: 1, rotate: 0, y: 0, opacity: 1, scaleX: 1 }
|
||||
}
|
||||
transition={{
|
||||
scale: { delay: 0.05 * i + 0.1, duration: 0.15, type: "spring" },
|
||||
default: { duration: 0.1 },
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</motion.button>
|
||||
|
||||
<SettingsModal
|
||||
open={state.value === "settings"}
|
||||
onClose={handleClose}
|
||||
onNavigate={handleNavigate}
|
||||
/>
|
||||
|
||||
<Suspense>
|
||||
<FAQModal open={state.value === "faq"} onClose={handleClose} />
|
||||
|
||||
<AccountModal open={state.value === "account"} onClose={handleClose} />
|
||||
|
||||
<SupportModal open={state.value === "support"} onClose={handleClose} />
|
||||
|
||||
<TransactionsHistoryModal
|
||||
open={state.value === "transactionsHistory"}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
|
||||
<LanguageModal
|
||||
open={state.value === "language"}
|
||||
onClose={handleClose}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Modal from "@components/modals/Modal";
|
||||
import ContentSurface from "@components/surface/ContentSurface";
|
||||
import LightSurface from "@components/surface/LightSurface";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const USER_DATA_MOCK = {
|
||||
id: 123456789,
|
||||
registrationDate: "2023-01-01",
|
||||
paymentBalance: 1000,
|
||||
withdrawalBalance: 500,
|
||||
};
|
||||
|
||||
export default function AccountModal({ open, onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal className="w-full max-w-67.5" open={open} onClose={onClose}>
|
||||
<ContentSurface className="w-full rounded-full">
|
||||
<LightSurface className="w-full rounded-full p-2.5 font-bold text-lg">
|
||||
{t("accountInfo.yourId")} - {USER_DATA_MOCK.id}
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
<ContentSurface className="w-full rounded-full">
|
||||
<LightSurface className="w-full rounded-full p-2.5 font-bold text-lg">
|
||||
{t("accountInfo.registrationDate")} - {USER_DATA_MOCK.registrationDate}
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
<ContentSurface className="w-full rounded-full">
|
||||
<LightSurface className="w-full rounded-full p-2.5 font-bold text-lg">
|
||||
{t("accountInfo.paymentBalance")} - {USER_DATA_MOCK.paymentBalance}
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
<ContentSurface className="w-full rounded-full">
|
||||
<LightSurface className="w-full rounded-full p-2.5 font-bold text-lg">
|
||||
{t("accountInfo.withdrawalBalance")} - {USER_DATA_MOCK.withdrawalBalance}
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./AccountModal";
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Modal from "@components/modals/Modal";
|
||||
import ContentSurface from "@components/surface/ContentSurface";
|
||||
import LightSurface from "@components/surface/LightSurface";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function FAQModal({ open, onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal className="w-full max-w-67.5" open={open} onClose={onClose} title={t("settings.faq")}>
|
||||
<ContentSurface className="w-full rounded-4xl">
|
||||
<LightSurface className="w-full rounded-4xl p-2.5 font-bold text-lg text-center">
|
||||
<p>{t("faq.1q")}</p>
|
||||
<p className="text-[#83552E]">{t("faq.1a")}</p>
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
<ContentSurface className="w-full rounded-4xl">
|
||||
<LightSurface className="w-full rounded-4xl p-2.5 font-bold text-lg text-center">
|
||||
<p>{t("faq.1q")}</p>
|
||||
<p className="text-[#83552E]">{t("faq.1a")}</p>
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
<ContentSurface className="w-full rounded-4xl">
|
||||
<LightSurface className="w-full rounded-4xl p-2.5 font-bold text-lg text-center">
|
||||
<p>{t("faq.1q")}</p>
|
||||
<p className="text-[#83552E]">{t("faq.1a")}</p>
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
<ContentSurface className="w-full rounded-4xl">
|
||||
<LightSurface className="w-full rounded-4xl p-2.5 font-bold text-lg text-center">
|
||||
<p>{t("faq.1q")}</p>
|
||||
<p className="text-[#83552E]">{t("faq.1a")}</p>
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./FAQModal";
|
||||
@@ -0,0 +1,48 @@
|
||||
@layer base {
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.contentSurface {
|
||||
border-radius: 9999px;
|
||||
width: 100%;
|
||||
|
||||
&.active > * {
|
||||
background-color: #f8eb86;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
all: unset;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.langIcon {
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
border-radius: 50%;
|
||||
background: #ccc;
|
||||
flex-shrink: 0;
|
||||
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.itemActive {
|
||||
color: #4b2c13;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
import Modal from "@components/modals/Modal";
|
||||
import ContentSurface from "@components/surface/ContentSurface";
|
||||
import DarkSurface from "@components/surface/DarkSurface";
|
||||
import { useLanguages } from "@/i18n/useLanguages";
|
||||
import { usePlaySound } from "@/audio";
|
||||
import tg from "@/tg";
|
||||
|
||||
import classes from "./LanguageModal.module.css";
|
||||
import LightSurface from "@components/surface/LightSurface";
|
||||
import clsx from "clsx";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
export default function LanguageModal({ open, onClose, onBack }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { languages, current, setLanguage } = useLanguages();
|
||||
const play = usePlaySound();
|
||||
|
||||
const handleSelect = (key: string) => {
|
||||
play("click");
|
||||
tg.hapticFeedback.click();
|
||||
setLanguage(key);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
play("click");
|
||||
tg.hapticFeedback.click();
|
||||
onBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal className="w-full" open={open} onClose={onClose}>
|
||||
<div className={classes.grid}>
|
||||
{languages.map((lang) => (
|
||||
<ContentSurface
|
||||
key={lang.key}
|
||||
className={clsx(classes.contentSurface, lang.key === current.key && classes.active)}
|
||||
>
|
||||
<LightSurface className="rounded-full">
|
||||
<motion.button
|
||||
className={classes.item}
|
||||
onClick={() => handleSelect(lang.key)}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<div className={classes.langIcon}>
|
||||
<img src={lang.image} alt={lang.label} />
|
||||
</div>
|
||||
<span>{lang.label}</span>
|
||||
</motion.button>
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
))}
|
||||
|
||||
<ContentSurface className={classes.contentSurface}>
|
||||
<DarkSurface className="rounded-full h-full">
|
||||
<motion.button className={classes.item} onClick={handleBack} whileTap={{ scale: 0.95 }}>
|
||||
<span>{t("settings.back")}</span>
|
||||
</motion.button>
|
||||
</DarkSurface>
|
||||
</ContentSurface>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./LanguageModal";
|
||||
@@ -0,0 +1,51 @@
|
||||
@layer base {
|
||||
.items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.contentSurface {
|
||||
border-radius: 9999px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.lightSurface {
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.item {
|
||||
all: unset;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
padding: 10px 12px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.itemRow {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.langIcon {
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
border-radius: 50%;
|
||||
background: #ccc;
|
||||
flex-shrink: 0;
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
import Modal from "@components/modals/Modal";
|
||||
import ContentSurface from "@components/surface/ContentSurface";
|
||||
import LightSurface from "@components/surface/LightSurface";
|
||||
import SwitchInput from "@components/form/SwitchInput";
|
||||
import { useAudioSettings, usePlaySound } from "@/audio";
|
||||
import tg from "@/tg";
|
||||
|
||||
import classes from "./SettingsModal.module.css";
|
||||
import { useLanguages } from "@/i18n/useLanguages";
|
||||
|
||||
export type SettingsModalId = "faq" | "account" | "support" | "transactionsHistory" | "language";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onNavigate: (modal: SettingsModalId) => void;
|
||||
liftIds?: string[];
|
||||
};
|
||||
|
||||
export default function SettingsModal({ open, onClose, onNavigate, liftIds }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { current } = useLanguages();
|
||||
const { isEnabled, setIsEnabled } = useAudioSettings();
|
||||
const play = usePlaySound();
|
||||
|
||||
const handleNavigate = (modal: SettingsModalId) => {
|
||||
play("click");
|
||||
tg.hapticFeedback.click();
|
||||
onNavigate(modal);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal className="w-full max-w-67.5" open={open} onClose={onClose} liftIds={liftIds}>
|
||||
<div className={classes.items}>
|
||||
<ContentSurface className={classes.contentSurface}>
|
||||
<LightSurface className={classes.lightSurface}>
|
||||
<motion.button
|
||||
className={classes.item}
|
||||
onClick={() => handleNavigate("faq")}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
{t("settings.faq")}
|
||||
</motion.button>
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
|
||||
<ContentSurface className={classes.contentSurface}>
|
||||
<LightSurface className={classes.lightSurface}>
|
||||
<motion.button
|
||||
className={classes.item}
|
||||
onClick={() => handleNavigate("account")}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
{t("settings.accountInfo")}
|
||||
</motion.button>
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
|
||||
<ContentSurface className={classes.contentSurface}>
|
||||
<LightSurface className={classes.lightSurface}>
|
||||
<motion.button
|
||||
className={classes.item}
|
||||
onClick={() => handleNavigate("support")}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
{t("settings.support")}
|
||||
</motion.button>
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
|
||||
<ContentSurface className={classes.contentSurface}>
|
||||
<LightSurface className={classes.lightSurface}>
|
||||
<motion.button
|
||||
className={classes.item}
|
||||
onClick={() => handleNavigate("transactionsHistory")}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
{t("settings.transactionHistory")}
|
||||
</motion.button>
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
|
||||
<ContentSurface className={classes.contentSurface}>
|
||||
<LightSurface className={classes.lightSurface}>
|
||||
<div className={classes.itemRow}>
|
||||
<span>{t("settings.sound")}</span>
|
||||
<SwitchInput
|
||||
value={isEnabled}
|
||||
onChange={(value) => {
|
||||
setIsEnabled(value);
|
||||
if (value) play("click", { force: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
|
||||
<ContentSurface className={classes.contentSurface}>
|
||||
<LightSurface className={classes.lightSurface}>
|
||||
<motion.button
|
||||
className={classes.itemRow}
|
||||
onClick={() => handleNavigate("language")}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<span>{t("settings.language")}</span>
|
||||
<div className={classes.langIcon}>
|
||||
<img src={current.image} alt="lang" />
|
||||
</div>
|
||||
</motion.button>
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from "./SettingsModal";
|
||||
export type { SettingsModalId } from "./SettingsModal";
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Modal from "@components/modals/Modal";
|
||||
import ContentSurface from "@components/surface/ContentSurface";
|
||||
import LightSurface from "@components/surface/LightSurface";
|
||||
import Button from "@components/atoms/Button";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function SupportModal({ open, onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="w-full max-w-67.5 flex flex-col items-center gap-2"
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title={t("support.title")}
|
||||
>
|
||||
<ContentSurface className="w-full rounded-4xl">
|
||||
<LightSurface className="rounded-4xl py-1 px-2 font-bold text-lg text-center">
|
||||
{t("support.text")}
|
||||
</LightSurface>
|
||||
</ContentSurface>
|
||||
<Button variant="blue" className="w-11/12">
|
||||
{t("support.action")}
|
||||
</Button>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./SupportModal";
|
||||
@@ -0,0 +1,6 @@
|
||||
@layer base {
|
||||
.placeholder {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Modal from "@components/modals/Modal";
|
||||
import ContentSurface from "@components/surface/ContentSurface";
|
||||
import LightSurface from "@components/surface/LightSurface";
|
||||
|
||||
import DarkSurface from "@components/surface/DarkSurface";
|
||||
import HoneyIcon from "@components/icons/HoneyIcon";
|
||||
import MoneyIcon from "@components/icons/MoneyIcon";
|
||||
import Pagination from "@components/atoms/Pagination";
|
||||
import { useState } from "react";
|
||||
|
||||
const MOCK_TRANSACTIONS: {
|
||||
id: number;
|
||||
amount: number;
|
||||
date: string;
|
||||
currency: "honey" | "money";
|
||||
kind: "withdrawal" | "deposit" | "greeting" | "referral";
|
||||
}[] = [
|
||||
{ id: 1, amount: 500, date: "2026-03-20", currency: "honey", kind: "deposit" },
|
||||
{ id: 2, amount: -150, date: "2026-03-19", currency: "money", kind: "withdrawal" },
|
||||
{ id: 3, amount: 100, date: "2026-03-18", currency: "honey", kind: "greeting" },
|
||||
{ id: 4, amount: 250, date: "2026-03-17", currency: "honey", kind: "referral" },
|
||||
{ id: 7, amount: 75, date: "2026-03-14", currency: "money", kind: "referral" },
|
||||
{ id: 8, amount: -200, date: "2026-03-13", currency: "honey", kind: "withdrawal" },
|
||||
];
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const numberFormat = new Intl.NumberFormat("en-US", { signDisplay: "exceptZero" });
|
||||
const dateFormat = new Intl.DateTimeFormat("fi-FI");
|
||||
|
||||
export default function TransactionsHistoryModal({ open, onClose }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const totalPages = 6;
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="w-full flex flex-col items-center gap-2"
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title={t("transactionHistory.title")}
|
||||
>
|
||||
<span className="text-center font-bold text-xl text-[#FBE6BE]">
|
||||
{t("transactionHistory.yourTransactions")}
|
||||
</span>
|
||||
<div className="w-full flex justify-between items-center text-center font-bold text-lg">
|
||||
<ContentSurface className="rounded-full w-25">
|
||||
<DarkSurface className="rounded-full p-1.5 text-white">
|
||||
{t("transactionHistory.sum")}
|
||||
</DarkSurface>
|
||||
</ContentSurface>
|
||||
<ContentSurface className="rounded-full w-25">
|
||||
<DarkSurface className="rounded-full p-1.5 text-white">
|
||||
{t("transactionHistory.date")}
|
||||
</DarkSurface>
|
||||
</ContentSurface>
|
||||
</div>
|
||||
|
||||
{MOCK_TRANSACTIONS.map(({ id, kind, date, currency, amount }) => (
|
||||
<ContentSurface key={id} className="rounded-2xl w-full">
|
||||
<LightSurface className="rounded-t-2xl rounded-b-sm flex justify-between p-2">
|
||||
<div className="flex gap-1">
|
||||
<span>{numberFormat.format(amount)}</span>
|
||||
{currency === "honey" ? <HoneyIcon /> : <MoneyIcon />}
|
||||
</div>
|
||||
<div>{dateFormat.format(new Date(date))}</div>
|
||||
</LightSurface>
|
||||
<DarkSurface className="rounded-b-2xl rounded-t-sm p-2">
|
||||
{t("transactionHistory.operationType")}: {t(`operationType.${kind}`)}
|
||||
</DarkSurface>
|
||||
</ContentSurface>
|
||||
))}
|
||||
|
||||
<Pagination value={page} total={totalPages} onChange={setPage} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./TransactionsHistoryModal";
|
||||
@@ -0,0 +1,65 @@
|
||||
import { setup } from "xstate";
|
||||
|
||||
export const settingsMachine = setup({
|
||||
types: {
|
||||
events: {} as
|
||||
| { type: "OPEN_SETTINGS" }
|
||||
| { type: "OPEN_FAQ" }
|
||||
| { type: "OPEN_ACCOUNT" }
|
||||
| { type: "OPEN_SUPPORT" }
|
||||
| { type: "OPEN_TRANSACTIONS_HISTORY" }
|
||||
| { type: "OPEN_LANGUAGE" }
|
||||
| { type: "CLOSE" }
|
||||
| { type: "BACK" },
|
||||
},
|
||||
}).createMachine({
|
||||
id: "settings",
|
||||
initial: "closed",
|
||||
states: {
|
||||
closed: {
|
||||
on: {
|
||||
OPEN_SETTINGS: { target: "settings" },
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
on: {
|
||||
CLOSE: { target: "closed" },
|
||||
OPEN_FAQ: { target: "faq" },
|
||||
OPEN_ACCOUNT: { target: "account" },
|
||||
OPEN_SUPPORT: { target: "support" },
|
||||
OPEN_TRANSACTIONS_HISTORY: { target: "transactionsHistory" },
|
||||
OPEN_LANGUAGE: { target: "language" },
|
||||
},
|
||||
},
|
||||
faq: {
|
||||
on: {
|
||||
CLOSE: { target: "closed" },
|
||||
BACK: { target: "settings" },
|
||||
},
|
||||
},
|
||||
account: {
|
||||
on: {
|
||||
CLOSE: { target: "closed" },
|
||||
BACK: { target: "settings" },
|
||||
},
|
||||
},
|
||||
support: {
|
||||
on: {
|
||||
CLOSE: { target: "closed" },
|
||||
BACK: { target: "settings" },
|
||||
},
|
||||
},
|
||||
transactionsHistory: {
|
||||
on: {
|
||||
CLOSE: { target: "closed" },
|
||||
BACK: { target: "settings" },
|
||||
},
|
||||
},
|
||||
language: {
|
||||
on: {
|
||||
CLOSE: { target: "closed" },
|
||||
BACK: { target: "settings" },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -23,6 +23,7 @@ import rouletteIcon from "./assets/roulette.svg";
|
||||
import tasksIcon from "./assets/tasks.svg";
|
||||
import earningsIcon from "./assets/earnings.svg";
|
||||
import tg, { useTelegramViewportValue } from "@/tg";
|
||||
import { usePlaySound } from "@/audio";
|
||||
|
||||
const ANIMATION_DURATION = 0.2;
|
||||
const SPRING_ANIMATION = {
|
||||
@@ -34,7 +35,6 @@ const SPRING_ANIMATION = {
|
||||
|
||||
const BAR_HEIGHT = 64;
|
||||
const ACTIVE_BAR_HEIGHT = 74;
|
||||
const OFFSCREEN_BAR_OFFSET = 20;
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ key: "nav.shop", route: ShopRoute, icon: shopIcon },
|
||||
@@ -162,6 +162,7 @@ function MenuBar({ labelKey, icon, delay, active, onClick }: MenuBarProps) {
|
||||
}
|
||||
|
||||
export default function Navigation() {
|
||||
const play = usePlaySound();
|
||||
const matchRoute = useMatchRoute();
|
||||
const navigate = useNavigate();
|
||||
const [menuOpen, setMenuOpen] = useState<number>(0);
|
||||
@@ -181,6 +182,7 @@ export default function Navigation() {
|
||||
}, [menuOpen, handleOutsideClick]);
|
||||
|
||||
const navigateRoute = async (route: AnyRoute, wait = false) => {
|
||||
play("click");
|
||||
tg.hapticFeedback.click();
|
||||
const redirection = navigate({ to: route.to });
|
||||
if (wait) {
|
||||
@@ -220,6 +222,7 @@ export default function Navigation() {
|
||||
onClick={
|
||||
item.isMenu
|
||||
? () => {
|
||||
play("click");
|
||||
tg.hapticFeedback.click();
|
||||
setMenuOpen((v) => (v ? 0 : Math.random()));
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import TextInput from "@components/form/TextInput";
|
||||
import NumberInput from "@components/form/NumberInput";
|
||||
import TextAreaInput from "@components/form/TextAreaInput";
|
||||
import ActionModal from "@components/modals/ActionModal";
|
||||
import Pagination from "@components/atoms/Pagination";
|
||||
|
||||
const TABS = [
|
||||
{ key: "tab1", title: "Tab 1" },
|
||||
@@ -24,9 +25,11 @@ const TABS = [
|
||||
|
||||
export default function GameRoute() {
|
||||
const [activeTab, setActiveTab] = useState<string | null>(TABS[0].key);
|
||||
const [page, setPage] = useState(1);
|
||||
const [progressValue, setProgressValue] = useState(0);
|
||||
const [switchValue, setSwitchValue] = useState<boolean | null>(false);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [buttonsDisabled, setButtonsDisabled] = useState(false);
|
||||
|
||||
return (
|
||||
<SectionSurface className="relative flex flex-col gap-4 w-full">
|
||||
@@ -42,12 +45,23 @@ export default function GameRoute() {
|
||||
<DataSurface>100$</DataSurface>
|
||||
</DarkSurface>
|
||||
</ContentSurface>
|
||||
<Button onClick={() => setModalOpen(true)}>Open modal</Button>
|
||||
<Button>Click me</Button>
|
||||
<Button variant="green">Click me</Button>
|
||||
<Button variant="red">Click me</Button>
|
||||
<Button variant="yellow">Click me</Button>
|
||||
<Button variant="blue">Click me</Button>
|
||||
<Button onClick={() => setButtonsDisabled((v) => !v)}>Toggle disabled</Button>
|
||||
<Button disabled={buttonsDisabled} onClick={() => setModalOpen(true)}>
|
||||
Open modal
|
||||
</Button>
|
||||
<Button disabled={buttonsDisabled}>Click me</Button>
|
||||
<Button disabled={buttonsDisabled} variant="green">
|
||||
Click me
|
||||
</Button>
|
||||
<Button disabled={buttonsDisabled} variant="red">
|
||||
Click me
|
||||
</Button>
|
||||
<Button disabled={buttonsDisabled} variant="yellow">
|
||||
Click me
|
||||
</Button>
|
||||
<Button disabled={buttonsDisabled} variant="blue">
|
||||
Click me
|
||||
</Button>
|
||||
<ContentSurface className="rounded-full">
|
||||
<Progress value={progressValue} max={10000} variant="green" />
|
||||
</ContentSurface>
|
||||
@@ -66,6 +80,8 @@ export default function GameRoute() {
|
||||
<NumberInput error prefix="$" value={progressValue} onChange={setProgressValue} />
|
||||
<TextAreaInput placeholder="Text Area Input" rows={3} />
|
||||
<TextAreaInput placeholder="Text Area Error" error rows={3} />
|
||||
<Pagination value={page} total={10} onChange={setPage} />
|
||||
<Pagination value={page} total={10} onChange={setPage} variant="blue" />
|
||||
<ActionModal
|
||||
open={modalOpen}
|
||||
description="Are you sure you want to proceed with this action?"
|
||||
|
||||
Reference in New Issue
Block a user