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 ( +
+
+ +

Habib Marriage

+ +
+
+
+ heavenly marriage +

+ A Path to Heavenly Marriage +

+

+ We have come together with the goal of creating a secure and + confidential path for "permanent marriage" among Muslims +

+
+
+
+ User +
+

120

+

+ user profile +

+
+
+
+ User +
+

14

+

+ matches +

+
+
+
+ User +
+

14

+

+ marriage +

+
+
+
+
+ video + play +
+
+ +
+
+
+ ); +} 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 ( -
-
- Next.js logo +
+ -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
-
- - Vercel logomark - Deploy Now - - - Documentation - -
-
-
+ + ); } 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 ( +
+
+
+ router.back()} + /> + +
+
+ +
+
+ {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 ( + + ); + })} +
+
+
+
+ ); +} 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 ( +
+ + +
+ ); +} 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 +

+
+ +
+
+ man +

Submit Man

+
+
+ man +

Submit Woman

+
+
+
+
+ ); +} 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 +

+
+
+
+ video + play +
+
+
+ ); +} 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 ( +