From e3088b7c470813fe3f12e62c13f96e903afb8b2c Mon Sep 17 00:00:00 2001 From: Hewston Fox Date: Sun, 22 Mar 2026 13:25:49 +0200 Subject: [PATCH] fix: profile lifting --- public/locales/de.json | 14 +- public/locales/en.json | 14 +- public/locales/es.json | 14 +- public/locales/fr.json | 14 +- public/locales/id.json | 14 +- public/locales/it.json | 14 +- public/locales/nl.json | 14 +- public/locales/pl.json | 14 +- public/locales/pt.json | 14 +- public/locales/ru.json | 14 +- public/locales/tr.json | 14 +- src/components/lift/LiftContext.tsx | 39 ++++++ src/components/lift/LiftLayer.tsx | 18 ++- src/components/lift/Liftable.tsx | 124 +++++++++++------- src/components/modals/Modal/Modal.tsx | 13 +- src/i18n/resources.d.ts | 19 +++ src/routes/-/RootLayout/RootLayout.module.css | 1 + .../components/Profile/Profile.module.css | 9 -- .../Header/components/Profile/Profile.tsx | 14 +- .../components/Settings/Settings.module.css | 1 - 20 files changed, 280 insertions(+), 112 deletions(-) diff --git a/public/locales/de.json b/public/locales/de.json index 959e03b..d531ed1 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -12,6 +12,11 @@ "off": "aus", "on": "an" }, + "faq": { + "1a": "Antwort", + "1q": "Frage", + "title": "FAQ" + }, "nav": { "apiary": "Bienenhaus", "cashdesk": "Kasse", @@ -31,10 +36,6 @@ "pagination": { "of": "von" }, - "support": { - "action": "Support kontaktieren", - "text": "Wenn Sie Fragen zum Spiel haben, wenden Sie sich bitte an unseren Support." - }, "settings": { "accountInfo": "Kontoinformationen", "back": "Zurück", @@ -44,6 +45,11 @@ "support": "Support", "transactionHistory": "Transaktionsverlauf" }, + "support": { + "action": "Support kontaktieren", + "text": "Wenn Sie Fragen zum Spiel haben, wenden Sie sich bitte an unseren Support.", + "title": "Support" + }, "transactionHistory": { "date": "Datum", "operationType": "Vorgangsart", diff --git a/public/locales/en.json b/public/locales/en.json index 1cc61cd..1b25a23 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -12,6 +12,11 @@ "off": "off", "on": "on" }, + "faq": { + "1a": "Answer", + "1q": "Question", + "title": "FAQ" + }, "nav": { "apiary": "Apiary", "cashdesk": "Cashdesk", @@ -31,10 +36,6 @@ "pagination": { "of": "of" }, - "support": { - "action": "Contact support", - "text": "If you have any questions related to the game, please contact our support team." - }, "settings": { "accountInfo": "Account information", "back": "Back", @@ -44,6 +45,11 @@ "support": "Support", "transactionHistory": "Transaction History" }, + "support": { + "action": "Contact support", + "text": "If you have any questions related to the game, please contact our support team.", + "title": "Support" + }, "transactionHistory": { "date": "Date", "operationType": "Operation type", diff --git a/public/locales/es.json b/public/locales/es.json index f091a87..45aa974 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -12,6 +12,11 @@ "off": "no", "on": "sí" }, + "faq": { + "1a": "Respuesta", + "1q": "Pregunta", + "title": "FAQ" + }, "nav": { "apiary": "Apiario", "cashdesk": "Caja", @@ -31,10 +36,6 @@ "pagination": { "of": "de" }, - "support": { - "action": "Contactar soporte", - "text": "Si tienes alguna pregunta relacionada con el juego, contacta con nuestro equipo de soporte." - }, "settings": { "accountInfo": "Información de la cuenta", "back": "Volver", @@ -44,6 +45,11 @@ "support": "Soporte", "transactionHistory": "Historial de transacciones" }, + "support": { + "action": "Contactar soporte", + "text": "Si tienes alguna pregunta relacionada con el juego, contacta con nuestro equipo de soporte.", + "title": "Soporte" + }, "transactionHistory": { "date": "Fecha", "operationType": "Tipo de operación", diff --git a/public/locales/fr.json b/public/locales/fr.json index 4f758ca..0d71f0a 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -12,6 +12,11 @@ "off": "non", "on": "oui" }, + "faq": { + "1a": "Réponse", + "1q": "Question", + "title": "FAQ" + }, "nav": { "apiary": "Rucher", "cashdesk": "Caisse", @@ -31,10 +36,6 @@ "pagination": { "of": "sur" }, - "support": { - "action": "Contacter le support", - "text": "Si vous avez des questions liées au jeu, veuillez contacter notre équipe de support." - }, "settings": { "accountInfo": "Informations du compte", "back": "Retour", @@ -44,6 +45,11 @@ "support": "Support", "transactionHistory": "Historique des transactions" }, + "support": { + "action": "Contacter le support", + "text": "Si vous avez des questions liées au jeu, veuillez contacter notre équipe de support.", + "title": "Support" + }, "transactionHistory": { "date": "Date", "operationType": "Type d'opération", diff --git a/public/locales/id.json b/public/locales/id.json index 456df1e..3ad4f37 100644 --- a/public/locales/id.json +++ b/public/locales/id.json @@ -12,6 +12,11 @@ "off": "mati", "on": "nyala" }, + "faq": { + "1a": "Jawaban", + "1q": "Pertanyaan", + "title": "FAQ" + }, "nav": { "apiary": "Peternakan Lebah", "cashdesk": "Kasir", @@ -31,10 +36,6 @@ "pagination": { "of": "dari" }, - "support": { - "action": "Hubungi dukungan", - "text": "Jika Anda memiliki pertanyaan terkait permainan, silakan hubungi tim dukungan kami." - }, "settings": { "accountInfo": "Informasi akun", "back": "Kembali", @@ -44,6 +45,11 @@ "support": "Dukungan", "transactionHistory": "Riwayat transaksi" }, + "support": { + "action": "Hubungi dukungan", + "text": "Jika Anda memiliki pertanyaan terkait permainan, silakan hubungi tim dukungan kami.", + "title": "Dukungan" + }, "transactionHistory": { "date": "Tanggal", "operationType": "Jenis operasi", diff --git a/public/locales/it.json b/public/locales/it.json index 780d35c..c45ef3d 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -12,6 +12,11 @@ "off": "no", "on": "sì" }, + "faq": { + "1a": "Risposta", + "1q": "Domanda", + "title": "FAQ" + }, "nav": { "apiary": "Apiario", "cashdesk": "Cassa", @@ -31,10 +36,6 @@ "pagination": { "of": "di" }, - "support": { - "action": "Contatta il supporto", - "text": "Se hai domande relative al gioco, contatta il nostro team di supporto." - }, "settings": { "accountInfo": "Informazioni account", "back": "Indietro", @@ -44,6 +45,11 @@ "support": "Supporto", "transactionHistory": "Cronologia transazioni" }, + "support": { + "action": "Contatta il supporto", + "text": "Se hai domande relative al gioco, contatta il nostro team di supporto.", + "title": "Supporto" + }, "transactionHistory": { "date": "Data", "operationType": "Tipo di operazione", diff --git a/public/locales/nl.json b/public/locales/nl.json index 68b881c..ba91db0 100644 --- a/public/locales/nl.json +++ b/public/locales/nl.json @@ -12,6 +12,11 @@ "off": "uit", "on": "aan" }, + "faq": { + "1a": "Antwoord", + "1q": "Vraag", + "title": "FAQ" + }, "nav": { "apiary": "Bijenstal", "cashdesk": "Kassa", @@ -31,10 +36,6 @@ "pagination": { "of": "van" }, - "support": { - "action": "Contact opnemen", - "text": "Als u vragen heeft over het spel, neem dan contact op met ons ondersteuningsteam." - }, "settings": { "accountInfo": "Accountinformatie", "back": "Terug", @@ -44,6 +45,11 @@ "support": "Ondersteuning", "transactionHistory": "Transactiegeschiedenis" }, + "support": { + "action": "Contact opnemen", + "text": "Als u vragen heeft over het spel, neem dan contact op met ons ondersteuningsteam.", + "title": "Ondersteuning" + }, "transactionHistory": { "date": "Datum", "operationType": "Bewerkingstype", diff --git a/public/locales/pl.json b/public/locales/pl.json index 525e7d2..2161cd3 100644 --- a/public/locales/pl.json +++ b/public/locales/pl.json @@ -12,6 +12,11 @@ "off": "wył", "on": "wł" }, + "faq": { + "1a": "Odpowiedź", + "1q": "Pytanie", + "title": "FAQ" + }, "nav": { "apiary": "Pasieka", "cashdesk": "Kasa", @@ -31,10 +36,6 @@ "pagination": { "of": "z" }, - "support": { - "action": "Skontaktuj się z pomocą", - "text": "Jeśli masz pytania dotyczące gry, skontaktuj się z naszym zespołem wsparcia." - }, "settings": { "accountInfo": "Informacje o koncie", "back": "Wstecz", @@ -44,6 +45,11 @@ "support": "Wsparcie", "transactionHistory": "Historia transakcji" }, + "support": { + "action": "Skontaktuj się z pomocą", + "text": "Jeśli masz pytania dotyczące gry, skontaktuj się z naszym zespołem wsparcia.", + "title": "Wsparcie" + }, "transactionHistory": { "date": "Data", "operationType": "Typ operacji", diff --git a/public/locales/pt.json b/public/locales/pt.json index f104a12..5fa2a66 100644 --- a/public/locales/pt.json +++ b/public/locales/pt.json @@ -12,6 +12,11 @@ "off": "não", "on": "sim" }, + "faq": { + "1a": "Resposta", + "1q": "Pergunta", + "title": "FAQ" + }, "nav": { "apiary": "Apiário", "cashdesk": "Caixa", @@ -31,10 +36,6 @@ "pagination": { "of": "de" }, - "support": { - "action": "Contactar suporte", - "text": "Se tiver dúvidas relacionadas ao jogo, entre em contato com nossa equipe de suporte." - }, "settings": { "accountInfo": "Informações da conta", "back": "Voltar", @@ -44,6 +45,11 @@ "support": "Suporte", "transactionHistory": "Histórico de transações" }, + "support": { + "action": "Contactar suporte", + "text": "Se tiver dúvidas relacionadas ao jogo, entre em contato com nossa equipe de suporte.", + "title": "Suporte" + }, "transactionHistory": { "date": "Data", "operationType": "Tipo de operação", diff --git a/public/locales/ru.json b/public/locales/ru.json index 02eeeda..2962a75 100644 --- a/public/locales/ru.json +++ b/public/locales/ru.json @@ -12,6 +12,11 @@ "off": "выкл", "on": "вкл" }, + "faq": { + "1a": "Ответ", + "1q": "Вопрос", + "title": "ЧаВо" + }, "nav": { "apiary": "Пасека", "cashdesk": "Касса", @@ -31,10 +36,6 @@ "pagination": { "of": "из" }, - "support": { - "action": "Связаться с поддержкой", - "text": "Если у вас возникли вопросы, связанные с игрой — обратитесь в нашу службу поддержки." - }, "settings": { "accountInfo": "Информация об аккаунте", "back": "Назад", @@ -44,6 +45,11 @@ "support": "Поддержка", "transactionHistory": "История транзакций" }, + "support": { + "action": "Связаться с поддержкой", + "text": "Если у вас возникли вопросы, связанные с игрой — обратитесь в нашу службу поддержки.", + "title": "Поддержка" + }, "transactionHistory": { "date": "Дата", "operationType": "Тип операции", diff --git a/public/locales/tr.json b/public/locales/tr.json index 65ac6d0..cb0115f 100644 --- a/public/locales/tr.json +++ b/public/locales/tr.json @@ -12,6 +12,11 @@ "off": "kapalı", "on": "açık" }, + "faq": { + "1a": "Cevap", + "1q": "Soru", + "title": "SSS" + }, "nav": { "apiary": "Arılık", "cashdesk": "Kasa", @@ -31,10 +36,6 @@ "pagination": { "of": "/" }, - "support": { - "action": "Destek ile iletişime geç", - "text": "Oyunla ilgili sorularınız varsa lütfen destek ekibimizle iletişime geçin." - }, "settings": { "accountInfo": "Hesap bilgileri", "back": "Geri", @@ -44,6 +45,11 @@ "support": "Destek", "transactionHistory": "İşlem Geçmişi" }, + "support": { + "action": "Destek ile iletişime geç", + "text": "Oyunla ilgili sorularınız varsa lütfen destek ekibimizle iletişime geçin.", + "title": "Destek" + }, "transactionHistory": { "date": "Tarih", "operationType": "İşlem türü", diff --git a/src/components/lift/LiftContext.tsx b/src/components/lift/LiftContext.tsx index 5af1a42..314f98a 100644 --- a/src/components/lift/LiftContext.tsx +++ b/src/components/lift/LiftContext.tsx @@ -1,14 +1,20 @@ import { createContext, useCallback, useContext, useRef, useState } from "react"; import type { ReactNode } from "react"; +import { cloneWithoutAnimations } from "./Liftable"; type LiftContextValue = { liftedIds: Set; alwaysLiftedIds: Set; + modalOpen: boolean; setLiftedIds: (ids: string[]) => void; registerAlways: (id: string) => void; unregisterAlways: (id: string) => void; + registerModal: () => void; + beginModalClose: () => void; + endModalClose: () => void; portalContainer: HTMLElement | null; setPortalContainer: (el: HTMLElement | null) => void; + setCloneContainer: (el: HTMLElement | null) => void; }; const LiftContext = createContext(null); @@ -22,13 +28,41 @@ export function useLift(): LiftContextValue { export function LiftProvider({ children }: { children: ReactNode }) { const [liftedIds, setLiftedIdsRaw] = useState>(new Set()); const [alwaysLiftedIds, setAlwaysLiftedIds] = useState>(new Set()); + const [modalOpen, setModalOpen] = useState(false); const [portalContainer, setPortalContainer] = useState(null); const alwaysRef = useRef>(new Set()); + const modalCountRef = useRef(0); + const cloneContainerRef = useRef(null); const setLiftedIds = useCallback((ids: string[]) => { setLiftedIdsRaw(new Set(ids)); }, []); + const registerModal = useCallback(() => { + modalCountRef.current++; + setModalOpen(true); + }, []); + + const beginModalClose = useCallback(() => { + if (portalContainer && cloneContainerRef.current) { + cloneWithoutAnimations(portalContainer, cloneContainerRef.current); + } + modalCountRef.current--; + if (modalCountRef.current === 0) { + setModalOpen(false); + } + }, [portalContainer]); + + const endModalClose = useCallback(() => { + if (cloneContainerRef.current) { + cloneContainerRef.current.innerHTML = ""; + } + }, []); + + const setCloneContainer = useCallback((el: HTMLElement | null) => { + cloneContainerRef.current = el; + }, []); + const registerAlways = useCallback((id: string) => { alwaysRef.current.add(id); setAlwaysLiftedIds(new Set(alwaysRef.current)); @@ -44,11 +78,16 @@ export function LiftProvider({ children }: { children: ReactNode }) { value={{ liftedIds, alwaysLiftedIds, + modalOpen, setLiftedIds, registerAlways, unregisterAlways, + registerModal, + beginModalClose, + endModalClose, portalContainer, setPortalContainer, + setCloneContainer, }} > {children} diff --git a/src/components/lift/LiftLayer.tsx b/src/components/lift/LiftLayer.tsx index fad673e..eeabc69 100644 --- a/src/components/lift/LiftLayer.tsx +++ b/src/components/lift/LiftLayer.tsx @@ -3,14 +3,26 @@ import { useLift } from "./LiftContext"; import classes from "./LiftLayer.module.css"; export function LiftLayer() { - const { setPortalContainer } = useLift(); + const { setPortalContainer, setCloneContainer } = useLift(); - const refCallback = useCallback( + const portalRef = useCallback( (node: HTMLDivElement | null) => { setPortalContainer(node); }, [setPortalContainer], ); - return
; + const cloneRef = useCallback( + (node: HTMLDivElement | null) => { + setCloneContainer(node); + }, + [setCloneContainer], + ); + + return ( + <> +
+
+ + ); } diff --git a/src/components/lift/Liftable.tsx b/src/components/lift/Liftable.tsx index da60d80..884da3e 100644 --- a/src/components/lift/Liftable.tsx +++ b/src/components/lift/Liftable.tsx @@ -1,8 +1,29 @@ -import { useEffect, useId, useLayoutEffect, useRef, useState } from "react"; +import { useEffect, useId, useLayoutEffect, useRef } from "react"; import type { ReactNode } from "react"; import { createPortal } from "react-dom"; import { useLift } from "./LiftContext"; +/** Recursively strip CSS animations and transitions from a cloned DOM tree */ +function stripAnimations(node: Node) { + if (node instanceof HTMLElement) { + node.style.animation = "none"; + node.style.transition = "none"; + node.getAnimations().forEach((a) => a.cancel()); + } + for (const child of Array.from(node.childNodes)) { + stripAnimations(child); + } +} + +function cloneWithoutAnimations(source: HTMLElement, target: HTMLElement) { + target.innerHTML = ""; + for (const child of Array.from(source.children)) { + const clone = child.cloneNode(true) as HTMLElement; + stripAnimations(clone); + target.appendChild(clone); + } +} + type LiftableProps = { id?: string; always?: boolean; @@ -23,7 +44,7 @@ export function useLiftable = Record; const id = idProp ?? autoId; - const { liftedIds, alwaysLiftedIds, registerAlways, unregisterAlways } = useLift(); + const { liftedIds, alwaysLiftedIds, modalOpen, registerAlways, unregisterAlways } = useLift(); useEffect(() => { if (always) { @@ -32,7 +53,7 @@ export function useLiftable = Record{render({ isLifted, ...extraProps } as { isLifted: boolean } & T)} @@ -41,13 +62,24 @@ export function useLiftable = Record(null); - const [rect, setRect] = useState(null); + const { + liftedIds, + alwaysLiftedIds, + modalOpen, + registerAlways, + unregisterAlways, + portalContainer, + } = useLift(); + + const childrenHostRef = useRef(null); + const inlineSlotRef = useRef(null); + const portalWrapperRef = useRef(null); + const wasLiftedRef = useRef(false); useEffect(() => { if (always) { @@ -56,29 +88,43 @@ export function Liftable({ id: idProp, always, children }: Props) { } }, [always, id, registerAlways, unregisterAlways]); - const isLifted = liftedIds.has(id) || alwaysLiftedIds.has(id); + const isLifted = liftedIds.has(id) || (alwaysLiftedIds.has(id) && modalOpen); useLayoutEffect(() => { - if (isLifted && wrapperRef.current) { - setRect(wrapperRef.current.getBoundingClientRect()); - } - if (!isLifted) { - setRect(null); + const host = childrenHostRef.current; + const inlineSlot = inlineSlotRef.current; + const portalWrapper = portalWrapperRef.current; + if (!host || !inlineSlot || !portalWrapper) return; + + if (isLifted && !wasLiftedRef.current) { + const rect = inlineSlot.getBoundingClientRect(); + cloneWithoutAnimations(host, inlineSlot); + portalWrapper.appendChild(host); + portalWrapper.style.cssText = `position:fixed;top:${rect.top}px;left:${rect.left}px;width:${rect.width}px;height:${rect.height}px;pointer-events:auto`; + wasLiftedRef.current = true; + } else if (!isLifted && wasLiftedRef.current) { + inlineSlot.innerHTML = ""; + inlineSlot.appendChild(host); + portalWrapper.style.cssText = "display:none"; + wasLiftedRef.current = false; } }, [isLifted]); - // Re-measure on resize while lifted useEffect(() => { - if (!isLifted || !wrapperRef.current) return; + if (!isLifted || !inlineSlotRef.current || !portalWrapperRef.current) return; + const portalWrapper = portalWrapperRef.current; + const inlineSlot = inlineSlotRef.current; const measure = () => { - if (wrapperRef.current) { - setRect(wrapperRef.current.getBoundingClientRect()); - } + const rect = inlineSlot.getBoundingClientRect(); + portalWrapper.style.top = `${rect.top}px`; + portalWrapper.style.left = `${rect.left}px`; + portalWrapper.style.width = `${rect.width}px`; + portalWrapper.style.height = `${rect.height}px`; }; const observer = new ResizeObserver(measure); - observer.observe(wrapperRef.current); + observer.observe(inlineSlot); window.addEventListener("resize", measure); return () => { @@ -87,35 +133,13 @@ export function Liftable({ id: idProp, always, children }: Props) { }; }, [isLifted]); - // When lifted and we have measurements + portal target, render in portal - if (isLifted && rect && portalContainer) { - return ( - <> - {/* Placeholder preserves layout space */} -
- {/* Portal children above blur */} - {createPortal( -
- {children} -
, - portalContainer, - )} - - ); - } - - // Normal inline rendering - return
{children}
; + return ( + <> +
+
{children}
+
+ {portalContainer && + createPortal(
, portalContainer)} + + ); } diff --git a/src/components/modals/Modal/Modal.tsx b/src/components/modals/Modal/Modal.tsx index f09ad01..692cee3 100644 --- a/src/components/modals/Modal/Modal.tsx +++ b/src/components/modals/Modal/Modal.tsx @@ -17,9 +17,18 @@ type Props = { }; export default function Modal({ open, children, onClose, liftIds, title, className }: Props) { - const { setLiftedIds } = useLift(); + const { setLiftedIds, registerModal, beginModalClose, endModalClose } = useLift(); const prevLiftIdsRef = useRef(""); + useEffect(() => { + if (open) { + registerModal(); + return () => { + beginModalClose(); + }; + } + }, [open, registerModal, beginModalClose]); + useEffect(() => { const key = liftIds?.join(",") ?? ""; @@ -45,7 +54,7 @@ export default function Modal({ open, children, onClose, liftIds, title, classNa }, [setLiftedIds]); return ( - + {open && ( <>
-
-
-
+
+ {satue} + + + +
+
diff --git a/src/routes/-/RootLayout/components/Header/components/Settings/Settings.module.css b/src/routes/-/RootLayout/components/Header/components/Settings/Settings.module.css index 805ba39..6440f18 100644 --- a/src/routes/-/RootLayout/components/Header/components/Settings/Settings.module.css +++ b/src/routes/-/RootLayout/components/Header/components/Settings/Settings.module.css @@ -1,6 +1,5 @@ @layer base { .settings { - margin-left: auto; width: 80px; height: 50px; display: flex;