feat: initialize mohfarawati app foundation
CI / quality (push) Waiting to run

This commit is contained in:
MOH
2026-03-06 13:53:57 +01:00
parent 30330c4f1e
commit f86b7f39f7
50 changed files with 4615 additions and 152 deletions
+38
View File
@@ -0,0 +1,38 @@
import Link from "next/link";
import { useLocale, useTranslations } from "next-intl";
const navItems = [
{ key: "home", path: "" },
{ key: "portfolio", path: "/portfolio" },
{ key: "products", path: "/products" },
{ key: "about", path: "/about" },
{ key: "contact", path: "/contact" },
{ key: "root", path: "/root" },
];
export function Footer() {
const locale = useLocale();
const tNav = useTranslations("navigation");
const tFooter = useTranslations("footer");
return (
<footer className="border-t border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-950">
<div className="mx-auto flex w-full max-w-6xl flex-col gap-4 px-4 py-8 sm:px-6 lg:px-8">
<nav className="flex flex-wrap gap-4 text-sm">
{navItems.map((item) => (
<Link
key={item.key}
href={`/${locale}${item.path}`}
className="text-zinc-600 transition hover:text-zinc-900 dark:text-zinc-300 dark:hover:text-zinc-100"
>
{tNav(item.key)}
</Link>
))}
</nav>
<p className="text-xs text-zinc-500 dark:text-zinc-400">
{tFooter("copyright", { year: new Date().getFullYear() })}
</p>
</div>
</footer>
);
}
+26
View File
@@ -0,0 +1,26 @@
"use client";
import { motion } from "framer-motion";
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
type MotionFadeProps = {
children: ReactNode;
className?: string;
delay?: number;
};
export function MotionFade({ children, className, delay = 0 }: MotionFadeProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.2 }}
transition={{ duration: 0.45, delay, ease: "easeOut" }}
className={cn(className)}
>
{children}
</motion.div>
);
}
+110
View File
@@ -0,0 +1,110 @@
"use client";
import { Menu, X } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useLocale, useTranslations } from "next-intl";
import { useState } from "react";
import { ThemeToggle } from "@/components/theme-toggle";
const navItems = [
{ key: "home", path: "" },
{ key: "portfolio", path: "/portfolio" },
{ key: "products", path: "/products" },
{ key: "about", path: "/about" },
{ key: "contact", path: "/contact" },
{ key: "root", path: "/root" },
];
export function Navbar() {
const [isOpen, setIsOpen] = useState(false);
const locale = useLocale();
const t = useTranslations("navigation");
return (
<header className="sticky top-0 z-40 border-b border-zinc-200 bg-white/95 backdrop-blur dark:border-zinc-800 dark:bg-zinc-950/95">
<div className="mx-auto flex w-full max-w-6xl items-center justify-between gap-4 px-4 py-3 sm:px-6 lg:px-8">
<div className="flex items-center gap-3">
<Link
href={`/${locale}`}
className="inline-flex items-center rounded-md border border-zinc-300 bg-white px-3 py-2 dark:border-zinc-700 dark:bg-zinc-950"
>
<Image
src="/logos/light-primary.svg"
alt="mohfarawati logo"
width={140}
height={24}
className="block h-6 w-auto dark:hidden"
priority
/>
<Image
src="/logos/dark-primary.svg"
alt="mohfarawati logo"
width={140}
height={24}
className="hidden h-6 w-auto dark:block"
priority
/>
</Link>
<nav className="hidden items-center gap-5 md:flex">
{navItems.map((item) => (
<Link
key={item.key}
href={`/${locale}${item.path}`}
className="text-sm text-zinc-700 transition hover:text-zinc-900 dark:text-zinc-300 dark:hover:text-zinc-100"
>
{t(item.key)}
</Link>
))}
</nav>
</div>
<div className="hidden items-center gap-2 md:flex">
<ThemeToggle />
<button
type="button"
className="rounded-md border border-zinc-300 px-3 py-2 text-xs font-medium text-zinc-600 dark:border-zinc-700 dark:text-zinc-300"
>
{t("languagePlaceholder")}
</button>
</div>
<button
type="button"
onClick={() => setIsOpen((open) => !open)}
className="inline-flex h-9 w-9 items-center justify-center rounded-md border border-zinc-300 text-zinc-700 md:hidden dark:border-zinc-700 dark:text-zinc-200"
aria-label={isOpen ? t("closeMenu") : t("openMenu")}
>
{isOpen ? <X className="h-4 w-4" /> : <Menu className="h-4 w-4" />}
</button>
</div>
{isOpen ? (
<div className="border-t border-zinc-200 px-4 py-4 md:hidden dark:border-zinc-800">
<nav className="flex flex-col gap-3">
{navItems.map((item) => (
<Link
key={item.key}
href={`/${locale}${item.path}`}
className="text-sm text-zinc-700 dark:text-zinc-300"
onClick={() => setIsOpen(false)}
>
{t(item.key)}
</Link>
))}
</nav>
<div className="mt-4 flex items-center gap-2">
<ThemeToggle />
<button
type="button"
className="rounded-md border border-zinc-300 px-3 py-2 text-xs font-medium text-zinc-600 dark:border-zinc-700 dark:text-zinc-300"
>
{t("languagePlaceholder")}
</button>
</div>
</div>
) : null}
</header>
);
}
+7
View File
@@ -0,0 +1,7 @@
"use client";
import { ThemeProvider as NextThemesProvider, type ThemeProviderProps } from "next-themes";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
+40
View File
@@ -0,0 +1,40 @@
"use client";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
export function ThemeToggle() {
const { setTheme, theme, resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return (
<button
type="button"
className="inline-flex h-9 w-9 items-center justify-center rounded-md border border-zinc-300 text-zinc-600 dark:border-zinc-700 dark:text-zinc-200"
aria-label="Toggle theme"
>
<Moon className="h-4 w-4" />
</button>
);
}
const activeTheme = theme === "system" ? resolvedTheme : theme;
const isDark = activeTheme === "dark";
return (
<button
type="button"
onClick={() => setTheme(isDark ? "light" : "dark")}
className="inline-flex h-9 w-9 items-center justify-center rounded-md border border-zinc-300 text-zinc-700 transition hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800"
aria-label="Toggle theme"
>
{isDark ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</button>
);
}