diff --git a/README.md b/README.md
index 2bfd267..5fb8b88 100644
--- a/README.md
+++ b/README.md
@@ -6,20 +6,22 @@ Built using Next.js and Tailwind CSS.
1. Clone the repository:
-```
+```bash
git clone https://github.com/aftabrehan/zin-tools.git
+# or
+gh repo clone aftabrehan/zin-tools
```
2. Install the required dependencies:
```bash
-npm
+npm install
# or
-yarn
+yarn install
# or
-pnpm
+pnpm install
# or
-bun
+bun install
```
3. Run the development server:
@@ -36,7 +38,5 @@ bun dev
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
-This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
-
> [!NOTE]
> This project is created solely for test/portfolio purposes, and you are free to use it as you see fit.
diff --git a/app/layout.tsx b/app/layout.tsx
index 2fbdde6..2d4fe34 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,5 +1,9 @@
-import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
+import type { Metadata } from 'next'
+
+import { Header } from '@/components/header'
+import { Footer } from '@/components/footer'
+
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
@@ -16,7 +20,17 @@ export default function RootLayout({
}>) {
return (
-
{children}
+
+
+
+ {children}
+
+
+
+
)
}
diff --git a/app/not-found.tsx b/app/not-found.tsx
index 1bb3e46..3d91066 100644
--- a/app/not-found.tsx
+++ b/app/not-found.tsx
@@ -1,7 +1,7 @@
const NotFoundPage = () => (
-
- Not Found Page
-
+
)
export default NotFoundPage
diff --git a/app/page.tsx b/app/page.tsx
index a2e373d..e39a769 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,7 +1,7 @@
const Home = () => (
-
- My Clean App
-
+
)
export default Home
diff --git a/components/MobileNav.tsx b/components/MobileNav.tsx
new file mode 100644
index 0000000..f709de4
--- /dev/null
+++ b/components/MobileNav.tsx
@@ -0,0 +1,42 @@
+'use client'
+
+import { useState } from 'react'
+import Image from 'next/image'
+
+import { Modal } from '@/components//modal/modal'
+import { NavDropdown } from '@/components/nav-dropdown'
+
+import { ALL_NAV_ITEMS } from '@/constants/nav-items'
+
+export const MobileNav = () => {
+ const [isOpen, setIsOpen] = useState(false)
+
+ return (
+ <>
+ setIsOpen(!isOpen)}
+ style={{ background: isOpen ? '#000' : '#f4f4f5' }}
+ >
+
+
+
+ setIsOpen(false)}>
+
+
+
+ {ALL_NAV_ITEMS.map((navItem, i) => (
+
+ ))}
+
+
+
+
+ >
+ )
+}
diff --git a/components/button.tsx b/components/button.tsx
new file mode 100644
index 0000000..bc3a0b9
--- /dev/null
+++ b/components/button.tsx
@@ -0,0 +1,30 @@
+import clsx from 'clsx'
+
+interface buttonProps {
+ label: string
+ onClick?: () => void
+ variant?: 'primary' | 'secondary'
+}
+
+export const Button = ({
+ label,
+ onClick,
+ variant = 'primary',
+}: buttonProps) => {
+ const cls = {
+ primary: 'bg-primary text-white',
+ secondary: 'text-black hover:bg-zinc-100',
+ }[variant]
+
+ return (
+
+ {label}
+
+ )
+}
diff --git a/components/footer.tsx b/components/footer.tsx
new file mode 100644
index 0000000..303ecef
--- /dev/null
+++ b/components/footer.tsx
@@ -0,0 +1,6 @@
+export const Footer = () => (
+ // TODO: remove dummy height min-h-[600px]
+
+)
diff --git a/components/header.tsx b/components/header.tsx
new file mode 100644
index 0000000..d5b6ceb
--- /dev/null
+++ b/components/header.tsx
@@ -0,0 +1,39 @@
+import Link from 'next/link'
+import Image from 'next/image'
+
+import { Searchbar } from '@/components/search-bar'
+import { Button } from '@/components/button'
+import { Navbar } from '@/components/navbar'
+import { MobileNav } from '@/components/MobileNav'
+
+export const Header = () => (
+
+)
diff --git a/components/modal/modal-portal.tsx b/components/modal/modal-portal.tsx
new file mode 100644
index 0000000..b06cf88
--- /dev/null
+++ b/components/modal/modal-portal.tsx
@@ -0,0 +1,19 @@
+import { useRef, useEffect, useState } from 'react'
+import { createPortal } from 'react-dom'
+
+interface modalPortal {
+ children: React.ReactNode
+ selector?: string
+}
+
+export const ModalPortal = ({ children, selector = 'modal' }: modalPortal) => {
+ const ref = useRef(null)
+ const [mounted, setMounted] = useState(false)
+
+ useEffect(() => {
+ ref.current = document.querySelector(selector)
+ setMounted(true)
+ }, [selector])
+
+ return mounted && ref.current ? createPortal(children, ref.current) : null
+}
diff --git a/components/modal/modal.tsx b/components/modal/modal.tsx
new file mode 100644
index 0000000..3a54e07
--- /dev/null
+++ b/components/modal/modal.tsx
@@ -0,0 +1,41 @@
+import { useEffect, useRef } from 'react'
+import clsx from 'clsx'
+
+import { ModalPortal } from '@/components/modal/modal-portal'
+
+import { useClickOutside } from '@/hooks/useClickOutside'
+
+interface modalProps {
+ isOpen: boolean
+ close: () => void
+ children: React.ReactNode
+ closeOnClickAway?: boolean
+ className?: string
+}
+
+export const Modal = ({
+ isOpen,
+ close,
+ children,
+ closeOnClickAway = false,
+ className,
+}: modalProps) => {
+ const contentRef = useRef(null)
+
+ useClickOutside({
+ onClick: () => closeOnClickAway && close(),
+ ref: contentRef,
+ })
+
+ useEffect(() => {
+ if (!isOpen) return
+ }, [isOpen])
+
+ return isOpen ? (
+
+
+ {children}
+
+
+ ) : null
+}
diff --git a/components/nav-dropdown.tsx b/components/nav-dropdown.tsx
new file mode 100644
index 0000000..2b490cf
--- /dev/null
+++ b/components/nav-dropdown.tsx
@@ -0,0 +1,50 @@
+import Link from 'next/link'
+import clsx from 'clsx'
+
+interface navDropdownProps {
+ title: string
+ link: string
+ options: Array<{ label: string; link: string }>
+ position?: 'left' | 'right' | 'center'
+}
+
+export const NavDropdown = ({
+ title,
+ link,
+ options,
+ position = 'center',
+}: navDropdownProps) => {
+ const positionCls = {
+ left: 'left-0',
+ right: 'right-0',
+ center: '',
+ }[position]
+
+ return (
+
+
+ {title}
+
+
+
+ {options.map(({ label, link: opLink }, i) => (
+
+ {label}
+
+ ))}
+
+
+ )
+}
diff --git a/components/navbar.tsx b/components/navbar.tsx
new file mode 100644
index 0000000..f1734f9
--- /dev/null
+++ b/components/navbar.tsx
@@ -0,0 +1,45 @@
+import clsx from 'clsx'
+
+import { NavDropdown } from '@/components/nav-dropdown'
+
+import { ALL_NAV_ITEMS, LESS_NAV_ITEMS } from '@/constants/nav-items'
+
+interface navbarProps {
+ className?: string
+}
+
+export const Navbar = ({ className }: navbarProps) => (
+
+
+ {ALL_NAV_ITEMS.map((navItem, i) => (
+
+ ))}
+
+
+
+ {LESS_NAV_ITEMS.map((navItem, i) => (
+
+ ))}
+
+
+)
diff --git a/components/search-bar.tsx b/components/search-bar.tsx
new file mode 100644
index 0000000..5ac9388
--- /dev/null
+++ b/components/search-bar.tsx
@@ -0,0 +1,33 @@
+import Image from 'next/image'
+import clsx from 'clsx'
+
+interface searchbarProps {
+ className?: string
+}
+
+export const Searchbar = ({ className }: searchbarProps) => (
+
+)
diff --git a/constants/nav-items.ts b/constants/nav-items.ts
new file mode 100644
index 0000000..bb58600
--- /dev/null
+++ b/constants/nav-items.ts
@@ -0,0 +1,28 @@
+const placeholderOptions = [
+ { label: 'Stock Fotage', link: '/' },
+ { label: 'Motion Graphics', link: '/' },
+ { label: 'All Stock Videos', link: '/' },
+]
+
+const commonProps = { link: '/', options: placeholderOptions }
+
+export const ALL_NAV_ITEMS = [
+ { title: 'Stock Video', ...commonProps },
+ { title: 'Video Templates', ...commonProps },
+ { title: 'Music', ...commonProps },
+ { title: 'Sound Effects', ...commonProps },
+ { title: 'Graphic Templates', ...commonProps },
+ { title: 'Presentation Templates', ...commonProps },
+ { title: 'Graphics', ...commonProps },
+ { title: 'Photos', ...commonProps },
+ { title: 'More', ...commonProps },
+]
+
+export const LESS_NAV_ITEMS = [
+ { title: 'Stock Video', ...commonProps },
+ { title: 'Video Templates', ...commonProps },
+ { title: 'Music', ...commonProps },
+ { title: 'Sound Effects', ...commonProps },
+ { title: 'Graphic Templates', ...commonProps },
+ { title: 'More', ...commonProps },
+]
diff --git a/hooks/useClickOutside.ts b/hooks/useClickOutside.ts
new file mode 100644
index 0000000..7c2571d
--- /dev/null
+++ b/hooks/useClickOutside.ts
@@ -0,0 +1,16 @@
+import { useEffect } from 'react'
+
+interface funcProps {
+ onClick: () => void
+ ref: { current: HTMLDivElement } | { current: null }
+}
+
+export const useClickOutside = ({ onClick, ref }: funcProps) => {
+ useEffect(() => {
+ const handleClickOutside = (e: any) =>
+ ref.current && !ref.current.contains(e.target) && onClick()
+
+ document.addEventListener('mousedown', handleClickOutside)
+ return () => document.removeEventListener('mousedown', handleClickOutside)
+ }, [onClick, ref])
+}
diff --git a/package.json b/package.json
index 0856583..d1bf530 100644
--- a/package.json
+++ b/package.json
@@ -9,19 +9,20 @@
"lint": "next lint"
},
"dependencies": {
+ "clsx": "^2.1.0",
+ "next": "14.1.0",
"react": "^18",
- "react-dom": "^18",
- "next": "14.1.0"
+ "react-dom": "^18"
},
"devDependencies": {
- "typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
+ "eslint": "^8",
+ "eslint-config-next": "14.1.0",
"postcss": "^8",
"tailwindcss": "^3.3.0",
- "eslint": "^8",
- "eslint-config-next": "14.1.0"
+ "typescript": "^5"
}
}
diff --git a/public/cross.svg b/public/cross.svg
new file mode 100644
index 0000000..3939b2c
--- /dev/null
+++ b/public/cross.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/public/hamburger.svg b/public/hamburger.svg
new file mode 100644
index 0000000..5438956
--- /dev/null
+++ b/public/hamburger.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/public/search.svg b/public/search.svg
new file mode 100644
index 0000000..5c72e36
--- /dev/null
+++ b/public/search.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 7e4bd91..516c914 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -1,20 +1,21 @@
-import type { Config } from "tailwindcss";
+import type { Config } from 'tailwindcss'
const config: Config = {
content: [
- "./pages/**/*.{js,ts,jsx,tsx,mdx}",
- "./components/**/*.{js,ts,jsx,tsx,mdx}",
- "./app/**/*.{js,ts,jsx,tsx,mdx}",
+ './pages/**/*.{js,ts,jsx,tsx,mdx}',
+ './components/**/*.{js,ts,jsx,tsx,mdx}',
+ './app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
- backgroundImage: {
- "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
- "gradient-conic":
- "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
+ colors: {
+ primary: '#616DF4',
+ },
+ boxShadow: {
+ header: '0px 4px 12px 0px rgba(189, 189, 189, 0.25)',
},
},
},
plugins: [],
-};
-export default config;
+}
+export default config
diff --git a/yarn.lock b/yarn.lock
index 7236ed4..b540de3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -591,6 +591,11 @@ client-only@0.0.1:
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
+clsx@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb"
+ integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==
+
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
@@ -2242,6 +2247,7 @@ streamsearch@^1.1.0:
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
+ name string-width-cjs
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -2302,6 +2308,7 @@ string.prototype.trimstart@^1.0.7:
es-abstract "^1.22.1"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+ name strip-ansi-cjs
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==