Commit f0be4220 by Madhankumar

feat: initial commit

parents
---
description: Optimized Next.js TypeScript Best Practices with Modern UI/UX
globs:
alwaysApply: false
---
You are an expert full-stack developer proficient in TypeScript, React, Next.js, and Tailwind CSS. Your task is to produce the most optimized and maintainable Next.js code, following best practices and adhering to the principles of clean code and robust architecture.
### Objective
- Create a Next.js solution that is not only functional but also adheres to the best practices in performance, security, and maintainability.
### Code Style and Structure
- Write concise, technical TypeScript code with accurate examples.
- Use functional and declarative programming patterns; avoid classes.
- Favor iteration and modularization over code duplication.
- Use descriptive variable names with auxiliary verbs (e.g., `isLoading`, `hasError`).
- Structure files with exported components, subcomponents, helpers, static content, and types.
### Optimization and Best Practices
- Minimize the use of `'use client'`, `useEffect`, and `setState`; favor React Server Components (RSC) and Next.js SSR features.
- Implement dynamic imports for code splitting and optimization.
- Use responsive design with a mobile-first approach.
- Optimize images: use WebP format, include size data, implement lazy loading.
### Error Handling and Validation
- Prioritize error handling and edge cases:
- Use early returns for error conditions.
- Implement guard clauses to handle preconditions and invalid states early.
- Use custom error types for consistent error handling.
### UI and Styling
- Use modern UI frameworks (e.g., Tailwind CSS, Shadcn UI, Radix UI) for styling.
- Implement consistent design and responsive patterns across platforms.
### State Management and Data Fetching
- Use modern state management solutions (e.g., Zustand, TanStack React Query) to handle global state and data fetching.
- Implement validation using Zod for schema validation.
### Security and Performance
- Implement proper error handling, user input validation, and secure coding practices.
- Follow performance optimization techniques, such as reducing load times and improving rendering efficiency.
### Testing and Documentation
- Write unit tests for components using Jest and React Testing Library.
- Provide clear and concise comments for complex logic.
- Use JSDoc comments for functions and components to improve IDE intellisense.
### Methodology
1. **System 2 Thinking**: Approach the problem with analytical rigor. Break down the requirements into smaller, manageable parts and thoroughly consider each step before implementation.
2. **Tree of Thoughts**: Evaluate multiple possible solutions and their consequences. Use a structured approach to explore different paths and select the optimal one.
3. **Iterative Refinement**: Before finalizing the code, consider improvements, edge cases, and optimizations. Iterate through potential enhancements to ensure the final solution is robust.
### Instructions
1. Do not assume anything. If you have confusion, Immediately ass for confirmation.
**Process**:
1. **Deep Dive Analysis**: Begin by conducting a thorough analysis of the task at hand, considering the technical requirements and constraints.
2. **Planning**: Develop a clear plan that outlines the architectural structure and flow of the solution, using <PLANNING> tags if necessary.
3. **Implementation**: Implement the solution step-by-step, ensuring that each part adheres to the specified best practices.
4. **Review and Optimize**: Perform a review of the code, looking for areas of potential optimization and improvement.
5. **Finalization**: Finalize the code by ensuring it meets all requirements, is secure, and is performant.
\ No newline at end of file
You are an expert programming assistant that primarily focus on producing clear, readable Next.JS + Tailwind + Typescript code.
You always use pnpm as package manager.
You always use latest version of Next.JS, and you are familiar with the latest features and best practices of Next.JS, TypeScript and Tailwind. Remember the API uses javascript, not typescript.
You are familiar with latest features of prima and how to integrate with Next.js application.
For styling, you use Tailwind CSS. Use appropriate and most used colors for light and dark mode.
You carefully provide accurate, factual, thoughtful answers, and are a genius at reasoning.
- Follow user's requirements carefully & to the letter.
- First think step-by-step - describe your plan for what to build in pseudocode, written out in great detail.
- Confirm, then write the code!
- Always write correct, up to date, bug free, fully functional and working, secure, performant and efficient code.
- Focus on readability over performant.
- Fully implement all requested functionality.
- Leave NO Todo's, placeholders and missing pieces.
- Be sure to reference filenames.
- Be concise. Minimize any other prose.
- If you think there might not be a correct answer, you say so. If you don't know the answer, say so instead of guessing.
- Do not assume anything, ask the user for clarification if you are not sure about the requirements.
- Do not modify any other unrelated code logic
{
"path": "cz-conventional-changelog"
}
/node_modules
/build
/.git
/.next
README.md
Dockerfile
*.log
*.log*
.vscode
.DS_Store
.gitignore
.editorconfig
.env
.env.local
.gitlab-ci.yml
.eslintrc.js
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
\ No newline at end of file
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=6LfKHP8qAAAAACTpTp8SYhHCfExIJKkAa-VB3RRD
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=
APP_RECAPTCHA_SECRET=
APP_APIGEE_USERNAME=
APP_APIGEE_PASSWORD=
{
"extends": [
"next/core-web-vitals",
"plugin:storybook/recommended"
]
}
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
# yarn
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
stages:
- build
- deploy
variables:
app_name: maf-egypt-business-park
app_image_tag: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
image_tag: $CI_BUILD_REF_NAME
image: $CI_REGISTRY_IMAGE
registry_pass: $CI_BUILD_TOKEN
registry_user: gitlab-ci-token
registry: $CI_REGISTRY
slack_channel: mafegypt-businesspark
ecs_definition: config/ecs-task-definition.json
ecs_entrypoint: app:3000
docker_build_staging:
tags:
- docker
- eu
stage: build
variables:
app_env: staging
app_url: https://maf-egypt-business-park-staging.eu-staging.kacdn.net
script:
- env
- docker login -u $registry_user -p $registry_pass $registry
- docker build -t $app_image_tag
--build-arg APP_ENV=$app_env
--build-arg NEXT_PUBLIC_RECAPTCHA_SITE_KEY=$app_recaptcha_site_key
--build-arg NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=$app_google_maps_api_key .
- docker push $app_image_tag
only:
- master
deploy_staging:
image: registry.git.int.krds.com/tools/deploy:edge
tags:
- deploy
- eu
stage: deploy
variables:
app_env: staging
script:
- deploy-ecs eu-staging
only:
- master
{
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS --extends @commitlint/config-conventional",
"pre-commit": "pretty-quick --staged"
}
}
public-hoist-pattern[]=*@heroui/*
{
"jsxBracketSameLine": true,
"singleQuote": true,
"jsxSingleQuote": false,
"trailingComma": "none"
}
import type { StorybookConfig } from '@storybook/nextjs';
import path from 'path';
const config: StorybookConfig = {
stories: [
'../components/**/*.stories.@(ts|tsx|js|jsx|mdx)',
'../theme/**/*.stories.@(ts|tsx|js|jsx|mdx)'
],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-onboarding',
'@storybook/addon-interactions',
'storybook-dark-mode'
],
webpackFinal: async (config) => {
if (config.resolve) {
config.resolve.alias = {
...config.resolve.alias,
'@': path.resolve(__dirname, '../')
};
}
return config;
},
framework: {
name: '@storybook/nextjs',
options: {}
},
docs: {
autodocs: 'tag'
}
};
export default config;
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="" />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap"
rel="stylesheet"
/>
import React from 'react';
import { themes } from '@storybook/theming';
import { HeroUIProvider } from '@heroui/react';
import { ParallaxProvider } from 'react-scroll-parallax';
import './style.css';
import '@/styles/globals.css';
import type { Preview } from '@storybook/react';
const decorators: Preview['decorators'] = [
(Story, { globals: { locale, disableAnimation } }) => {
const direction =
// @ts-ignore
locale && new Intl.Locale(locale)?.textInfo?.direction === 'rtl'
? 'rtl'
: undefined;
return (
<HeroUIProvider locale={locale} disableAnimation={disableAnimation}>
<ParallaxProvider>
{' '}
<div className="bg-dark" lang={locale} dir={direction}>
<Story />
</div>
</ParallaxProvider>
</HeroUIProvider>
);
}
];
const parameters: Preview['parameters'] = {
actions: { argTypesRegex: '^on[A-Z].*' },
options: {
storySort: {
method: 'alphabetical',
order: ['Foundations']
}
},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i
}
},
darkMode: {
current: 'dark',
stylePreview: true,
darkClass: 'dark',
lightClass: 'light',
classTarget: 'html',
dark: {
...themes.dark,
appBg: '#161616',
barBg: 'black',
background: 'black',
appContentBg: 'black',
appBorderRadius: 14
},
light: {
...themes.light
}
}
};
const globalTypes: Preview['globalTypes'] = {
locale: {
toolbar: {
icon: 'globe',
items: ['en', 'ar'].map((locale) => ({
value: locale,
title: new Intl.DisplayNames(undefined, { type: 'language' }).of(
locale
),
right:
// @ts-ignore
new Intl.Locale(locale)?.textInfo?.direction === 'rtl'
? 'Right to Left'
: undefined
}))
}
},
disableAnimation: {
name: 'Disable Animation',
description: 'Disable all animations in the stories',
toolbar: {
icon: 'photodrag',
items: [
{ value: true, title: 'True' },
{ value: false, title: 'False' }
]
}
}
};
const preview: Preview = {
decorators,
parameters,
globalTypes
};
export default preview;
@tailwind base;
@tailwind components;
@tailwind utilities;
h1 {
@apply text-4xl font-bold text-foreground;
}
h2 {
@apply text-2xl font-bold text-foreground border-none;
}
h3 {
@apply text-xl font-bold text-neutral-600;
}
.dark .sbdocs-wrapper,
.dark .sbdocs-preview {
background-color: #000000;
color: #fff;
}
.dark .sbdocs-preview {
border: 1px solid #292929;
}
.dark .docblock-code-toggle {
background: transparent;
color: #d4d4d4;
}
.dark div:has(.docblock-code-toggle) {
background: transparent;
}
.dark .os-theme-dark {
background: #161616;
color: #fff;
}
.dark .sbdocs-title {
color: #fff;
}
.dark .docblock-argstable-head {
background: #161616;
}
.dark .docblock-argstable-head th {
color: #bcbcbc;
border-bottom: 1px solid #292929 !important;
}
.dark .docblock-argstable-head th span {
color: #bcbcbc;
}
.dark #docs-root tbody td {
background: #161616 !important;
color: #bcbcbc !important;
}
.dark #docs-root tbody tr:first-child td:first-child {
border-top-left-radius: 0 !important;
}
.dark #docs-root tbody tr:first-child td:last-child {
border-top-right-radius: 0 !important;
}
.dark #docs-root tbody tr:not(:first-child) {
border-top: 1px solid #292929 !important;
}
{
"cSpell.words": [
"dignissimos"
]
}
FROM node:18-alpine AS base
ARG APP_ENV
ARG NEXT_PUBLIC_RECAPTCHA_SITE_KEY
ARG NEXT_PUBLIC_GOOGLE_MAPS_API_KEY
ENV NEXT_TELEMETRY_DISABLED 1
# Install corepack for pnpm
RUN corepack enable pnpm
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --production --shamefully-hoist
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Build the application
RUN pnpm build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
# SET appropriate robots.txt for the environment
RUN if [ "$APP_ENV" = "staging" ]; then \
mv ./public/robots.staging.txt ./public/robots.txt; \
else \
mv ./public/robots.production.txt ./public/robots.txt; \
fi
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]
# MAF EGYPT BUSINESS PARK
## Tools Configured
- Next.js
- React Storybook
- Typescript
- Tailwind CSS
- Hero UI
- pnpm
- Commitizen
- Editorconfig
- Eslint
- Prettier
- Husky (to lint commit messages)
## Important Notes
- Enable nodejs corepack `corepack enable`
- Install pnpm using `corepack prepare pnpm@latest --activate`
- Install dependencies using `pnpm install`
- Install commitizen globally `npm install -g commitizen`
- Then, use `git cz` or `cz` to commit changes
- Commit messages must follow [angular conventional commit
standards](https://github.com/conventional-changelog/commitlint)
- Install prettier extension in editor. Enable "Format on save" option.
- Install editorconfig extension in editor.
- All files will be formatted using prettier before commit
### Development Server
```bash
pnpm dev
```
### Build Task
```bash
pnpm build
```
### Production Server
```bash
pnpm start
```
### Run Storybook
```bash
pnpm storybook
```
### Build Static Storybook
```bash
pnpm build-storybook
```
/** @module api/fetcher
* @param {string} url - The url to fetch
* @param {RequestInit} options - The fetch options
* @returns {Promise<T>} - The fetched data
* @throws {string} - The error message
* Fetches the data from the given url with the given options *
* @hidden
*/
// import { headers } from "next/headers";
export default async function fetcher<T>(
url: string,
options?: RequestInit,
showStrapiMetadata?: boolean
): Promise<T> {
const isSSR = typeof window === 'undefined';
const reqUrl = isSSR ? `${process.env.NEXT_PUBLIC_API_URL}${url}` : `/api/${url}`;
const printResponseInConsole =
false && process.env.NODE_ENV === 'development';
try {
const headers = options?.headers || {};
const response = await fetch(reqUrl, {
next: { revalidate: false },
...options,
headers: {
Authorization: `Bearer ${process.env.NEXT_API_TOKEN}`,
...headers
}
});
const json = await response.json();
if (response.ok) {
const response = showStrapiMetadata
? (json as Promise<T>)
: (json.data as Promise<T>);
if (printResponseInConsole) {
print.success(reqUrl, response);
} else {
print.success(reqUrl);
}
return response;
} else {
throw json;
}
} catch (error) {
console.log('3')
print.error(reqUrl, error);
throw 'Error fetching data from API';
}
}
const print = {
error: (url: string, error: any) => {
console.log('============================');
console.log(`\x1b[31mAPI Error:\x1b[0m ${url}\n`, error);
},
success: (url: string, data?: any) => {
console.log('============================');
console.log(`\x1b[32mReceived:\x1b[0m ${url}`);
if (data) {
console.log(data);
}
},
info: (url: string) => {
console.log('============================');
console.log(`\x1b[34mRequesting:\x1b[0m ${url}`);
}
};
import fetcher from "./fetcher";
export const getContact = async (): Promise<IPageRenderer> => {
const query = `/pages?filters[slug]=contact&populate[components][on][contact.contact][populate]=*`;
const response = await fetcher< IPageRenderer[]>(query);
return response[0];
};
import fetcher from "./fetcher";
export default async function getHeader(): Promise<IHeader> {
const response = await fetcher<IHeader>('/header?populate=*');
return response;
}
import fetcher from "./fetcher";
export default async function getLabels(): Promise<IFormLabels> {
const response = await fetcher<{ data: IFormLabels }>('/label?populate=*');
const formData = response.data;
console.log('formData', formData);
return formData
}
import fetcher from './fetcher';
interface Params {
slug: string;
}
export const getPage = async (): Promise<IPageRenderer> => {
const query = `/pages?filters[slug]=home&populate[components][on][banner.banner][populate][specifications][populate]=*&populate[components][on][banner.banner][populate][image][populate]=*&populate[components][on][image-carousel.image-carousel][populate][slides][populate]=*&populate[components][on][amenities.amenities][populate][image][populate]=*&populate[components][on][gallery.gallery][populate][gallery][populate]=*&populate[components][on][address-map.address-map][populate]=*`;
const response = await fetcher<IPageRenderer[]>(query);
return response[0];
};
import fetcher from "./fetcher";
interface ITermsAndConditionsResponse extends Object {
termsAndConditions: IMarkdown;
}
export const getTermsAndConditions = async (): Promise<ITermsAndConditionsResponse> => {
const query = `/terms-and-conditions?populate=*`;
const response = await fetcher<IApiResponse<ITermsAndConditionsResponse>>(query);
const item = response.data[0];
return { termsAndConditions: item.termsAndConditions };
};
'use server';
import { IContactFormValues } from '@/components/form/ContactForm/ContactForm';
import { verifyRecaptcha } from './utils/verifyRecaptcha';
import fetcher from './fetcher';
const submitForm = async (values: IContactFormValues): Promise<IResponse> => {
try {
const captchaToken = await verifyRecaptcha(values.recaptchaToken);
if (!captchaToken) {
throw new Error('Invalid captcha token');
}
const payload = {
title: values.title,
firstName: values.firstName,
lastName: values.lastName,
middleName: '',
countryCode: values.dialCode,
mobileNumber: values.mobileNumber,
email: values.email,
source: 'WebToLead',
channelType: 'WebToLead',
channelName: '',
countryOfResidence: values.residence,
propertyType: values.unitType,
unitSize: values.unitSize,
budget: values.budget,
buyingPurpose: values.purpose,
project: 'JN',
companyName: '',
formName: ''
};
await fetcher(`/contacts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ data: payload })
});
return { success: true };
} catch (error: any) {
console.error('=> Error submitting lead', error);
return { success: false, code: error?.message };
}
};
export default submitForm;
export const verifyRecaptcha = async (token: string): Promise<boolean> => {
console.log('token', token);
const response = await fetch(
`https://www.google.com/recaptcha/api/siteverify?secret=${process.env.APP_RECAPTCHA_SECRET}&response=${token}`
);
const json = await response.json();
console.log('json',json);
if (json.success) {
console.log('=> ReCaptcha verified');
return true;
}
console.error('=> ReCaptcha verification failed', json);
throw new Error(json['error-codes']);
};
import { getPage } from '@/api/getPage';
import { notFound } from 'next/navigation';
import { Suspense } from 'react';
import PageRenderer from '@/components/layout/PageRenderer';
export default async function AsyncPage() {
const page = await getPage();
return <PageRenderer components={page.components} prefix="home" />;
}
import { getContact } from '@/api/getContact';
import PageRenderer from '@/components/layout/PageRenderer';
export default async function ContactPage() {
const page = await getContact();
return <PageRenderer components={page.components} prefix="contact" />;
}
import type { Metadata } from 'next';
import Providers from '@/components/layout/Providers';
import '@/styles/globals.css';
import AsyncHeader from '@/components/layout/Header/AsyncHeader';
interface LayoutProps {
params: Promise<IParams>;
children: React.ReactNode;
}
export const metadata: Metadata = {
title: 'MAF Egypt Business Park',
description: 'MAF Egypt Business Park'
};
export default async function RootLayout({ params, children }: LayoutProps) {
const { locale } = await params;
return (
<html lang={locale}>
<head>
<meta content="width=device-width, initial-scale=1" name="viewport" />
<link rel="icon" href="/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin=""
/>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap"
rel="stylesheet"
/>
{/* eslint-disable-next-line @next/next/next-script-for-ga */}
<script
dangerouslySetInnerHTML={{
__html: `
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-5SZXVTJC');
`
}}
/>
</head>
<body>
{/* eslint-disable-next-line @next/next/next-script-for-ga */}
<noscript>
<iframe
src="https://www.googletagmanager.com/ns.html?id=GTM-5SZXVTJC"
height="0"
width="0"
style={{ display: 'none', visibility: 'hidden' }}></iframe>
</noscript>
<Providers>
<div className="flex flex-col min-h-screen">
<main className="grow [&>*:last-child:not(:only-child)]:mb-0">
<AsyncHeader />
{children}
</main>
</div>
</Providers>
</body>
</html>
);
}
import { Spinner } from '@heroui/react';
export default function Loading() {
return <h1>Loading...</h1>;
}
// import { getPage } from '@/api/getPageBySlug';
// import { notFound } from 'next/navigation';
// import AsyncPage from './AsyncPage';
// import { metaDataFactory } from '@/lib/metaDataFactory';
// interface Params {
// params: {
// slug: string;
// };
// }
// // export const generateMetadata = metaDataFactory({
// // apiMethod: getPageBySlug
// // });
// export default async function Page({ params }: Params) {
// try {
// await getPageBySlug({ slug: params.slug });
// } catch {
// return notFound();
// }
// return <AsyncPage slug={params.slug} />;
// }
import { getPage } from '@/api/getPage';
import { notFound } from 'next/navigation';
import { Suspense } from 'react';
import AsyncPage from './AsyncPage';
// interface Params {
// params: Promise<{ slug: string }>;
// }
export default async function Page() {
try {
await getPage();
} catch {
return notFound();
}
return <AsyncPage />;
}
export const metadata = {
title: 'Next.js',
description: 'Generated by Next.js',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
import Container from '@/components/layout/Container';
import RenderMarkdown from '@/components/shared/RenderMarkdown';
import { getTermsAndConditions } from '@/api/getTermsAndConditions';
export default async function TermsAndConditions() {
const { termsAndConditions } = await getTermsAndConditions();
return (
<Container as="section" className="my-16">
<RenderMarkdown
components={{
h1: ({ children }) => (
<h1 className="text-inherit text-2xl font-serif mb-4 md:text-4xl">
{children}
</h1>
),
p: ({ children }) => <p className="mb-3">{children}</p>,
ul: ({ children }) => <ul className="list-disc ps-6">{children}</ul>
}}>
{termsAndConditions}
</RenderMarkdown>
</Container>
);
}
import type { Meta, StoryObj } from '@storybook/react';
import Button from './Button';
import type { ButtonProps } from '@heroui/button';
const meta: Meta<typeof Button> = {
title: 'Base/Button',
component: Button,
tags: ['autodocs']
};
export default meta;
type Story = StoryObj<typeof meta>;
type TemplateProps = {
color: ButtonProps['color'];
};
const Template = ({ color }: TemplateProps) => {
return (
<div>
<h6 className="mb-2 font-semibold">Size</h6>
<div className="flex gap-5 mb-4 flex-wrap items-center">
<Button color={color} size="sm">
Small button
</Button>
<Button color={color} size="md">
Medium button
</Button>
<Button color={color} size="lg">
Large button
</Button>
<Button color={color} size="sm" variant="ghost">
Small Outline
</Button>
<Button color={color} size="md" variant="ghost">
Medium Outline
</Button>
<Button color={color} size="lg" variant="ghost">
Large Outline
</Button>
</div>
<h6 className="mb-2 font-semibold">Loading</h6>
<div className="flex gap-5 mb-4 flex-wrap items-center">
<Button color={color} size="sm" isLoading>
Small button
</Button>
<Button color={color} size="md" isLoading>
Medium button
</Button>
<Button color={color} size="lg" isLoading>
Large button
</Button>
<Button color={color} size="sm" variant="ghost" isLoading>
Small Outline
</Button>
<Button color={color} size="md" variant="ghost" isLoading>
Medium Outline
</Button>
<Button color={color} size="lg" variant="ghost" isLoading>
Large Outline
</Button>
</div>
<h6 className="mb-2 font-semibold">Disabled</h6>
<div className="flex gap-5 mb-4 flex-wrap items-center">
<Button color={color} size="sm" isDisabled>
Small button
</Button>
<Button color={color} size="md" isDisabled>
Medium button
</Button>
<Button color={color} size="lg" isDisabled>
Large button
</Button>
<Button color={color} size="sm" variant="ghost" isDisabled>
Small Outline
</Button>
<Button color={color} size="md" variant="ghost" isDisabled>
Medium Outline
</Button>
<Button color={color} size="lg" variant="ghost" isDisabled>
Large Outline
</Button>
</div>
<h6 className="mb-2 font-semibold">Full Width</h6>
<div className="flex flex-col gap-5">
<Button color={color} size="sm" fullWidth>
Small button
</Button>
<Button color={color} size="md" fullWidth>
Medium button
</Button>
<Button color={color} size="lg" fullWidth>
Large button
</Button>
<Button color={color} size="sm" variant="ghost" fullWidth>
Small Outline
</Button>
<Button color={color} size="md" variant="ghost" fullWidth>
Medium Outline
</Button>
<Button color={color} size="lg" variant="ghost" fullWidth>
Large Outline
</Button>
</div>
</div>
);
};
export const Default: Story = {
render: () => <Template color="default" />
};
export const Primary: Story = {
render: () => <Template color="primary" />
};
'use client';
import { extendVariants, Button as NextButton } from '@heroui/react';
const Button = extendVariants(NextButton, {
variants: {
size: {
sm: 'rounded-full font-light',
md: 'rounded-full font-light',
lg: 'rounded-full font-light'
},
color: {
default:
'border-[1.2px] bg-origin-border bg-background data-[hover=true]:border-transparent data-[hover=true]:before:bg-primary-800 data-[hover=true]:text-background data-[hover=true]:bg-[linear-gradient(90deg,#CC93D3_0%,#793BE0_100%)] before:content-[""] before:absolute before:inset-0 before:-z-10 dark:bg-foreground dark:data-[hover=true]:before:bg-primary-800 dark:data-[hover=true]:text-foreground data-[hover=true]:!duration-[600ms] dark:text-background',
primary:
'border-[1.2px] bg-origin-border border-transparent bg-[linear-gradient(90deg,#CC93D3_0%,#793BE0_100%)] before:content-[""] before:absolute before:inset-0 before:bg-primary-800/95 before:-z-10 data-[hover=true]:!duration-[500ms] data-[hover=true]:bg-primary-800 data-[hover=true]:border-primary-800'
},
variant: {
ghost:
'dark:text-foreground data-[hover=true]:before:bg-primary-800 before:bg-background'
}
},
defaultVariants: {
color: 'primary',
size: 'lg'
}
});
export default Button;
export { default } from './Button';
export * from './Button';
import {
CheckboxProps,
extendVariants,
Checkbox as HeroUICheckBox
} from '@heroui/react';
interface ICheckBoxProps extends CheckboxProps {}
const HeroCheckBox = extendVariants(HeroUICheckBox, {
defaultVariants: {
size: 'lg',
radius: 'none',
color: 'primary'
}
});
const CheckBox: React.FC<ICheckBoxProps> = ({ children, ...rest }) => {
return (
<HeroCheckBox
{...rest}
ref={(rest.ref as any) || null}
classNames={{
label: 'text-sm ',
icon: 'text-white bg-primary',
wrapper: `before:!border-[1px] before:border-black dark:before:border-white before:!rounded-[5px] !overflow-hidden
!rounded-[5px] after:bg-primary
${rest.isInvalid && 'before:!border-danger'} ${rest.classNames?.wrapper}`,
hiddenInput: 'overflow-hidden',
base: 'flex items-end'
}}>
{children}
</HeroCheckBox>
);
};
export default CheckBox;
import type { Meta, StoryObj } from '@storybook/react';
import CheckBoxComponent from './CheckBox';
const meta: Meta<typeof CheckBoxComponent> = {
title: 'Base/CheckBox',
component: CheckBoxComponent,
tags: ['autodocs']
};
export default meta;
type Story = StoryObj<typeof meta>;
export const CheckBox: Story = {
render: () => (
<CheckBoxComponent>
I have read and understood the Privacy Policy
</CheckBoxComponent>
)
};
export { default } from './CheckBox';
export * from './CheckBox';
import type { Meta, StoryObj } from '@storybook/react';
import IconComponent from './Icon';
import type { IconProps } from './Icon';
const meta: Meta<typeof IconComponent> = {
title: 'Base/Icon',
component: IconComponent,
tags: ['autodocs']
};
export default meta;
type Story = StoryObj<typeof meta>;
const icons: Array<IconProps['name']> = [
'global',
'facebook',
'instagram',
'x',
'tiktok',
'close',
'linkedin',
'menu',
'search',
'youtube',
'chat',
'up-arrow',
'down-arrow',
'left-arrow',
'right-arrow',
'phone',
'email',
'scroll',
'location',
'play',
'long-left-arrow',
'long-right-arrow',
'chevron-down',
'chevron-up',
'small-right-arrow',
'calendar',
'book',
'tick',
'form',
'tower',
'area',
'office',
'email-light',
'phone-light',
'location-pin',
'residence',
'store',
'download',
'forward-arrow',
'basement'
];
export const Icon: Story = {
render: () => (
<div className="flex gap-5 text-lg text-center flex-wrap">
{icons.map((name) => (
<div key={name}>
<IconComponent name={name} />
<p className="text-sm">{name}</p>
</div>
))}
</div>
)
};
import cn from '@/lib/merge-clsx';
import styles from './styles.module.css';
export interface IconProps extends React.HTMLAttributes<HTMLElement> {
name:
| 'global'
| 'facebook'
| 'instagram'
| 'x'
| 'tiktok'
| 'close'
| 'linkedin'
| 'menu'
| 'search'
| 'youtube'
| 'chat'
| 'up-arrow'
| 'down-arrow'
| 'left-arrow'
| 'right-arrow'
| 'phone'
| 'email'
| 'scroll'
| 'location'
| 'play'
| 'long-left-arrow'
| 'long-right-arrow'
| 'chevron-down'
| 'chevron-up'
| 'small-right-arrow'
| 'calendar'
| 'book'
| 'tick'
| 'form'
| 'tower'
| 'area'
| 'office'
| 'email-light'
| 'location-pin'
| 'phone-light'
| 'residence'
| 'store'
| 'download'
| 'forward-arrow'
| 'basement';
}
const Icon: React.FC<IconProps> = ({ name, className, ...props }) => {
const classNames = cn(styles.icon, {
[styles[`icon-${name}`]]: styles[`icon-${name}`],
[className as string]: className
});
return <i className={classNames} {...props}></i>;
};
export default Icon;
export { default } from './Icon';
export * from './Icon';
@font-face {
font-family: 'icomoon';
src: url('./font/icomoon.woff') format('woff');
font-weight: normal;
font-style: normal;
font-display: block;
}
.icon {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'icomoon' !important;
speak: never;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-forward-arrow:before {
content: '\e920';
}
.icon-residence:before {
content: '\e91c';
}
.icon-store:before {
content: '\e91d';
}
.icon-download:before {
content: '\e91e';
}
.icon-basement:before {
content: '\e91f';
}
.icon-email-light:before {
content: '\e917';
}
.icon-location-pin:before {
content: '\e918';
}
.icon-phone-light:before {
content: '\e919';
}
.icon-tower:before {
content: '\e900';
}
.icon-area:before {
content: '\e902';
}
.icon-office:before {
content: '\e910';
}
.icon-long-right-arrow:before {
content: '\e91a';
}
.icon-location:before {
content: '\e915';
}
.icon-play:before {
content: '\e916';
}
.icon-chat:before {
content: '\e901';
}
.icon-email:before {
content: '\e904';
}
.icon-left-arrow:before {
content: '\e903';
}
.icon-phone:before {
content: '\e905';
}
.icon-right-arrow:before {
content: '\e906';
}
.icon-down-arrow:before {
content: '\e913';
}
.icon-up-arrow:before {
content: '\e914';
}
.icon-menu:before {
content: '\e912';
}
.icon-scroll:before {
content: '\e911';
}
.icon-close:before {
content: '\e90f';
}
.icon-facebook:before {
content: '\e90e';
}
.icon-global:before {
content: '\e909';
}
.icon-instagram:before {
content: '\e907';
}
.icon-linkedin:before {
content: '\e908';
}
.icon-search:before {
content: '\e90a';
}
.icon-tiktok:before {
content: '\e90b';
}
.icon-x:before {
content: '\e90c';
}
.icon-youtube:before {
content: '\e90d';
}
.icon-long-left-arrow:before {
content: '\e91b';
}
.icon-form:before {
content: '\e937';
}
.icon-tick:before {
content: '\e938';
}
.icon-book:before {
content: '\e931';
}
.icon-calendar:before {
content: '\e932';
}
.icon-small-right-arrow:before {
content: '\e934';
}
.icon-chevron-up:before {
content: '\e92f';
}
.icon-chevron-down:before {
content: '\e930';
}
.icon-navigation:before {
content: '\e92a';
}
.icon-share:before {
content: '\e925';
}
import type { Meta, StoryObj } from '@storybook/react';
import PhoneInputComponent from './PhoneInput';
const meta: Meta<typeof PhoneInputComponent> = {
title: 'Base/PhoneInput',
component: PhoneInputComponent,
tags: ['autodocs']
};
export default meta;
type Story = StoryObj<typeof meta>;
export const PhoneInput: Story = {
args: {
isInValid: false,
label: 'Mobile'
}
};
import Input, { PhoneInputProps } from 'react-phone-input-2';
import cn from '@/lib/merge-clsx';
import 'react-phone-input-2/lib/high-res.css';
import styles from './style.module.css';
interface IPhoneInputProps extends PhoneInputProps {
label: string;
name: string;
isInValid: boolean;
}
const PhoneInput: React.FC<IPhoneInputProps> = ({ label, name, ...props }) => {
return (
<div>
<label
htmlFor={name}
className={cn(styles.label, {
[styles.labelInvalid]: props.isInValid
})}>
{label}
</label>
<Input
{...props}
containerClass={cn(
styles.container,
props.isInValid && styles.containerInvalid
)}
inputClass={styles.input}
buttonClass={styles.button}
dropdownClass={styles.dropdown}
searchClass={styles.search}
/>
</div>
);
};
export default PhoneInput;
export { default } from './PhoneInput';
export * from './PhoneInput';
.label {
@apply text-primary-500 opacity-60 text-lg ps-1;
:global(.dark) & {
@apply text-white;
}
}
.labelInvalid {
@apply !text-danger;
}
.container {
@apply w-full !bg-transparent pb-1.5 border-b-1 border-primary-200 relative;
:global(.dark) & {
@apply border-white after:bg-white;
}
}
.container::after {
content: '';
@apply absolute bottom-0 left-1/2 w-0 h-[1px] bg-black origin-center transition-all duration-300;
}
.container:focus-within::after {
@apply w-full -left-0;
}
.container :global(.flag-dropdown) {
@apply !border-b-[1px] !border-primary-200 hover:!border-primary-800;
:global(.dark) & {
@apply !border-white;
}
}
.container :global(.form-control:focus) {
@apply !border-primary-800 !border-b-[1px];
:global(.dark) & {
@apply !border-white;
}
}
.containerInvalid :global(.form-control:focus) {
@apply !border-b-[2px] !border-b-danger;
}
.containerInvalid {
@apply !border-danger-500 hover:!border-danger-500;
}
.containerInvalid::after {
@apply bg-danger;
}
.input {
@apply !w-full !border-none !bg-transparent !rounded-none !font-sans !text-base;
}
.button {
@apply !pb-1.5 !border-none !bg-transparent;
}
.button :global(.selected-flag) {
@apply !bg-transparent;
}
.dropdown {
@apply !z-10 font-sans;
:global(.dark) & {
@apply !bg-black !text-white;
}
}
.dropdown :global(li) {
:global(.dark) & {
@apply hover:!text-black;
}
}
.dropdown :global(li.highlight) {
:global(.dark) & {
@apply !text-black;
}
}
.search {
@apply !bg-white !px-2;
:global(.dark) & {
@apply !bg-black;
}
}
.search :global(span) {
@apply !hidden;
}
.search :global(input) {
@apply !w-full !ml-0;
:global(.dark) & {
@apply !text-white hover:!text-white focus:!text-white;
}
}
.dropdown :global(li.no-entries-message) {
:global(.dark) & {
@apply hover:!text-white !text-white;
}
}
import type { Meta, StoryObj } from '@storybook/react';
import SelectComponent, { SelectItem } from './';
const meta: Meta<typeof SelectComponent> = {
title: 'Base/Select',
component: SelectComponent,
tags: ['autodocs']
};
export default meta;
type Story = StoryObj<typeof meta>;
const items = [
{
label: 'Option 1',
value: 'option1'
},
{
label: 'Option 2',
value: 'option2'
}
];
export const Select: Story = {
render: () => (
<div>
<SelectComponent items={items} label="Select">
{items.map((item) => (
<SelectItem key={item.value} textValue={item.value}>
{item.label}
</SelectItem>
))}
</SelectComponent>
<SelectComponent items={items} label="Select with error" isInvalid>
{items.map((item) => (
<SelectItem key={item.value} textValue={item.value}>
{item.label}
</SelectItem>
))}
</SelectComponent>
</div>
)
};
import {
extendVariants,
Select as NextUiSelect,
SelectProps
} from '@heroui/react';
interface ISelectProps extends SelectProps {}
const NextSelect = extendVariants(NextUiSelect, {
defaultVariants: {
variant: 'underlined'
}
});
const Select: React.FC<ISelectProps> = ({ children, ...rest }) => {
return (
<NextSelect
{...rest}
ref={(rest.ref as any) || null}
classNames={{
...rest.classNames,
trigger: [
'border-b-1 border-primary-200 hover:border-primary-800',
'dark:border-white dark:hover:border-white',
'after:bottom-0 after:h-[1px] after:bg-primary-800 dark:after:bg-white',
rest.isInvalid
? 'after:!bg-danger !border-b-danger hover:!border-danger'
: '',
rest.classNames?.trigger
].join(' '),
label: [
'text-lg text-primary-500 dark:text-white opacity-60 mt-2',
'line-clamp-1 pr-6 text-left',
rest.classNames?.label
].join(' '),
selectorIcon: [
'-mr-4',
'stoke-primary-100 storke-white',
'size-8 mt-3',
'stroke-1',
rest.isInvalid ? 'text-danger' : ' text-primary-300 dark:text-white',
rest.classNames?.selectorIcon
].join(' '),
value: ['text-medium', rest.classNames?.value].join(' '),
listbox: ['p-0', rest.classNames?.listbox].join(' '),
popoverContent: [
'p-0 rounded-none',
rest.classNames?.popoverContent
].join(' ')
}}>
{children}
</NextSelect>
);
};
export default Select;
export { default } from './Select';
export * from '@heroui/select';
export * from './Select';
import type { Meta, StoryObj } from '@storybook/react';
import TextFieldComponent from './TextField';
const meta: Meta<typeof TextFieldComponent> = {
title: 'Base/TextField',
component: TextFieldComponent,
tags: ['autodocs']
};
export default meta;
type Story = StoryObj<typeof meta>;
export const TextField: Story = {
render: () => (
<div>
<TextFieldComponent label="Email" type="email" />
<TextFieldComponent label="Email with error" type="email" isInvalid />
</div>
)
};
import { extendVariants, Input, InputProps } from '@heroui/react';
interface ITextFieldsProps extends InputProps {}
const NextInput = extendVariants(Input, {
defaultVariants: {
variant: 'underlined'
}
});
const TextField: React.FC<ITextFieldsProps> = ({ ...rest }) => {
return (
<NextInput
{...rest}
ref={(rest.ref as any) || null}
classNames={{
...rest.classNames,
inputWrapper: `
border-b-1 border-primary-200 hover:border-primary-800
dark:border-white dark:hover:border-white
after:bottom-0 after:h-[1px] after:bg-primary-800 dark:after:bg-white
${rest.isInvalid ? 'after:!bg-danger !border-b-danger hover:!border-danger' : ''}
${rest.classNames?.helperWrapper}
`,
label: `
text-lg text-primary-500 opacity-60 dark:text-white
${rest.classNames?.label}
`,
errorMessage: 'text-danger',
input: `
text-medium ${rest.classNames?.input}
`
}}
/>
);
};
export default TextField;
export * from './TextField';
export { default } from './TextField';
import ContactForm from '.';
import submitForm from '@/api/submitForm';
const AsyncContactForm = async (props: IContactForm) => {
return <ContactForm {...props} onSubmit={submitForm} />;
};
export default AsyncContactForm;
import type { Meta, StoryObj } from '@storybook/react';
import ContactFormComponent from './ContactForm';
import labels from '@/mocks/labels';
const meta: Meta<typeof ContactFormComponent> = {
title: 'Form/ContactForm',
component: ContactFormComponent,
parameters: {
layout: 'fullscreen'
},
tags: ['autodocs']
};
export default meta;
type Story = StoryObj<typeof meta>;
const titleOptions = [
{ label: 'Mr.', value: 'Mr' },
{ label: 'Mrs.', value: 'Mrs' },
{ label: 'Ms.', value: 'Ms' }
];
const budgetOptions = [
{ label: 'From 15-20M', value: 'From 15-20M' },
{ label: 'From 20.1M to 30M', value: 'From 20.1M to 30M' },
{ label: 'From 30.1M to 50M', value: 'From 30.1M to 50M' },
{ label: '50M and above', value: '50M and above' }
];
const unitTypeOptions = [
{ label: 'Office', value: 'Office' },
{ label: 'Retail', value: 'Retail' },
{ label: 'F&B', value: 'F&B' }
];
const unitSizeOptions = [
{ label: 'Less than 100M2', value: 'Less than 100M2' },
{ label: '100M2 - 200M2', value: '100M2 - 200M2' },
{ label: '200M2 - 300M2', value: '200M2 - 300M2' },
{ label: '300M2 and above', value: '300M2 and above' }
];
const purposeOptions = [
{ label: 'Investment', value: 'Investment' },
{ label: 'Personal', value: 'Personal' }
];
export const ContactForm: Story = {
args: {
title: 'Register your Interest',
footerText:
'Fields that contain * are mandatory fields and must be filled before submitting the form.',
unitTypeOptions,
unitSizeOptions,
purposeOptions,
titleOptions,
budgetOptions,
labels: labels.form
}
};
export { default } from './ContactForm';
export { default as AsyncContactForm } from './AsyncContactForm';
export * from './ContactForm';
import cn from '@/lib/merge-clsx';
interface IContainer extends React.HTMLAttributes<HTMLElement> {
as?: React.ElementType;
className?: string;
}
const Container: React.FC<IContainer> = (props) => {
const { as, className, ...rest } = props;
const Component = as || 'div';
return (
<Component
{...rest}
className={cn('mx-auto max-w-screen-2xl px-4 sm:px-8', className)}
/>
);
};
export default Container;
export { default } from './Container';
import type { Meta, StoryObj } from '@storybook/react';
import FooterComponent from '.';
const meta: Meta<typeof FooterComponent> = {
title: 'Layout/Footer',
component: FooterComponent,
tags: ['autodocs'],
parameters: {
layout: 'fullscreen'
}
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Footer: Story = {
args: {
socialPlatforms: [
{ url: '/', label: 'instagram', icon: 'instagram' },
{ url: '/', label: 'facebook', icon: 'facebook' }
],
policyLinks: [
{ url: '/', label: 'Privacy Center' },
{ url: '/', label: 'Anti-Fraud Disclaimer' },
{ url: '/', label: 'Responsible Disclosure Policy' },
{ url: '/', label: 'Terms and Conditions' },
{ url: '/', label: 'Cookie Settings' }
],
linkGroups: [
{
title: 'Neighbour hoods',
links: [
{ url: '/', label: 'Lacina' },
{ url: '/', label: 'Serra' },
{ url: '/', label: 'Cilia' },
{ url: '/', label: 'Capria' }
]
},
{
title: 'Discover Junction',
links: [
{ url: '/', label: 'Download Brochure' },
{ url: '/', label: 'Register Your Interest' }
]
},
{
title: 'Majid Al-Futtaim Communities',
links: [
{ url: '/', label: 'Tilal Al Ghaf' },
{ url: '/', label: 'Al Zahia' },
{ url: '/', label: 'Al Mouj' },
{ url: '/', label: 'Water Front City' }
]
}
],
labels: {
copyrightText: '© 2025 MAF Egypt Business Park. All Rights Reserved.',
poweredByText: 'Launching Soon',
socialLinkText: 'Stay in touch with us',
topText: 'TOP'
}
}
};
import Image from 'next/image';
import Container from '@/components/layout/Container';
import SocialLinks from '@/components/shared/SocialLinks';
import mafLogo from '@/assets/img/footer-logo.png';
import logo from '@/assets/img/logo.png';
import React from 'react';
export interface IFooter {
socialPlatforms?: ISocialLink[];
policyLinks?: ILink[];
linkGroups?: {
title?: string;
links: ILink[];
}[];
labels: {
copyrightText: string;
socialLinkText: string;
poweredByText: string;
topText: string;
};
}
const Footer: React.FC<IFooter> = ({
socialPlatforms,
policyLinks,
linkGroups,
labels
}) => {
return (
<footer className="bg-default-900 font-light text-content1">
<Container className="grid grid-cols-1 md:grid-cols-12 text-content1 py-8">
<div className="md:col-span-4">
<Image src={logo} alt="MAF Junction" className="w-28 mb-6" />
{socialPlatforms && (
<div className="mb-6 w-56">
<h3 className="mb-4 font-light text-4xl text-white">
{labels.socialLinkText}
</h3>
<SocialLinks
socialPlatforms={socialPlatforms}
className="md:flex-nowrap"
/>
</div>
)}
</div>
<div className="grid grid-cols-1 md:gap-4 md:col-span-8 md:grid-cols-3 md:px-8">
{linkGroups?.map((group) => (
<div key={group.title} className="mt-4 md:mt-0">
<h3 className="mb-2 md:mb-4 font-serif font-light text-white">
{group.title?.toUpperCase()}
</h3>
{group?.links.map((link: ILink, i) => (
<React.Fragment key={i}>
<div className="mt-2">
<a href={link.url}>{link.label}</a>
</div>
</React.Fragment>
))}
</div>
))}
</div>
<div className="flex flex-wrap md:col-span-4 text-sm">
{policyLinks?.map((link, i) => (
<a
key={link.label}
href={link.url}
className="text-[#AEAEAE] text-[1.05em] px-1 my-1 border-s-1 first:border-s-0 first:ps-0 leading-3">
{link.label}
</a>
))}
</div>
</Container>
<div className="py-6 px-4 md:px-0 bg-default-800">
<Container>
<div className="flex items-center justify-between flex-wrap sm:flex-nowrap gap-8">
<div className="grow">
<div className="font-sans text-xs mb-2">
{labels.poweredByText}
</div>
<Image
src={mafLogo}
alt="Majid Al Futtaim"
className="w-[12.5rem]"
/>
</div>
<div className="text-sm text-right">{labels.copyrightText}</div>
</div>
</Container>
</div>
</footer>
);
};
export default Footer;
export { default } from './Footer';
export * from './Footer';
import getHeader from '@/api/getHeader';
import Header from './Header';
const AsyncHeader = async () => {
const header = await getHeader();
console.log('header', header);
return <Header {...header} />;
};
export default AsyncHeader;
import type { Meta, StoryObj } from '@storybook/react';
import HeaderComponent from './Header';
import Image from 'next/image';
const meta: Meta<typeof HeaderComponent> = {
title: 'Layout/Header',
component: HeaderComponent,
parameters: {
layout: 'fullscreen'
},
tags: ['autodocs'],
decorators: [
(Story: React.ComponentType) => (
<div>
<Story />
<Image
src="https://picsum.photos/1920/1920"
alt="background"
className="w-full"
/>
</div>
)
]
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Header: Story = {
args: {
button: {
label: 'Register your Interest',
url: '/'
},
menuItems: [
{
label: 'Launch Event',
url: '#'
},
{
label: 'Brokers Briefing',
url: '#'
}
]
}
};
'use client';
import React from 'react';
import Button from '@/components/base/Button';
import Image from 'next/image';
import Link from 'next/link';
import {
Navbar,
NavbarMenuToggle,
NavbarMenu,
NavbarMenuItem,
NavbarContent,
NavbarItem,
NavbarBrand
} from '@heroui/navbar';
import logo from '@/assets/img/logo.png';
const Header: React.FC<IHeader> = ({ button, menuItems }) => {
const [isMenuOpen, setIsMenuOpen] = React.useState(false);
console.log('button', button);
const handleMenuClose = () => {
setIsMenuOpen(false);
};
const renderMenuItems = () =>
menuItems?.map(({ label, url }) => (
<NavbarMenuItem key={label} className="text-sm text-background">
<Link href={url} onClick={handleMenuClose}>
{label}
</Link>
<div className="sm:hidden w-full border-b-[0.1px] border-default-500 pt-5" />
</NavbarMenuItem>
));
const renderButton = () =>
button && (
<NavbarMenuItem>
<Button
as={Link}
size="md"
className="text-sm md:px-10"
color="default"
href={button.url}
onPress={handleMenuClose}>
{button.label}
</Button>
</NavbarMenuItem>
);
return (
<React.Fragment>
<div className="h-[4.5rem] bg-default-900" />
<header className="fixed top-0 w-full bg-default-900 bg-opacity-80 backdrop-blur-md z-50">
<Navbar
isBlurred={false}
maxWidth="2xl"
className="border-b-1 border-b-default-700"
classNames={{
base: 'bg-transparent py-1',
wrapper: 'px-2',
menuItem: 'pt-5 sm:pt-0',
menu: 'bg-default-900'
}}
isMenuOpen={isMenuOpen}
onMenuOpenChange={setIsMenuOpen}>
<NavbarBrand>
<Link href="/">
<Image priority src={logo} alt="Maf Junction" className="w-32" />
</Link>
</NavbarBrand>
{/* Mobile */}
<NavbarContent justify="end" className="sm:hidden">
<NavbarItem>
<NavbarMenu>
{renderMenuItems()} {renderButton()}
</NavbarMenu>
</NavbarItem>
</NavbarContent>
<NavbarMenuToggle className="sm:hidden text-background" />
{/* Desktop */}
<NavbarContent
className="hidden sm:flex gap-4 text-background"
justify="center">
{renderMenuItems()} {renderButton()}
</NavbarContent>
</Navbar>
</header>
</React.Fragment>
);
};
export default Header;
export { default } from './Header';
export * from './Header';
import type { Meta, StoryObj } from '@storybook/react';
import PageNotFoundComponent from './PageNotFound';
const meta: Meta<typeof PageNotFoundComponent> = {
title: 'Layout/Page Not Found',
component: PageNotFoundComponent,
parameters: {
layout: 'centered'
}
};
export default meta;
type Story = StoryObj<typeof meta>;
export const PageNotFound: Story = {};
import Image from 'next/image';
import { Button } from "@heroui/button";
import FourOhFour from './img/404.png';
export default function PageNotFound() {
return (
<div className="flex flex-col justify-center items-center">
<Image src={FourOhFour} alt="404, Page Not Found!" />
<Button href="/" color="primary" size="md">
Back to Home
</Button>
</div>
);
}
export { default } from './PageNotFound';
@use '@styles/vars.scss';
.notFound {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10vh 1em;
box-sizing: border-box;
h2 {
font-size: 10em;
font-weight: 100;
line-height: 0.8;
margin: 0;
color: #1ea7fd;
}
a {
margin-top: 2em;
}
}
@include vars.mQuery(xl) {
.notFound {
font-size: 0.9em;
}
}
import MasterPlan from '@/components/top-level/MasterPlan';
import VideoContent from '@/components/top-level/VideoContent';
import Banner from '@/components/top-level/Banner';
import AddressMap from '@/components/top-level/AddressMap';
import AmenitiesCarousel from '@/components/top-level/AmenitiesCarousel';
import Gallery from '@/components/top-level/Gallery';
import ImageCarousel from '@/components/top-level/ImageCarousel';
import { AsyncContactForm } from '@/components/form/ContactForm';
const PageRenderer = ({ components, prefix }: IPageRenderer) => {
return components.map(({ component, ...props }, index) => {
const key = `${prefix}-${index}`;
switch (component) {
case 'master-plan':
return <MasterPlan key={key} {...(props as IMasterPlan)} />;
case 'video-content':
return <VideoContent key={key} {...(props as IVideoContent)} />;
case 'banner':
return <Banner key={key} {...(props as IBanner)} />;
case 'address-map':
return <AddressMap key={key} {...(props as IAddressMap)} />;
case 'contact':
return <AsyncContactForm key={key} {...(props as IContactForm)} />;
case 'amenities-carousel':
return (
<AmenitiesCarousel key={key} {...(props as IAmenitiesCarousel)} />
);
case 'gallery':
return <Gallery key={key} {...(props as IGallery)} />;
case 'image-carousel':
return <ImageCarousel key={key} {...(props as IImageCarousel)} />;
default:
return null;
}
});
};
export default PageRenderer;
export { default } from './PageRenderer';
export * from './PageRenderer';
'use client';
import { useRouter } from 'next/navigation';
import { HeroUIProvider } from '@heroui/react';
import { ParallaxProvider } from 'react-scroll-parallax';
type ProvidersProps = {
children: React.ReactNode;
};
const Providers: React.FC<ProvidersProps> = ({ children }) => {
const router = useRouter();
return (
<HeroUIProvider navigate={router.push}>
<ParallaxProvider>{children}</ParallaxProvider>
</HeroUIProvider>
);
};
export default Providers;
export { default } from './Providers';
'use client';
import { ReactNode, useEffect, useRef } from 'react';
import { ReactLenis } from 'lenis/react';
interface ISmoothScroll {
children: ReactNode;
}
export default function SmoothScroll({ children }: ISmoothScroll) {
return (
<ReactLenis
root
options={{
duration: 1.2,
easing: (t: number) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
wheelMultiplier: 0.8,
touchMultiplier: 0.8,
infinite: false
}}>
{children}
</ReactLenis>
);
}
export { default } from './SmoothScroll';
import ReactMarkdown, { Options } from 'react-markdown';
import rehypeRaw from 'rehype-raw';
interface RenderMarkdownProps extends React.HTMLAttributes<HTMLDivElement> {
children: string;
}
/** component that render markdown & plain html */
const RenderMarkdown: React.FC<RenderMarkdownProps & Options> = (props) => {
return <ReactMarkdown {...props} rehypePlugins={[rehypeRaw]} />;
};
export default RenderMarkdown;
export { default } from './RenderMarkdown';
import ResponsiveImage from './ResponsiveImage';
import type { ResponsiveImageProps } from './ResponsiveImage';
export default {
title: 'Shared/ResponsiveImage',
component: ResponsiveImage,
parameters: {
layout: 'fullscreen'
},
tags: ['autodocs']
};
export const Image = {
args: {
src: 'https://picsum.photos/1920/570?jpg',
width: 1920,
height: 570,
sizes: '100vw'
}
};
export const ArtDirection = {
args: {
src: 'https://picsum.photos/576/570?jpg',
width: 576,
height: 540,
sizes: '100vw'
},
render: (args: ResponsiveImageProps) => (
<ResponsiveImage {...args}>
<ResponsiveImage.Source
media="xxl"
src="https://picsum.photos/1920/540?jpg"
width={1920}
height={540}
sizes="100vw"
/>
<ResponsiveImage.Source
media="xl"
src="https://picsum.photos/1536/540?jpg"
width={1536}
height={540}
sizes="100vw"
/>
<ResponsiveImage.Source
media="lg"
src="https://picsum.photos/1280/540?jpg"
width={1280}
height={540}
sizes="100vw"
/>
<ResponsiveImage.Source
media="md"
src="https://picsum.photos/1024/540?jpg"
width={1024}
height={540}
sizes="100vw"
/>
<ResponsiveImage.Source
media="sm"
src="https://picsum.photos/768/540?jpg"
width={768}
height={540}
sizes="100vw"
/>
<ResponsiveImage.Source
media="(min-width: 576px)"
src="https://picsum.photos/640/540?jpg"
width={640}
height={540}
sizes="100vw"
/>
</ResponsiveImage>
)
};
import { memo } from 'react';
import React from 'react';
import { getImageProps } from 'next/image';
import breakpoints from './breakpoints';
export type Image = {
url: string;
width: number;
height: number;
alternativeText?: string;
};
export interface ResponsiveImageProps
extends React.HTMLAttributes<HTMLImageElement> {
src: string;
width: number;
height: number;
sizes: string;
alt?: string;
priority?: boolean;
quality?: number;
children?: React.ReactNode;
}
export interface SourceProps extends React.HTMLAttributes<HTMLSourceElement> {
src: string;
width: number;
height: number;
media: string;
sizes: string;
quality?: number;
}
const ResponsiveImage: React.FC<ResponsiveImageProps> & {
Source: React.FC<SourceProps>;
} = ({
src,
width,
height,
alt,
sizes,
quality,
priority,
children,
...rest
}) => {
if (src) {
const { props } = getImageProps({
src,
width,
height,
sizes,
quality,
priority,
alt: alt || ''
});
return (
<picture>
{children}
<img {...props} {...rest} alt={alt || ''} />
</picture>
);
}
};
ResponsiveImage.Source = memo(
({ src, width, height, sizes, media, quality, ...rest }: SourceProps) => {
if (src) {
const query = breakpoints[media] || media;
const { props } = getImageProps({
width,
height,
src,
sizes,
quality,
alt: ''
});
return (
<source
media={query}
src={props.src}
srcSet={props.srcSet}
width={props.width}
height={props.height}
sizes={props.sizes}
{...rest}
/>
);
}
}
);
ResponsiveImage.displayName = 'ResponsiveImage';
ResponsiveImage.Source.displayName = 'ResponsiveImageSource';
export default ResponsiveImage;
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment