diff --git a/package-lock.json b/package-lock.json
index 3e685cf..b73efa6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,8 @@
"dependencies": {
"next": "16.2.3",
"react": "19.2.4",
- "react-dom": "19.2.4"
+ "react-dom": "19.2.4",
+ "react-icons": "^5.6.0"
},
"devDependencies": {
"@biomejs/biome": "2.2.0",
@@ -1683,6 +1684,15 @@
"react": "^19.2.4"
}
},
+ "node_modules/react-icons": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz",
+ "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
diff --git a/package.json b/package.json
index 0791c9c..dda289c 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,8 @@
"dependencies": {
"next": "16.2.3",
"react": "19.2.4",
- "react-dom": "19.2.4"
+ "react-dom": "19.2.4",
+ "react-icons": "^5.6.0"
},
"devDependencies": {
"@biomejs/biome": "2.2.0",
diff --git a/public/assets/images/Frame 1116607280.svg b/public/assets/images/Frame 1116607280.svg
new file mode 100644
index 0000000..cff4e27
--- /dev/null
+++ b/public/assets/images/Frame 1116607280.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/assets/images/Frame 20953586523.png b/public/assets/images/Frame 20953586523.png
new file mode 100644
index 0000000..27b68cc
Binary files /dev/null and b/public/assets/images/Frame 20953586523.png differ
diff --git a/public/assets/images/Frame 2095586523.png b/public/assets/images/Frame 2095586523.png
new file mode 100644
index 0000000..9c8266e
Binary files /dev/null and b/public/assets/images/Frame 2095586523.png differ
diff --git a/public/assets/images/Group 1597880452.svg b/public/assets/images/Group 1597880452.svg
new file mode 100644
index 0000000..c8be4e4
--- /dev/null
+++ b/public/assets/images/Group 1597880452.svg
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/assets/images/Group 27032.svg b/public/assets/images/Group 27032.svg
new file mode 100644
index 0000000..2fdf00b
--- /dev/null
+++ b/public/assets/images/Group 27032.svg
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/assets/images/Group 27033.svg b/public/assets/images/Group 27033.svg
new file mode 100644
index 0000000..51a9746
--- /dev/null
+++ b/public/assets/images/Group 27033.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/public/assets/images/Inner Plugin Iframe.svg b/public/assets/images/Inner Plugin Iframe.svg
new file mode 100644
index 0000000..07d9431
--- /dev/null
+++ b/public/assets/images/Inner Plugin Iframe.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/public/assets/images/home-Checkups-List.svg b/public/assets/images/home-Checkups-List.svg
new file mode 100644
index 0000000..fe1a7b8
--- /dev/null
+++ b/public/assets/images/home-Checkups-List.svg
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/assets/images/icon-park-solid_success.svg b/public/assets/images/icon-park-solid_success.svg
new file mode 100644
index 0000000..4a460e3
--- /dev/null
+++ b/public/assets/images/icon-park-solid_success.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/assets/images/support.svg b/public/assets/images/support.svg
new file mode 100644
index 0000000..88865a9
--- /dev/null
+++ b/public/assets/images/support.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/public/assets/images/tabler_user-filled.svg b/public/assets/images/tabler_user-filled.svg
new file mode 100644
index 0000000..b65b012
--- /dev/null
+++ b/public/assets/images/tabler_user-filled.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/public/assets/images/typcn_heart-full-outline.svg b/public/assets/images/typcn_heart-full-outline.svg
new file mode 100644
index 0000000..0749d16
--- /dev/null
+++ b/public/assets/images/typcn_heart-full-outline.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/public/fonts/Faminela/Faminela.otf b/public/fonts/Faminela/Faminela.otf
new file mode 100644
index 0000000..091ff77
Binary files /dev/null and b/public/fonts/Faminela/Faminela.otf differ
diff --git a/public/fonts/Faminela/Faminela.ttf b/public/fonts/Faminela/Faminela.ttf
new file mode 100644
index 0000000..a99d4f2
Binary files /dev/null and b/public/fonts/Faminela/Faminela.ttf differ
diff --git a/src/app/globals.css b/src/app/globals.css
index a2dc41e..823d594 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,8 +1,7 @@
@import "tailwindcss";
:root {
- --background: #ffffff;
- --foreground: #171717;
+ --default-page-background-image: url("/assets/images/home-Checkups-List.svg");
}
@theme inline {
@@ -10,17 +9,38 @@
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
+ --font-faminela: var(--font-faminela-local);
}
-@media (prefers-color-scheme: dark) {
- :root {
- --background: #0a0a0a;
- --foreground: #ededed;
- }
+html {
+ min-height: 100%;
}
body {
- background: var(--background);
+ min-height: 100vh;
+ margin: 0;
+ display: flex;
+ justify-content: center;
color: var(--foreground);
- font-family: Arial, Helvetica, sans-serif;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+}
+
+.app-shell {
+ width: min(100%, 375px);
+ min-height: 100vh;
+ padding-inline: 17px;
+ box-sizing: border-box;
+ background-color: var(--background);
+ background-image: var(--default-page-background-image);
+ background-position: top;
+ background-repeat: no-repeat;
+ background-size: cover;
+}
+
+body[data-page-background="none"] .app-shell {
+ background-image: none;
+}
+
+body[data-page-background="custom"] .app-shell {
+ background-image: var(--page-background-image);
}
diff --git a/src/app/intro/page.tsx b/src/app/intro/page.tsx
new file mode 100644
index 0000000..ac9bb8b
--- /dev/null
+++ b/src/app/intro/page.tsx
@@ -0,0 +1,96 @@
+import Button from "@/components/ui/button";
+import NavigationButton from "@/components/ui/navigation-button";
+import Image from "next/image";
+
+export default function Intro() {
+ return (
+
+
+
+
+
+
+ A Path to Heavenly Marriage
+
+
+ We have come together with the goal of creating a secure and
+ confidential path for "permanent marriage" among Muslims
+
+
+
+
+
+
+
120
+
+ user profile
+
+
+
+
+
+
+
+
+
+
+
+
+ Submit
+
+
+
+
+ );
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 976eb90..ca984b2 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,15 +1,11 @@
import type { Metadata } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
+import localFont from "next/font/local";
import "./globals.css";
-const geistSans = Geist({
- variable: "--font-geist-sans",
- subsets: ["latin"],
-});
-
-const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
- subsets: ["latin"],
+const faminela = localFont({
+ src: "../../public/fonts/Faminela/Faminela.otf",
+ variable: "--font-faminela-local",
+ display: "swap",
});
export const metadata: Metadata = {
@@ -23,11 +19,10 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
- {children}
+
+
+ {children}
+
);
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 3f36f7c..6d3849b 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,65 +1,16 @@
-import Image from "next/image";
+import InfoProgressCard from "@/components/ui/info-progress-card";
export default function Home() {
return (
-
-
-
+
+
-
-
- To get started, edit the page.tsx file.
-
-
- Looking for a starting point or more instructions? Head over to{" "}
-
- Templates
- {" "}
- or the{" "}
-
- Learning
- {" "}
- center.
-
-
-
-
-
+
+
);
}
diff --git a/src/app/slider/page.tsx b/src/app/slider/page.tsx
new file mode 100644
index 0000000..bc51f22
--- /dev/null
+++ b/src/app/slider/page.tsx
@@ -0,0 +1,5 @@
+import SliderPage from "@/components/sliders/slider-page";
+
+export default function SliderRoute() {
+ return ;
+}
diff --git a/src/components/sliders/slider-page.tsx b/src/components/sliders/slider-page.tsx
new file mode 100644
index 0000000..67a30f8
--- /dev/null
+++ b/src/components/sliders/slider-page.tsx
@@ -0,0 +1,129 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+import type { TouchEvent } from "react";
+import { useState } from "react";
+import { SliderSlideFour } from "@/components/sliders/slider-slide-four";
+import {
+ SliderSlideOne,
+ SliderSlideOneActions,
+} from "@/components/sliders/slider-slide-one";
+import { SliderSlideThree } from "@/components/sliders/slider-slide-three";
+import { SliderSlideTwo } from "@/components/sliders/slider-slide-two";
+import NavigationButton from "@/components/ui/navigation-button";
+
+const SLIDE_COUNT = 4;
+const SWIPE_THRESHOLD = 40;
+const SLIDES = [
+ { id: "slide-1", component: SliderSlideOne },
+ { id: "slide-2", component: SliderSlideTwo },
+ { id: "slide-3", component: SliderSlideThree },
+ { id: "slide-4", component: SliderSlideFour },
+] as const;
+
+export default function SliderPage() {
+ const router = useRouter();
+ const [activeSlide, setActiveSlide] = useState(0);
+ const [touchStartX, setTouchStartX] = useState(null);
+ const shouldShowBottomActions =
+ activeSlide === 0 ||
+ activeSlide === 1 ||
+ activeSlide === 2 ||
+ activeSlide === 3;
+
+ const goToSlide = (index: number) => {
+ setActiveSlide(Math.max(0, Math.min(index, SLIDE_COUNT - 1)));
+ };
+
+ const goToPreviousSlide = () => {
+ goToSlide(activeSlide - 1);
+ };
+
+ const goToNextSlide = () => {
+ goToSlide(activeSlide + 1);
+ };
+
+ const handleTouchStart = (event: TouchEvent) => {
+ setTouchStartX(event.touches[0]?.clientX ?? null);
+ };
+
+ const handleTouchEnd = (event: TouchEvent) => {
+ if (touchStartX === null) {
+ return;
+ }
+
+ const touchEndX = event.changedTouches[0]?.clientX ?? touchStartX;
+ const deltaX = touchStartX - touchEndX;
+
+ if (Math.abs(deltaX) >= SWIPE_THRESHOLD) {
+ if (deltaX > 0) {
+ goToNextSlide();
+ } else {
+ goToPreviousSlide();
+ }
+ }
+
+ setTouchStartX(null);
+ };
+
+ return (
+
+
+
+
+
+ {SLIDES.map(({ id, component: SlideComponent }, index) => (
+
+ ))}
+
+
+
+ {shouldShowBottomActions ? (
+
+ ) : null}
+
+ );
+}
diff --git a/src/components/sliders/slider-slide-four.tsx b/src/components/sliders/slider-slide-four.tsx
new file mode 100644
index 0000000..9d0e6e0
--- /dev/null
+++ b/src/components/sliders/slider-slide-four.tsx
@@ -0,0 +1,88 @@
+"use client";
+
+import { useState } from "react";
+import type { SliderSlideProps } from "@/components/sliders/slider-slide";
+
+export function SliderSlideFour({ index }: SliderSlideProps) {
+ const [selectedOption, setSelectedOption] = useState<"self" | "other">(
+ "other",
+ );
+ const options = [
+ { id: "self", label: "Registering for myself" },
+ { id: "other", label: "Registering for someone else" },
+ ] as const;
+
+ return (
+
+
+
+
Submit Process
+
+ {index + 1}/4 terms & conditions
+
+
+
+
+ Read the terms and conditions carefully so that you don't run into
+ any problems in the next process - reading this section is
+ mandatory.
+
+
+
+
+ {options.map((option) => {
+ const isSelected = option.id === selectedOption;
+
+ return (
+
setSelectedOption(option.id)}
+ >
+
+
+
+
+
+
+ {option.label}
+
+
+ );
+ })}
+
+
+
+
+ );
+}
diff --git a/src/components/sliders/slider-slide-one.tsx b/src/components/sliders/slider-slide-one.tsx
new file mode 100644
index 0000000..2460568
--- /dev/null
+++ b/src/components/sliders/slider-slide-one.tsx
@@ -0,0 +1,119 @@
+import type { SliderSlideProps } from "@/components/sliders/slider-slide";
+import Button from "../ui/button";
+
+const ACTION_AREA_HEIGHT = 112;
+const CONTENT_MASK = `linear-gradient(to bottom, black 0, black calc(100% - ${ACTION_AREA_HEIGHT}px), transparent calc(100% - ${ACTION_AREA_HEIGHT}px), transparent 100%)`;
+
+export function SliderSlideOne({ index }: SliderSlideProps) {
+ return (
+
+
+
+
Submit Process
+
+ {index + 1}/4 terms & conditions
+
+
+
+
+ Dear user, while welcoming you to the Habib Marij application,
+ please read the following rules and regulations carefully before
+ using the services of this platform. Your registration and use of
+ this application constitutes full acceptance of these rules.
+
+
+
+
+
+
+ 1. Eligibility and Membership Requirements
+
+
+
+ Legal Age: {" "}
+
+ Users must meet the minimum legal age for independent
+ registration.
+
+
+
+
+ Identity Verification: {" "}
+
+ Mandatory submission of valid government-issued ID upon
+ registration.
+
+
+
+
+ Single Status Commitment: {" "}
+
+ Users must provide proof of being single, or documents
+ confirming divorce or spouse's death upon request.
+
+
+
+
+ Intent: {" "}
+
+ Commitment to monogamy and intention for permanent marriage
+ only (no simultaneous or temporary relationships).
+
+
+
+
+ General Health: {" "}
+
+ Self-declaration regarding mental health, absence of
+ addiction, and no criminal record.
+
+
+
+
+
+
+ 2. Privacy and Data Management
+
+
+
+ Content Security: {" "}
+
+ Technical prevention of screenshots from profiles and chat
+ environments.
+
+
+
+
+ Progressive Disclosure: {" "}
+
+ Sensitive information (face, contact details) revealed
+ step-by-step only with mutual consent.
+
+
+
+
+
+
+
+ );
+}
+
+export function SliderSlideOneActions() {
+ return (
+
+ Decline
+
+ Accept
+
+
+ );
+}
diff --git a/src/components/sliders/slider-slide-three.tsx b/src/components/sliders/slider-slide-three.tsx
new file mode 100644
index 0000000..b32e54a
--- /dev/null
+++ b/src/components/sliders/slider-slide-three.tsx
@@ -0,0 +1,30 @@
+import { SliderSlide, type SliderSlideProps } from "@/components/sliders/slider-slide";
+import Image from "next/image";
+
+export function SliderSlideThree({ index }: SliderSlideProps) {
+ return (
+
+
+
Submit Process
+
+ {index + 1}/4 terms & conditions
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/sliders/slider-slide-two.tsx b/src/components/sliders/slider-slide-two.tsx
new file mode 100644
index 0000000..2c09c81
--- /dev/null
+++ b/src/components/sliders/slider-slide-two.tsx
@@ -0,0 +1,36 @@
+import Image from "next/image";
+import type { SliderSlideProps } from "@/components/sliders/slider-slide";
+
+export function SliderSlideTwo({ index }: SliderSlideProps) {
+ return (
+
+
+
Submit Process
+
+ {index + 1}/4 terms & conditions
+
+
+
+
+ );
+}
diff --git a/src/components/sliders/slider-slide.tsx b/src/components/sliders/slider-slide.tsx
new file mode 100644
index 0000000..9ee1617
--- /dev/null
+++ b/src/components/sliders/slider-slide.tsx
@@ -0,0 +1,15 @@
+export type SliderSlideProps = {
+ index: number;
+};
+
+export function SliderSlide({ index }: SliderSlideProps) {
+ return (
+
+ {`Empty slide ${index + 1}`}
+ hi
+
+ );
+}
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
new file mode 100644
index 0000000..8239fa8
--- /dev/null
+++ b/src/components/ui/button.tsx
@@ -0,0 +1,183 @@
+"use client";
+
+import { type ButtonHTMLAttributes, useEffect, useId, useState } from "react";
+import { GoArrowRight } from "react-icons/go";
+
+type ButtonVariant = "default" | "secondary" | "countdown" | "outlined";
+type ArrowDirection = "left" | "right";
+
+export type ButtonProps = Omit<
+ ButtonHTMLAttributes,
+ "children"
+> & {
+ children: string;
+ description?: string;
+ variant?: ButtonVariant;
+ arrowDirection?: ArrowDirection;
+ countdownSeconds?: number;
+};
+
+const FILLED_STROKE = "#FFFFFF";
+const EMPTY_STROKE = "rgba(255, 255, 255, 0.5)";
+const RADIUS = 18;
+const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
+
+export function Button({
+ children,
+ description,
+ variant = "default",
+ arrowDirection,
+ countdownSeconds = 0,
+ disabled,
+ className,
+ type = "button",
+ ...props
+}: ButtonProps) {
+ const countdownId = useId();
+ const isCountdown = variant === "countdown";
+ const isOutlined = variant === "outlined";
+ const initialCountdown = Math.max(0, Math.ceil(countdownSeconds));
+ const [remainingSeconds, setRemainingSeconds] = useState(initialCountdown);
+
+ useEffect(() => {
+ setRemainingSeconds(initialCountdown);
+ }, [initialCountdown]);
+
+ useEffect(() => {
+ if (!isCountdown || remainingSeconds <= 0) {
+ return;
+ }
+
+ const timeoutId = window.setTimeout(() => {
+ setRemainingSeconds((current) => Math.max(0, current - 1));
+ }, 1000);
+
+ return () => window.clearTimeout(timeoutId);
+ }, [isCountdown, remainingSeconds]);
+
+ const countdownLocked = isCountdown && remainingSeconds > 0;
+ const isDisabled = disabled || countdownLocked;
+ const progress =
+ isCountdown && initialCountdown > 0
+ ? (initialCountdown - remainingSeconds) / initialCountdown
+ : 1;
+
+ const widthClass = variant === "secondary" ? "w-1/2" : "w-full";
+ const baseClassName = [
+ "inline-flex",
+ widthClass,
+ "items-center",
+ "justify-center",
+ "rounded-[11px]",
+ "px-4",
+ "py-[14px]",
+ "text-center",
+ "transition-opacity",
+ isOutlined
+ ? "border border-[#8B8B8B] bg-transparent text-[#8B8B8B]"
+ : "bg-linear-to-tl from-[#FF8DF0] to-[#F14B46] text-white",
+ isDisabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
+ className,
+ ]
+ .filter(Boolean)
+ .join(" ");
+
+ const renderArrow = (direction: ArrowDirection) => {
+ if (arrowDirection !== direction) {
+ return null;
+ }
+
+ return (
+
+ );
+ };
+
+ return (
+
+
+ {renderArrow("left")}
+
+
+ {children}
+
+ {description && !isOutlined ? (
+
+ {description}
+
+ ) : null}
+
+ {countdownLocked ? (
+
+
+
+ ) : (
+ renderArrow("right")
+ )}
+
+
+ );
+}
+
+type CountdownProgressProps = {
+ value: number;
+ progress: number;
+};
+
+function CountdownProgress({ value, progress }: CountdownProgressProps) {
+ const dashOffset = CIRCUMFERENCE * (1 - progress);
+
+ return (
+
+
+
+
+
+
+ {value}
+
+
+ );
+}
+
+export default Button;
diff --git a/src/components/ui/info-progress-card.tsx b/src/components/ui/info-progress-card.tsx
new file mode 100644
index 0000000..730b440
--- /dev/null
+++ b/src/components/ui/info-progress-card.tsx
@@ -0,0 +1,93 @@
+import { IoInformation, IoInformationCircle, IoPerson } from "react-icons/io5";
+
+type InfoProgressCardProps = {
+ title: string;
+ requiredLabel?: string;
+ estimate: string;
+ progress: number;
+};
+
+const RADIUS = 12;
+const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
+
+export function InfoProgressCard({
+ title,
+ requiredLabel = "(Required)",
+ estimate,
+ progress,
+}: InfoProgressCardProps) {
+ const normalizedProgress = Math.max(0, Math.min(progress, 100));
+ const dashOffset = CIRCUMFERENCE - (normalizedProgress / 100) * CIRCUMFERENCE;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {title}{" "}
+
+ {requiredLabel}
+
+
+
+ Estimate time: {estimate}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {normalizedProgress}%
+
+
+
+
+
+ );
+}
+
+export default InfoProgressCard;
diff --git a/src/components/ui/navigation-button.tsx b/src/components/ui/navigation-button.tsx
new file mode 100644
index 0000000..d2d69fd
--- /dev/null
+++ b/src/components/ui/navigation-button.tsx
@@ -0,0 +1,71 @@
+"use client";
+
+import Image from "next/image";
+import type { ButtonHTMLAttributes } from "react";
+import { GoArrowLeft } from "react-icons/go";
+import { IoCloseOutline } from "react-icons/io5";
+
+type NavigationButtonIcon = "back" | "support" | "close";
+
+export type NavigationButtonProps = Omit<
+ ButtonHTMLAttributes,
+ "children"
+> & {
+ icon: NavigationButtonIcon;
+ iconLabel?: string;
+};
+
+export function NavigationButton({
+ icon,
+ iconLabel,
+ type = "button",
+ className,
+ ...props
+}: NavigationButtonProps) {
+ const iconNode = (() => {
+ switch (icon) {
+ case "back":
+ return (
+
+ );
+ case "close":
+ return (
+
+ );
+ case "support":
+ return (
+
+ );
+ default:
+ return null;
+ }
+ })();
+
+ return (
+
+ {iconNode}
+
+ );
+}
+
+export default NavigationButton;
diff --git a/src/components/utils/page-background.tsx b/src/components/utils/page-background.tsx
new file mode 100644
index 0000000..6c95ec0
--- /dev/null
+++ b/src/components/utils/page-background.tsx
@@ -0,0 +1,46 @@
+"use client";
+
+import { useEffect } from "react";
+
+type PageBackgroundProps = {
+ image?: string;
+ disabled?: boolean;
+};
+
+export function PageBackground({
+ image,
+ disabled = false,
+}: PageBackgroundProps) {
+ useEffect(() => {
+ const { body } = document;
+ const previousMode = body.dataset.pageBackground;
+ const previousImage = body.style.getPropertyValue("--page-background-image");
+
+ if (disabled) {
+ body.dataset.pageBackground = "none";
+ body.style.removeProperty("--page-background-image");
+ } else if (image) {
+ body.dataset.pageBackground = "custom";
+ body.style.setProperty("--page-background-image", `url("${image}")`);
+ } else {
+ delete body.dataset.pageBackground;
+ body.style.removeProperty("--page-background-image");
+ }
+
+ return () => {
+ if (previousMode) {
+ body.dataset.pageBackground = previousMode;
+ } else {
+ delete body.dataset.pageBackground;
+ }
+
+ if (previousImage) {
+ body.style.setProperty("--page-background-image", previousImage);
+ } else {
+ body.style.removeProperty("--page-background-image");
+ }
+ };
+ }, [disabled, image]);
+
+ return null;
+}