Browse Source

feat: implement view paddings management with ViewPaddingsBridge and related components

master
mortezaei 1 month ago
parent
commit
7c2316fe0f
  1. 6
      src/app/globals.css
  2. 9
      src/app/layout.tsx
  3. 6
      src/app/providers.tsx
  4. 8
      src/components/ui/sticky-header.tsx
  5. 22
      src/components/utils/fixed-bottom.tsx
  6. 12
      src/components/utils/view-paddings-provider.tsx
  7. 14
      src/hooks/use-view-paddings.ts
  8. 92
      src/lib/view-paddings.ts
  9. 9
      src/types/window.d.ts
  10. 3
      src/view-paddings.ts

6
src/app/globals.css

@ -2,6 +2,10 @@
:root {
--default-page-background-image: url("/assets/images/home-Checkups-List.svg");
--safe-top: 0px;
--safe-bottom: 0px;
--safe-left: 0px;
--safe-right: 0px;
}
@theme inline {
@ -50,6 +54,8 @@ html:lang(ar) body,
width: 100%;
min-height: 100vh;
padding-inline: 17px;
padding-top: var(--safe-top);
padding-bottom: var(--safe-bottom);
box-sizing: border-box;
background-color: var(--background);
background-image: var(--default-page-background-image);

9
src/app/layout.tsx

@ -143,6 +143,15 @@ export default function RootLayout({
`,
}}
/>
<script
dangerouslySetInnerHTML={{
__html: `
if (typeof window !== 'undefined' && window.HabibApp) {
window.HabibApp.postMessage(JSON.stringify({ action: 'get_view_paddings' }));
}
`,
}}
/>
</head>
<body className={`${faminela.variable} ${amiri.variable}`}>
<Providers>

6
src/app/providers.tsx

@ -2,6 +2,7 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { type ReactNode, useState } from "react";
import { ViewPaddingsProvider } from "@/components/utils/view-paddings-provider";
type ProvidersProps = {
children: ReactNode;
@ -23,6 +24,9 @@ export default function Providers({ children }: ProvidersProps) {
);
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
<QueryClientProvider client={queryClient}>
<ViewPaddingsProvider />
{children}
</QueryClientProvider>
);
}

8
src/components/ui/sticky-header.tsx

@ -1,4 +1,7 @@
"use client";
import type { ReactNode } from "react";
import { useViewPaddings } from "@/hooks/use-view-paddings";
type StickyHeaderProps = {
children: ReactNode;
@ -9,10 +12,13 @@ export default function StickyHeader({
children,
className,
}: StickyHeaderProps) {
const { top } = useViewPaddings();
return (
<header
style={{ paddingTop: `calc(1.75rem + ${top}px)` }}
className={[
"sticky top-0 z-30 rounded-b-[15px] bg-[linear-gradient(135deg,#E03950_0%,#FE6F82_100%)] px-[17px] pt-7 pb-5",
"sticky top-0 z-30 rounded-b-[15px] bg-[linear-gradient(135deg,#E03950_0%,#FE6F82_100%)] px-[17px] pb-5",
className,
]
.filter(Boolean)

22
src/components/utils/fixed-bottom.tsx

@ -0,0 +1,22 @@
"use client";
import type { ReactNode } from "react";
import { useViewPaddings } from "@/hooks/use-view-paddings";
type FixedBottomProps = {
children: ReactNode;
className?: string;
};
export function FixedBottom({ children, className }: FixedBottomProps) {
const { bottom } = useViewPaddings();
return (
<div
style={{ paddingBottom: `${bottom}px` }}
className={className}
>
{children}
</div>
);
}

12
src/components/utils/view-paddings-provider.tsx

@ -0,0 +1,12 @@
"use client";
import { useEffect } from "react";
import { viewPaddingsBridge } from "@/lib/view-paddings";
export function ViewPaddingsProvider() {
useEffect(() => {
viewPaddingsBridge;
}, []);
return null;
}

14
src/hooks/use-view-paddings.ts

@ -0,0 +1,14 @@
"use client";
import { useEffect, useState } from "react";
import { viewPaddingsBridge } from "@/lib/view-paddings";
export function useViewPaddings() {
const [paddings, setPaddings] = useState(() => viewPaddingsBridge.getPaddings());
useEffect(() => {
return viewPaddingsBridge.subscribe(setPaddings);
}, []);
return paddings;
}

92
src/lib/view-paddings.ts

@ -0,0 +1,92 @@
interface ViewPaddings {
top: number;
bottom: number;
left: number;
right: number;
}
class ViewPaddingsBridge {
private paddings: ViewPaddings = { top: 0, bottom: 0, left: 0, right: 0 };
private listeners: Array<(paddings: ViewPaddings) => void> = [];
private flutterUnsubscribe?: () => void;
constructor() {
this.init();
}
private init() {
if (typeof window === "undefined") return;
this.setupFlutterListener();
this.requestPaddings();
}
private setupFlutterListener() {
const win = window as Window & {
addFlutterResponseListener?: (
listener: (event: any) => void,
) => () => void;
};
const attach = () => {
if (typeof win.addFlutterResponseListener !== "function") return false;
this.flutterUnsubscribe = win.addFlutterResponseListener((event) => {
if (event.action === "get_view_paddings" && event.success && event.data) {
this.paddings = {
top: event.data.top || 0,
bottom: event.data.bottom || 0,
left: event.data.left || 0,
right: event.data.right || 0,
};
this.applyPaddings();
this.notifyListeners();
}
});
return true;
};
if (!attach()) {
const interval = setInterval(() => {
if (attach()) clearInterval(interval);
}, 50);
}
}
private requestPaddings() {
if (typeof window === "undefined") return;
const app = (window as any).HabibApp;
if (app?.postMessage) {
app.postMessage(JSON.stringify({ action: "get_view_paddings" }));
}
}
private applyPaddings() {
if (typeof document === "undefined") return;
document.documentElement.style.setProperty("--safe-top", `${this.paddings.top}px`);
document.documentElement.style.setProperty("--safe-bottom", `${this.paddings.bottom}px`);
document.documentElement.style.setProperty("--safe-left", `${this.paddings.left}px`);
document.documentElement.style.setProperty("--safe-right", `${this.paddings.right}px`);
}
private notifyListeners() {
this.listeners.forEach((listener) => listener(this.paddings));
}
public getPaddings(): ViewPaddings {
return { ...this.paddings };
}
public subscribe(listener: (paddings: ViewPaddings) => void): () => void {
this.listeners.push(listener);
return () => {
const index = this.listeners.indexOf(listener);
if (index >= 0) this.listeners.splice(index, 1);
};
}
}
export const viewPaddingsBridge = new ViewPaddingsBridge();

9
src/types/window.d.ts

@ -2,7 +2,14 @@ declare global {
interface FlutterResponseEvent {
action: string;
success: boolean;
data?: { latitude: number; longitude: number };
data?: {
latitude?: number;
longitude?: number;
top?: number;
bottom?: number;
left?: number;
right?: number;
};
}
interface Window {

3
src/view-paddings.ts

@ -0,0 +1,3 @@
export { useViewPaddings } from "./hooks/use-view-paddings";
export { viewPaddingsBridge } from "./lib/view-paddings";
export { FixedBottom } from "./components/utils/fixed-bottom";
Loading…
Cancel
Save