Login and dashboard added
This commit is contained in:
32
components/auth/authCard.tsx
Normal file
32
components/auth/authCard.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import LoginForm from "./loginForm";
|
||||
import VerifyCode from "./verifyCode";
|
||||
import ForgotPassword from "./forgetPassword";
|
||||
import {Step} from "@/constants/types"
|
||||
|
||||
|
||||
|
||||
export default function AuthCard() {
|
||||
const [step, setStep] = useState<Step>("login");
|
||||
|
||||
return (
|
||||
<>
|
||||
{step === "login" && (
|
||||
<LoginForm
|
||||
onSuccess={() => setStep("verify")}
|
||||
onForgotPassword={() => setStep("forgot")}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step === "verify" && (
|
||||
<VerifyCode onBack={() => setStep("login")} />
|
||||
)}
|
||||
|
||||
{step === "forgot" && (
|
||||
<ForgotPassword onBack={() => setStep("login")} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
16
components/auth/authlayout.tsx
Normal file
16
components/auth/authlayout.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import LeftPanel from "./leftpanel";
|
||||
|
||||
interface AuthLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function AuthLayout({ children }: AuthLayoutProps) {
|
||||
return (
|
||||
<main className="min-h-screen grid lg:grid-cols-2 bg-[#081321]">
|
||||
<LeftPanel />
|
||||
<div className="flex items-center justify-center p-8">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
39
components/auth/forgetPassword.tsx
Normal file
39
components/auth/forgetPassword.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import FormInput from "@/utils/formInput";
|
||||
import Button from "@/utils/button";
|
||||
|
||||
|
||||
import {Props} from "@/constants/types"
|
||||
export default function ForgotPassword({ onBack }: Props) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="text-slate-400 hover:text-white"
|
||||
>
|
||||
← Back to sign in
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">
|
||||
Reset your password
|
||||
</h2>
|
||||
|
||||
<p className="mt-3 text-slate-400">
|
||||
Enter your work email and we'll send a secure
|
||||
time-limited reset link.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Your email input */}
|
||||
<FormInput
|
||||
label="Email"
|
||||
value="dhananjay@kafm.io"
|
||||
/>
|
||||
|
||||
{/* Send reset button */}
|
||||
<Button className="h-10 w-full rounded-xl bg-gradient-to-r from-teal-400 to-emerald-400 font-semibold text-black">
|
||||
Send reset link
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
83
components/auth/leftpanel.tsx
Normal file
83
components/auth/leftpanel.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
import Badge from "@/utils/badge";
|
||||
import Stat from "@/utils/statCard";
|
||||
|
||||
import { ShieldCheck, KeyRound, FileText } from "lucide-react";
|
||||
|
||||
export default function LeftPanel() {
|
||||
return (
|
||||
|
||||
<div className="relative overflow-hidden border-r border-slate-800 bg-[#091523] hidden lg:block">
|
||||
{/* Grid Background overlay */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-10 pointer-events-none"
|
||||
style={{
|
||||
backgroundImage: `
|
||||
linear-gradient(rgba(255,255,255,.05) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,.05) 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: "44px 44px",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative flex h-full flex-col justify-between p-16 z-10">
|
||||
<div>
|
||||
{/* Logo Group */}
|
||||
<div className="mb-20 flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-teal-300 to-cyan-500 font-bold text-black shadow-md text-lg">
|
||||
K
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold tracking-wider text-white">
|
||||
KAF<span className="text-teal-400">M</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Subtitle */}
|
||||
<p className="mb-6 text-xs font-semibold uppercase tracking-[4px] text-teal-400/90">
|
||||
Unified Facility Platform
|
||||
</p>
|
||||
|
||||
{/* Punchy Catchphrase */}
|
||||
<h1 className="max-w-lg text-4xl font-bold tracking-tight text-white leading-[1]">
|
||||
One platform.
|
||||
<br />
|
||||
Configure it.
|
||||
<br />
|
||||
Run it.
|
||||
</h1>
|
||||
|
||||
{/* Paragraph explanation */}
|
||||
<p className="mt-6 max-w-md text-[16px] leading-relaxed text-slate-400">
|
||||
Administer tenants, sites and access — then operate every
|
||||
building from the same console. Multi-tenant, scoped to the site
|
||||
in front of you.
|
||||
</p>
|
||||
|
||||
{/* Badges Stack */}
|
||||
<div className="mt-10 flex flex-col items-start gap-3">
|
||||
<Badge icon={<ShieldCheck size={14} className="text-teal-400" />}>
|
||||
MFA enforced
|
||||
</Badge>
|
||||
<Badge icon={<KeyRound size={14} className="text-teal-400" />}>
|
||||
Encrypted in transit
|
||||
</Badge>
|
||||
<Badge icon={<FileText size={14} className="text-teal-400" />}>
|
||||
Full audit trail
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Global Statistics Footer */}
|
||||
<div className=" flex gap-2 mt-10 text-slate-300 ">
|
||||
<Stat number="4" label="TENANTS" />
|
||||
<Stat number="8" label="MODULES" />
|
||||
<Stat number="2" label="WORKSPACES" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
103
components/auth/loginForm.tsx
Normal file
103
components/auth/loginForm.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Eye,EyeOff } from "lucide-react";
|
||||
import FormInput from "@/utils/formInput";
|
||||
import Button from "@/utils/button";
|
||||
import { useState } from "react";
|
||||
|
||||
import {Props} from "@/constants/types"
|
||||
|
||||
|
||||
export default function LoginForm({ onSuccess,onForgotPassword }: Props) {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
return (
|
||||
|
||||
<div className="w-full max-w-md space-y-3">
|
||||
{/* Header text container */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight text-white">
|
||||
Sign in to your account
|
||||
</h2>
|
||||
<p className="mt-3 text-[15px] text-slate-400">
|
||||
Use your registered work credentials.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Field Elements */}
|
||||
<div className="space-y-2">
|
||||
<FormInput
|
||||
label="Email"
|
||||
value="dhananjay@kafm.io"
|
||||
|
||||
|
||||
/>
|
||||
|
||||
<div className="relative">
|
||||
<FormInput
|
||||
label="PASSWORD"
|
||||
value=""
|
||||
type={showPassword ? "text" : "password"}
|
||||
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-4 top-[40px] text-slate-500 hover:text-slate-300"
|
||||
>
|
||||
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Interactive options row */}
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<label className="flex cursor-pointer items-center gap-2.5 text-slate-300 select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
defaultChecked
|
||||
className="h-4 w-4 rounded border-slate-700 bg-slate-900 accent-teal-400 focus:ring-0 focus:ring-offset-0"
|
||||
/>
|
||||
Keep me signed in
|
||||
</label>
|
||||
|
||||
<Button
|
||||
|
||||
onClick={onForgotPassword}
|
||||
className="font-medium text-teal-400 transition hover:text-teal-300 bg-transparent border-0 p-0 cursor-pointer"
|
||||
>
|
||||
Forgot password?
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Actions / Submit Stack */}
|
||||
<div className="space-y-4">
|
||||
<Button
|
||||
onClick={onSuccess}
|
||||
className="h-10 w-full rounded-lg bg-gradient-to-r from-teal-400 to-emerald-400 font-semibold text-slate-950 shadow-md hover:opacity-95 transition"
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
|
||||
{/* Custom Visual Divider */}
|
||||
<div className="flex items-center gap-4 py-2">
|
||||
<div className="h-px flex-1 bg-slate-800" />
|
||||
<span className="text-xs uppercase tracking-wider text-slate-600 font-medium">or</span>
|
||||
<div className="h-px flex-1 bg-slate-800" />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="h-10 w-full rounded-lg border border-slate-800 bg-[#122031] text-sm font-semibold text-white hover:bg-[#17293e] transition"
|
||||
>
|
||||
Sign in with SSO
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Bottom Legal Info */}
|
||||
<p className="text-center text-xs tracking-wide text-slate-500 font-mono">
|
||||
Protected by 2-factor auth • every sign-in is
|
||||
<br/> audit-logged
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
);
|
||||
}
|
||||
32
components/auth/otpInput.tsx
Normal file
32
components/auth/otpInput.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
|
||||
export default function OTPInput() {
|
||||
const refs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
index: number
|
||||
) => {
|
||||
if (e.target.value && index < 5) {
|
||||
refs.current[index + 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
{[...Array(6)].map((_, index) => (
|
||||
<input
|
||||
key={index}
|
||||
maxLength={1}
|
||||
ref={(el) => {
|
||||
refs.current[index] = el;
|
||||
}}
|
||||
onChange={(e) => handleChange(e, index)}
|
||||
className="h-16 w-16 rounded-xl border border-slate-700 bg-[#13233a] text-center text-2xl text-white outline-none focus:border-teal-400"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
86
components/auth/verifyCode.tsx
Normal file
86
components/auth/verifyCode.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import Button from "@/utils/button";
|
||||
import OTPInput from "./otpInput";
|
||||
import { useEffect, useState, } from "react";
|
||||
|
||||
import {Props} from "@/constants/types"
|
||||
|
||||
|
||||
export default function VerifyCode({ onBack }: Props) {
|
||||
const [timer, setTimer] = useState(30);
|
||||
|
||||
useEffect(() => {
|
||||
if (timer === 0) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setTimer((prev) => prev - 1);
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [timer]);
|
||||
|
||||
const handleResend = () => {
|
||||
if (timer > 0) return;
|
||||
|
||||
// Call your resend OTP API here
|
||||
|
||||
setTimer(30);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="text-slate-400 hover:text-white"
|
||||
>
|
||||
← Back to sign in
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">
|
||||
Two-factor authentication
|
||||
</h2>
|
||||
|
||||
<p className="text-slate-400">
|
||||
Enter the 6-digit code from your authenticator app, or the<br/>
|
||||
code we sent to{" "}
|
||||
<span className="font-semibold text-white">
|
||||
d•••@kafm.io
|
||||
</span>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<OTPInput />
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
|
||||
onClick={handleResend}
|
||||
|
||||
className={`font-semibold transition-colors ${
|
||||
timer > 0
|
||||
? "text-slate-400 cursor-not-allowed"
|
||||
: "text-teal-400 hover:text-teal-300 cursor-pointer"
|
||||
}`}
|
||||
>
|
||||
Resend code
|
||||
</Button>
|
||||
|
||||
{timer > 0 && (
|
||||
<span className="text-sm text-slate-500">
|
||||
{timer}s
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<label className="flex items-center gap-2 text-slate-300">
|
||||
<input type="checkbox" />
|
||||
Trust this device for 30 days
|
||||
</label>
|
||||
|
||||
<Button className="h-10 w-full rounded-xl bg-gradient-to-r from-teal-400 to-emerald-400 font-semibold text-black">
|
||||
Verify & Continue
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
50
components/dashboard/sideBarItem.tsx
Normal file
50
components/dashboard/sideBarItem.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import {Props} from "@/constants/types";
|
||||
|
||||
|
||||
|
||||
export default function SidebarItem({ item }: Props) {
|
||||
const [open, setOpen] = useState(item.title === "Overview");
|
||||
|
||||
return (
|
||||
<div className="border-b border-slate-800">
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="flex w-full items-center justify-between px-5 py-4 hover:bg-slate-900"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<item.icon size={20} />
|
||||
|
||||
<span className="font-medium">{item.title}</span>
|
||||
</div>
|
||||
|
||||
<ChevronDown
|
||||
size={18}
|
||||
className={`transition-transform ${
|
||||
open ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{open && item.children.length > 0 && (
|
||||
<div className="ml-8 border-l border-slate-700 pl-5 pb-3">
|
||||
{item.children.map((child: any) => (
|
||||
<Link
|
||||
key={child.title}
|
||||
href={child.href}
|
||||
className="group mt-2 flex items-center gap-3 rounded-lg px-3 py-2 text-slate-300 hover:bg-teal-500/20 hover:text-teal-400"
|
||||
>
|
||||
<child.icon size={18} />
|
||||
|
||||
<span>{child.title}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
73
components/dashboard/sidebar.tsx
Normal file
73
components/dashboard/sidebar.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { SIDEBAR_ITEMS } from "@/constants/constant";
|
||||
import SidebarItem from "./sideBarItem";
|
||||
import {
|
||||
|
||||
ShieldCheck,
|
||||
|
||||
Activity,
|
||||
ChevronDown,
|
||||
ChevronsLeft,
|
||||
|
||||
|
||||
} from "lucide-react";
|
||||
|
||||
export default function Sidebar() {
|
||||
return (
|
||||
<aside className="w-80 bg-[#071321] text-white h-screen overflow-y-auto">
|
||||
<div className="flex items-center gap-2.5 px-4 pt-4 pb-3">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-[#3de0c4] font-bold text-[#05111d] text-lg">
|
||||
K
|
||||
</div>
|
||||
<span className="text-xl font-bold tracking-tight text-white">
|
||||
KA<span className="text-[#3de0c4]">FM</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="mx-3 my-2 flex rounded-xl bg-[#091624] p-1 border border-[#142538]">
|
||||
<button className="flex flex-1 items-center justify-center gap-1.5 rounded-lg bg-[#3de0c4] py-1.5 text-xs text-black font-semibold shadow-sm">
|
||||
<Activity size={14} />
|
||||
Operations
|
||||
</button>
|
||||
<button className="flex flex-1 items-center justify-center gap-1.5 rounded-lg py-1.5 text-xs text-slate-400 font-medium hover:text-white transition-colors">
|
||||
<ShieldCheck size={14} />
|
||||
Admin
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="px-4 pt-2 pb-1 text-xs font-semibold uppercase tracking-wider text-slate-500">
|
||||
Tenant runtime
|
||||
</p>
|
||||
<div className="py-4">
|
||||
{SIDEBAR_ITEMS.map((item) => (
|
||||
<SidebarItem
|
||||
key={item.title}
|
||||
item={item}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="px-3 pb-2 pt-1 border-t border-[#0f1f30]">
|
||||
<button className="flex w-full items-center gap-2 rounded-xl py-2 px-3 text-xs text-slate-400 hover:bg-[#091624] hover:text-white transition-colors">
|
||||
<ChevronsLeft size={16} />
|
||||
Minimise menu
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Profile Card Section */}
|
||||
<div className="m-3 mt-0 rounded-xl border border-[#142538] bg-[#071321] p-2.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2.5 items-center">
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-cyan-600/50 bg-[#0d2430] text-cyan-400 text-xs font-bold">
|
||||
DT
|
||||
</div>
|
||||
<div className="min-w-0 leading-tight">
|
||||
<h4 className="font-semibold text-xs text-white truncate">Dhananjay T</h4>
|
||||
<p className="text-[11px] text-slate-500 truncate">
|
||||
dhananjay@kafm.io
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDown className="-rotate-90 text-slate-500 shrink-0" size={14} />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user