old version placed
This commit is contained in:
15
src/app/building/page.tsx
Normal file
15
src/app/building/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
"use server";
|
||||
import React from "react";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Building Management</h1>
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<p>Building page content goes here</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
17
src/app/dashboard/page.tsx
Normal file
17
src/app/dashboard/page.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
"use server";
|
||||
import React from "react";
|
||||
|
||||
const DashboardPage = () => {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h1 className="text-2xl font-bold mb-4">Dashboard</h1>
|
||||
<div className="grid gap-4">
|
||||
<div className="bg-white p-4 rounded-lg shadow">
|
||||
<p>Welcome to your dashboard</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardPage;
|
||||
BIN
src/app/favicon.ico
Normal file
BIN
src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
72
src/app/globals.css
Normal file
72
src/app/globals.css
Normal file
@@ -0,0 +1,72 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
34
src/app/layout.tsx
Normal file
34
src/app/layout.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Evyos Web App",
|
||||
description: "Generated by evyos app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
18
src/app/login/email/page.tsx
Normal file
18
src/app/login/email/page.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
"use server";
|
||||
|
||||
import { checkAccessTokenIsValid } from "@/apicalls/cookies/token";
|
||||
import LoginWithEmail from "@/pages/LoginViaEmail/page";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
const LoginEmailPage = async () => {
|
||||
if (await checkAccessTokenIsValid()) {
|
||||
redirect("/login/select");
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<LoginWithEmail />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginEmailPage;
|
||||
14
src/app/login/page.tsx
Normal file
14
src/app/login/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
"use server";
|
||||
|
||||
import { checkAccessTokenIsValid } from "@/apicalls/cookies/token";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
const LoginPage = async () => {
|
||||
if (await checkAccessTokenIsValid()) {
|
||||
redirect("/login/select");
|
||||
} else {
|
||||
redirect("/login/email");
|
||||
}
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
17
src/app/login/phone/page.tsx
Normal file
17
src/app/login/phone/page.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
"use server";
|
||||
import { checkAccessTokenIsValid } from "@/apicalls/cookies/token";
|
||||
import { redirect } from "next/navigation";
|
||||
import LoginWithPhone from "@/pages/LoginViaPhone/page";
|
||||
|
||||
const LoginPhonePage = async () => {
|
||||
if (await checkAccessTokenIsValid()) {
|
||||
redirect("/login/select");
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<LoginWithPhone />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPhonePage;
|
||||
27
src/app/login/select/LoginEmployeePage.tsx
Normal file
27
src/app/login/select/LoginEmployeePage.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
"use server";
|
||||
import React from "react";
|
||||
import { retrieveAccessObjects } from "@/apicalls/cookies/token";
|
||||
import LoginSelectEmployeeCard from "@/pages/LoginSelectEmployee/page";
|
||||
|
||||
const LoginEmployeePage: React.FC = async () => {
|
||||
const accessObject = await retrieveAccessObjects();
|
||||
return accessObject ? (
|
||||
<>
|
||||
<div className="absolute top-0 left-0 min-w-full min-h-full">
|
||||
<div className="rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||
<div className="flex flex-wrap items-center">
|
||||
<LoginSelectEmployeeCard
|
||||
companyList={accessObject.companies_list}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h1>No register Employeer Company has found for this user</h1>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginEmployeePage;
|
||||
27
src/app/login/select/LoginOccupantPage.tsx
Normal file
27
src/app/login/select/LoginOccupantPage.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
"use server";
|
||||
import React from "react";
|
||||
import { retrieveAccessObjects } from "@/apicalls/cookies/token";
|
||||
import LoginSelectOccupantCard from "@/pages/LoginSelectOccupant/page";
|
||||
|
||||
const LoginOccupantPage: React.FC = async () => {
|
||||
const accessObject: any = await retrieveAccessObjects();
|
||||
return accessObject ? (
|
||||
<>
|
||||
<div className="absolute top-0 left-0 min-w-full min-h-full">
|
||||
<div className="rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||
<div className="flex flex-wrap items-center">
|
||||
<LoginSelectOccupantCard
|
||||
availableOccupants={accessObject?.available_occupants}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h1>No register Occupant has found for this user</h1>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginOccupantPage;
|
||||
22
src/app/login/select/page.tsx
Normal file
22
src/app/login/select/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
"use server";
|
||||
import {
|
||||
checkAccessTokenIsValid,
|
||||
retrieveUserType,
|
||||
} from "@/apicalls/cookies/token";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import LoginEmployeePage from "./LoginEmployeePage";
|
||||
import LoginOccupantPage from "./LoginOccupantPage";
|
||||
|
||||
const SelectPage = async () => {
|
||||
const token_is_valid = await checkAccessTokenIsValid();
|
||||
const userType: "employee" | "occupant" = await retrieveUserType();
|
||||
const isEmployee = userType === "employee";
|
||||
|
||||
if (!userType || !token_is_valid) {
|
||||
redirect("/login/email");
|
||||
}
|
||||
return <>{isEmployee ? <LoginEmployeePage /> : <LoginOccupantPage />}</>;
|
||||
};
|
||||
|
||||
export default SelectPage;
|
||||
101
src/app/page.tsx
Normal file
101
src/app/page.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import Image from "next/image";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
||||
<li className="mb-2">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
|
||||
src/app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li>Save and see your changes instantly.</li>
|
||||
</ol>
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
84
src/components/login/buttons.tsx
Normal file
84
src/components/login/buttons.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const GoogleButton: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<button className="flex w-full items-center justify-center gap-3.5 rounded-lg border border-stroke bg-gray p-4 hover:bg-opacity-50 dark:border-strokedark dark:bg-meta-4 dark:hover:bg-opacity-50">
|
||||
<span>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_191_13499)">
|
||||
<path
|
||||
d="M19.999 10.2217C20.0111 9.53428 19.9387 8.84788 19.7834 8.17737H10.2031V11.8884H15.8266C15.7201 12.5391 15.4804 13.162 15.1219 13.7195C14.7634 14.2771 14.2935 14.7578 13.7405 15.1328L13.7209 15.2571L16.7502 17.5568L16.96 17.5774C18.8873 15.8329 19.9986 13.2661 19.9986 10.2217"
|
||||
fill="#4285F4"
|
||||
/>
|
||||
<path
|
||||
d="M10.2055 19.9999C12.9605 19.9999 15.2734 19.111 16.9629 17.5777L13.7429 15.1331C12.8813 15.7221 11.7248 16.1333 10.2055 16.1333C8.91513 16.1259 7.65991 15.7205 6.61791 14.9745C5.57592 14.2286 4.80007 13.1801 4.40044 11.9777L4.28085 11.9877L1.13101 14.3765L1.08984 14.4887C1.93817 16.1456 3.24007 17.5386 4.84997 18.5118C6.45987 19.4851 8.31429 20.0004 10.2059 19.9999"
|
||||
fill="#34A853"
|
||||
/>
|
||||
<path
|
||||
d="M4.39899 11.9777C4.1758 11.3411 4.06063 10.673 4.05807 9.99996C4.06218 9.32799 4.1731 8.66075 4.38684 8.02225L4.38115 7.88968L1.19269 5.4624L1.0884 5.51101C0.372763 6.90343 0 8.4408 0 9.99987C0 11.5589 0.372763 13.0963 1.0884 14.4887L4.39899 11.9777Z"
|
||||
fill="#FBBC05"
|
||||
/>
|
||||
<path
|
||||
d="M10.2059 3.86663C11.668 3.84438 13.0822 4.37803 14.1515 5.35558L17.0313 2.59996C15.1843 0.901848 12.7383 -0.0298855 10.2059 -3.6784e-05C8.31431 -0.000477834 6.4599 0.514732 4.85001 1.48798C3.24011 2.46124 1.9382 3.85416 1.08984 5.51101L4.38946 8.02225C4.79303 6.82005 5.57145 5.77231 6.61498 5.02675C7.65851 4.28118 8.9145 3.87541 10.2059 3.86663Z"
|
||||
fill="#EB4335"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_191_13499">
|
||||
<rect width="20" height="20" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</span>
|
||||
Google ile giriş yap
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const LoginButton: React.FC = () => {
|
||||
return (
|
||||
<div className="mb-5">
|
||||
<input
|
||||
type="submit"
|
||||
value="Giriş Yap"
|
||||
className="w-full cursor-pointer rounded-lg border border-primary bg-primary p-4 text-white transition hover:bg-opacity-90"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface InterfaceChangeSignTypeButton {
|
||||
buttonType: "phone" | "email";
|
||||
}
|
||||
const ChangeSignTypeButton: React.FC<InterfaceChangeSignTypeButton> = ({
|
||||
buttonType,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="my-5">
|
||||
<button
|
||||
className="w-full cursor-pointer rounded-lg border border-primary bg-primary p-4 text-white transition hover:bg-opacity-90"
|
||||
onClick={() =>
|
||||
router.push(buttonType === "phone" ? "/login/phone" : "/login/email")
|
||||
}
|
||||
>
|
||||
{buttonType === "phone"
|
||||
? "Telefon ile Giriş Yap"
|
||||
: "Email ile Giriş Yap"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { GoogleButton, LoginButton, ChangeSignTypeButton };
|
||||
33
src/components/login/leftsidepanel.tsx
Normal file
33
src/components/login/leftsidepanel.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
interface InterfaceLeftSidePanel {
|
||||
textLabel: string;
|
||||
}
|
||||
|
||||
const LeftSidePanel: React.FC<InterfaceLeftSidePanel> = ({
|
||||
textLabel
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="hidden w-full xl:block xl:w-1/2">
|
||||
<div className="text-center">
|
||||
<p className="text-xl 2xl:px-20 my-5 text-black dark:text-white">
|
||||
{textLabel}
|
||||
</p>
|
||||
<span className="mt-15 inline-block">
|
||||
<Image
|
||||
src="/green-house.webp"
|
||||
alt="login-image"
|
||||
width={480}
|
||||
height={480}
|
||||
className="w-108 h-108 mt-5"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LeftSidePanel;
|
||||
57
src/components/ui/button.tsx
Normal file
57
src/components/ui/button.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
76
src/components/ui/calendar.tsx
Normal file
76
src/components/ui/calendar.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react"
|
||||
import { DayPicker } from "react-day-picker"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
...props
|
||||
}: CalendarProps) {
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn("p-3", className)}
|
||||
classNames={{
|
||||
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
||||
month: "space-y-4",
|
||||
caption: "flex justify-center pt-1 relative items-center",
|
||||
caption_label: "text-sm font-medium",
|
||||
nav: "space-x-1 flex items-center",
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
||||
),
|
||||
nav_button_previous: "absolute left-1",
|
||||
nav_button_next: "absolute right-1",
|
||||
table: "w-full border-collapse space-y-1",
|
||||
head_row: "flex",
|
||||
head_cell:
|
||||
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
||||
row: "flex w-full mt-2",
|
||||
cell: cn(
|
||||
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
||||
props.mode === "range"
|
||||
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
||||
: "[&:has([aria-selected])]:rounded-md"
|
||||
),
|
||||
day: cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
|
||||
),
|
||||
day_range_start: "day-range-start",
|
||||
day_range_end: "day-range-end",
|
||||
day_selected:
|
||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||
day_today: "bg-accent text-accent-foreground",
|
||||
day_outside:
|
||||
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
||||
day_disabled: "text-muted-foreground opacity-50",
|
||||
day_range_middle:
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||
day_hidden: "invisible",
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
IconLeft: ({ className, ...props }) => (
|
||||
<ChevronLeft className={cn("h-4 w-4", className)} {...props} />
|
||||
),
|
||||
IconRight: ({ className, ...props }) => (
|
||||
<ChevronRight className={cn("h-4 w-4", className)} {...props} />
|
||||
),
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Calendar.displayName = "Calendar"
|
||||
|
||||
export { Calendar }
|
||||
178
src/components/ui/form.tsx
Normal file
178
src/components/ui/form.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
} from "react-hook-form"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
)
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState, formState } = useFormContext()
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
}
|
||||
|
||||
const { id } = itemContext
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
}
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
)
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
})
|
||||
FormItem.displayName = "FormItem"
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormLabel.displayName = "FormLabel"
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormControl.displayName = "FormControl"
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-[0.8rem] text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormDescription.displayName = "FormDescription"
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message) : children
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-[0.8rem] font-medium text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
FormMessage.displayName = "FormMessage"
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
||||
22
src/components/ui/input.tsx
Normal file
22
src/components/ui/input.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
26
src/components/ui/label.tsx
Normal file
26
src/components/ui/label.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
117
src/components/ui/pagination.tsx
Normal file
117
src/components/ui/pagination.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import * as React from "react"
|
||||
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ButtonProps, buttonVariants } from "@/components/ui/button"
|
||||
|
||||
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
Pagination.displayName = "Pagination"
|
||||
|
||||
const PaginationContent = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
React.ComponentProps<"ul">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
className={cn("flex flex-row items-center gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
PaginationContent.displayName = "PaginationContent"
|
||||
|
||||
const PaginationItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<"li">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li ref={ref} className={cn("", className)} {...props} />
|
||||
))
|
||||
PaginationItem.displayName = "PaginationItem"
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean
|
||||
} & Pick<ButtonProps, "size"> &
|
||||
React.ComponentProps<"a">
|
||||
|
||||
const PaginationLink = ({
|
||||
className,
|
||||
isActive,
|
||||
size = "icon",
|
||||
...props
|
||||
}: PaginationLinkProps) => (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
size,
|
||||
}),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
PaginationLink.displayName = "PaginationLink"
|
||||
|
||||
const PaginationPrevious = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 pl-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<span>Previous</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
PaginationPrevious.displayName = "PaginationPrevious"
|
||||
|
||||
const PaginationNext = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 pr-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<span>Next</span>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</PaginationLink>
|
||||
)
|
||||
PaginationNext.displayName = "PaginationNext"
|
||||
|
||||
const PaginationEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) => (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
)
|
||||
PaginationEllipsis.displayName = "PaginationEllipsis"
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationLink,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
}
|
||||
33
src/components/ui/popover.tsx
Normal file
33
src/components/ui/popover.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
|
||||
const PopoverAnchor = PopoverPrimitive.Anchor
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||
48
src/components/ui/scroll-area.tsx
Normal file
48
src/components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
29
src/components/ui/switch.tsx
Normal file
29
src/components/ui/switch.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
||||
120
src/components/ui/table.tsx
Normal file
120
src/components/ui/table.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
78
src/lib/renderZodValidation.ts
Normal file
78
src/lib/renderZodValidation.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
import * as z from "zod";
|
||||
import { ZodDecimal } from "./zodDecimal";
|
||||
|
||||
function convertApiValidationToZodValidation(apiValidation: any) {
|
||||
let zodValidation: any = {};
|
||||
Object.entries(apiValidation).forEach(([key, value]: any) => {
|
||||
const fieldType: String = value.fieldType || "string";
|
||||
const required = value.required || false;
|
||||
if (fieldType === "string") {
|
||||
zodValidation[key] = required
|
||||
? z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine((val) => val !== "" || val !== null)
|
||||
: z
|
||||
.string()
|
||||
.min(1)
|
||||
.optional()
|
||||
.refine((val) => val !== "" || val !== null);
|
||||
} else if (fieldType === "integer") {
|
||||
zodValidation[key] = required
|
||||
? z.preprocess((value) => {
|
||||
try {
|
||||
const parsedValue = Number(value);
|
||||
return isNaN(parsedValue) ? undefined : parsedValue;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}, z.number().min(1))
|
||||
: z.preprocess((value) => {
|
||||
try {
|
||||
const parsedValue = Number(value);
|
||||
return isNaN(parsedValue) ? undefined : parsedValue;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}, z.number().min(1).optional());
|
||||
} else if (fieldType === "boolean") {
|
||||
zodValidation[key] = required ? z.boolean() : z.boolean().optional();
|
||||
} else if (fieldType === "datetime") {
|
||||
zodValidation[key] = required ? z.date() : z.date().optional();
|
||||
} else if (fieldType === "float") {
|
||||
zodValidation[key] = required
|
||||
? ZodDecimal.create({ coerce: true })
|
||||
: ZodDecimal.create({ coerce: true }).optional();
|
||||
}
|
||||
});
|
||||
const validSchemaZod = z.object({
|
||||
...zodValidation,
|
||||
});
|
||||
return {
|
||||
validSchemaZod: validSchemaZod,
|
||||
zodValidation: zodValidation,
|
||||
apiValidation: apiValidation,
|
||||
};
|
||||
}
|
||||
|
||||
function retrieveDataWhichHaveValidation(data: any, apiValidation: any) {
|
||||
const apiValidated = apiValidation?.validated || {};
|
||||
Object.entries(apiValidated).forEach(([key, value]: any) => {
|
||||
const fieldType: String = value.fieldType || "string";
|
||||
const required = value.required || false;
|
||||
if (fieldType === "string") {
|
||||
data[key] = required ? data[key] : data[key] || "";
|
||||
} else if (fieldType === "integer") {
|
||||
data[key] = required ? data[key] : data[key] || 0;
|
||||
} else if (fieldType === "boolean") {
|
||||
data[key] = required ? data[key] : data[key] || false;
|
||||
} else if (fieldType === "datetime") {
|
||||
data[key] = required ? data[key] : new Date(data[key]) || "";
|
||||
} else if (fieldType === "float") {
|
||||
data[key] = required ? data[key] : data[key] || 0.0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { convertApiValidationToZodValidation, retrieveDataWhichHaveValidation };
|
||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
306
src/lib/zodDecimal.ts
Normal file
306
src/lib/zodDecimal.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
import {
|
||||
INVALID,
|
||||
ParseContext,
|
||||
ParseInput,
|
||||
ParseReturnType,
|
||||
ParseStatus,
|
||||
RawCreateParams,
|
||||
ZodIssueCode,
|
||||
ZodParsedType,
|
||||
ZodType,
|
||||
ZodTypeDef,
|
||||
addIssueToContext,
|
||||
} from "zod";
|
||||
|
||||
export type ZodDecimalCheck =
|
||||
| { kind: "precision"; value: number; message?: string }
|
||||
| { kind: "wholeNumber"; value: number; message?: string }
|
||||
| { kind: "min"; value: number; inclusive: boolean; message?: string }
|
||||
| { kind: "max"; value: number; inclusive: boolean; message?: string }
|
||||
| { kind: "finite"; message?: string };
|
||||
|
||||
const zodDecimalKind = "ZodDecimal";
|
||||
|
||||
export interface ZodDecimalDef extends ZodTypeDef {
|
||||
checks: ZodDecimalCheck[];
|
||||
typeName: typeof zodDecimalKind;
|
||||
coerce: boolean;
|
||||
}
|
||||
|
||||
const precisionRegex = /(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export class ZodDecimal extends ZodType<number, ZodDecimalDef, any> {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
_parse(input: ParseInput): ParseReturnType<number> {
|
||||
// detect decimal js object
|
||||
if (
|
||||
input.data !== null &&
|
||||
typeof input.data === "object" &&
|
||||
"toNumber" in input.data
|
||||
) {
|
||||
input.data = input.data.toNumber();
|
||||
}
|
||||
if (this._def.coerce) {
|
||||
input.data = Number(input.data);
|
||||
}
|
||||
|
||||
const parsedType = this._getType(input);
|
||||
if (parsedType !== ZodParsedType.number) {
|
||||
const ctx = this._getOrReturnCtx(input);
|
||||
addIssueToContext(ctx, {
|
||||
code: ZodIssueCode.invalid_type,
|
||||
expected: ZodParsedType.number,
|
||||
received: ctx.parsedType,
|
||||
});
|
||||
return INVALID;
|
||||
}
|
||||
|
||||
let ctx: undefined | ParseContext = undefined;
|
||||
const status = new ParseStatus();
|
||||
|
||||
for (const check of this._def.checks) {
|
||||
if (check.kind === "precision") {
|
||||
const parts = input.data.toString().match(precisionRegex);
|
||||
const decimals = Math.max(
|
||||
(parts[1] ? parts[1].length : 0) -
|
||||
(parts[2] ? parseInt(parts[2], 10) : 0),
|
||||
0
|
||||
);
|
||||
if (decimals > check.value) {
|
||||
ctx = this._getOrReturnCtx(input, ctx);
|
||||
addIssueToContext(ctx, {
|
||||
code: ZodIssueCode.custom,
|
||||
message: check.message,
|
||||
params: {
|
||||
precision: check.value,
|
||||
},
|
||||
});
|
||||
status.dirty();
|
||||
}
|
||||
} else if (check.kind === "wholeNumber") {
|
||||
const wholeNumber = input.data.toString().split(".")[0];
|
||||
const tooLong = wholeNumber.length > check.value;
|
||||
|
||||
if (tooLong) {
|
||||
ctx = this._getOrReturnCtx(input, ctx);
|
||||
addIssueToContext(ctx, {
|
||||
code: ZodIssueCode.custom,
|
||||
message: check.message,
|
||||
params: {
|
||||
wholeNumber: check.value,
|
||||
},
|
||||
});
|
||||
status.dirty();
|
||||
}
|
||||
} else if (check.kind === "min") {
|
||||
const tooSmall = check.inclusive
|
||||
? input.data < check.value
|
||||
: input.data <= check.value;
|
||||
if (tooSmall) {
|
||||
ctx = this._getOrReturnCtx(input, ctx);
|
||||
addIssueToContext(ctx, {
|
||||
code: ZodIssueCode.too_small,
|
||||
minimum: check.value,
|
||||
type: "number",
|
||||
inclusive: check.inclusive,
|
||||
exact: false,
|
||||
message: check.message,
|
||||
});
|
||||
status.dirty();
|
||||
}
|
||||
} else if (check.kind === "max") {
|
||||
const tooBig = check.inclusive
|
||||
? input.data > check.value
|
||||
: input.data >= check.value;
|
||||
if (tooBig) {
|
||||
ctx = this._getOrReturnCtx(input, ctx);
|
||||
addIssueToContext(ctx, {
|
||||
code: ZodIssueCode.too_big,
|
||||
maximum: check.value,
|
||||
type: "number",
|
||||
inclusive: check.inclusive,
|
||||
exact: false,
|
||||
message: check.message,
|
||||
});
|
||||
status.dirty();
|
||||
}
|
||||
} else if (check.kind === "finite") {
|
||||
if (!Number.isFinite(input.data)) {
|
||||
ctx = this._getOrReturnCtx(input, ctx);
|
||||
addIssueToContext(ctx, {
|
||||
code: ZodIssueCode.not_finite,
|
||||
message: check.message,
|
||||
});
|
||||
status.dirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { status: status.value, value: input.data };
|
||||
}
|
||||
|
||||
static create = (
|
||||
params?: RawCreateParams & { coerce?: true }
|
||||
): ZodDecimal => {
|
||||
return new ZodDecimal({
|
||||
checks: [],
|
||||
typeName: zodDecimalKind,
|
||||
coerce: params?.coerce ?? false,
|
||||
});
|
||||
};
|
||||
|
||||
protected setLimit(
|
||||
kind: "min" | "max",
|
||||
value: number,
|
||||
inclusive: boolean,
|
||||
message?: string
|
||||
): ZodDecimal {
|
||||
return new ZodDecimal({
|
||||
...this._def,
|
||||
checks: [
|
||||
...this._def.checks,
|
||||
{
|
||||
kind,
|
||||
value,
|
||||
inclusive,
|
||||
message,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
_addCheck(check: ZodDecimalCheck): ZodDecimal {
|
||||
return new ZodDecimal({
|
||||
...this._def,
|
||||
checks: [...this._def.checks, check],
|
||||
});
|
||||
}
|
||||
|
||||
lte(value: number, message?: string): ZodDecimal {
|
||||
return this.setLimit("max", value, true, message);
|
||||
}
|
||||
|
||||
lt(value: number, message?: string): ZodDecimal {
|
||||
return this.setLimit("max", value, false, message);
|
||||
}
|
||||
max = this.lte;
|
||||
|
||||
gt(value: number, message?: string): ZodDecimal {
|
||||
return this.setLimit("min", value, false, message);
|
||||
}
|
||||
gte(value: number, message?: string): ZodDecimal {
|
||||
return this.setLimit("min", value, true, message);
|
||||
}
|
||||
|
||||
min = this.gte;
|
||||
|
||||
precision(value: number, message?: string): ZodDecimal {
|
||||
return this._addCheck({
|
||||
kind: "precision",
|
||||
value,
|
||||
message,
|
||||
});
|
||||
}
|
||||
wholeNumber(value: number, message?: string): ZodDecimal {
|
||||
return this._addCheck({
|
||||
kind: "wholeNumber",
|
||||
value,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
get minValue() {
|
||||
let min: number | null = null;
|
||||
for (const ch of this._def.checks) {
|
||||
if (ch.kind === "min") {
|
||||
if (min === null || ch.value > min) min = ch.value;
|
||||
}
|
||||
}
|
||||
return min;
|
||||
}
|
||||
|
||||
get maxValue() {
|
||||
let max: number | null = null;
|
||||
for (const ch of this._def.checks) {
|
||||
if (ch.kind === "max") {
|
||||
if (max === null || ch.value < max) max = ch.value;
|
||||
}
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
positive(message?: string) {
|
||||
return this._addCheck({
|
||||
kind: "min",
|
||||
value: 0,
|
||||
inclusive: false,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
negative(message?: string) {
|
||||
return this._addCheck({
|
||||
kind: "max",
|
||||
value: 0,
|
||||
inclusive: false,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
nonpositive(message?: string) {
|
||||
return this._addCheck({
|
||||
kind: "max",
|
||||
value: 0,
|
||||
inclusive: true,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
nonnegative(message?: string) {
|
||||
return this._addCheck({
|
||||
kind: "min",
|
||||
value: 0,
|
||||
inclusive: true,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
finite(message?: string) {
|
||||
return this._addCheck({
|
||||
kind: "finite",
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
safe(message?: string) {
|
||||
return this._addCheck({
|
||||
kind: "min",
|
||||
inclusive: true,
|
||||
value: Number.MIN_SAFE_INTEGER,
|
||||
message,
|
||||
})._addCheck({
|
||||
kind: "max",
|
||||
inclusive: true,
|
||||
value: Number.MAX_SAFE_INTEGER,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
get isFinite() {
|
||||
let max: number | null = null,
|
||||
min: number | null = null;
|
||||
for (const ch of this._def.checks) {
|
||||
if (ch.kind === "finite") {
|
||||
return true;
|
||||
} else if (ch.kind === "min") {
|
||||
if (min === null || ch.value > min) min = ch.value;
|
||||
} else if (ch.kind === "max") {
|
||||
if (max === null || ch.value < max) max = ch.value;
|
||||
}
|
||||
}
|
||||
return Number.isFinite(min) && Number.isFinite(max);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const zodDecimal = ZodDecimal.create;
|
||||
34
src/pages/LoginSelectEmployee/page.tsx
Normal file
34
src/pages/LoginSelectEmployee/page.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
"use server";
|
||||
import React from "react";
|
||||
import SelectEmployeeFrom from "./selectFrom";
|
||||
import { redirect } from "next/navigation";
|
||||
// import LeftSidePanel from "@/components/login/leftsidepanel";
|
||||
{
|
||||
/* <LeftSidePanel textLabel="Evyos Yönetim Modülüne hoşgeldiniz" /> */
|
||||
}
|
||||
|
||||
interface InterfaceLoginSelectEmployee {
|
||||
companyList: any;
|
||||
}
|
||||
|
||||
const LoginSelectEmployeeCard: React.FC<InterfaceLoginSelectEmployee> = async (
|
||||
companyList: any
|
||||
) => {
|
||||
if (!companyList) {
|
||||
redirect("/login/email");
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-full min-w-full">
|
||||
<h1 className="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">
|
||||
Şirket Seçimi Yapınız
|
||||
</h1>
|
||||
<div>
|
||||
<SelectEmployeeFrom companyList={companyList?.companyList} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginSelectEmployeeCard;
|
||||
70
src/pages/LoginSelectEmployee/selectFrom.tsx
Normal file
70
src/pages/LoginSelectEmployee/selectFrom.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { loginSelectEmployee } from "@/apicalls/login/login";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface InterfaceSelectEmployeeFrom {
|
||||
companyList: any;
|
||||
}
|
||||
|
||||
const SelectEmployeeFrom: React.FC<InterfaceSelectEmployeeFrom> = ({
|
||||
companyList,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
console.log("companyList", companyList);
|
||||
function onClick(data: any) {
|
||||
loginSelectEmployee({ company_uu_id: data?.uu_id })
|
||||
.then((responseData: any) => {
|
||||
if (responseData?.completed) {
|
||||
router.push("/dashboard");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
{companyList.map((data: any) => (
|
||||
<div className="flex" key={data.uu_id}>
|
||||
<div
|
||||
className="flex sm:h-56 hover:bg-white bg-emerald-800 m-3 ring-1 shadow-xl ring-emerald-700 p-3 h-64 rounded-2xl"
|
||||
key={data.uu_id}
|
||||
onClick={() => onClick(data)}
|
||||
>
|
||||
<div className="lg:w-1/4">
|
||||
<Image
|
||||
src={"/green-house.webp"}
|
||||
alt={`Evyos ${data.uu_id}`}
|
||||
className=" w-full h-full object-cover"
|
||||
width={300}
|
||||
height={300}
|
||||
/>
|
||||
</div>
|
||||
<div className="lg:w-3/4 m-5">
|
||||
<h2 className="text-lg font-bold mb-2">UUID : {data.uu_id}</h2>
|
||||
<h2 className="text-lg font-bold mb-2">
|
||||
Şirket Unvanı : {data.public_name}
|
||||
</h2>
|
||||
<h2 className="text-lg font-bold mb-2">
|
||||
Şirket Tipi Name : {data.company_type}
|
||||
</h2>
|
||||
<h2 className="text-lg font-bold mb-2">
|
||||
Adres :{" "}
|
||||
{data.company_address
|
||||
? data.company_address
|
||||
: "Tanımlı Değil"}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectEmployeeFrom;
|
||||
30
src/pages/LoginSelectOccupant/page.tsx
Normal file
30
src/pages/LoginSelectOccupant/page.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
"use server";
|
||||
import React from "react";
|
||||
import SelectOccupantFrom from "./selectFrom";
|
||||
// import LeftSidePanel from "@/components/login/leftsidepanel";
|
||||
{
|
||||
/* <LeftSidePanel textLabel="Evyos Yönetim Modülüne hoşgeldiniz. Lütfen mail adresinizi ve şifreniz ile giriş yapınız." /> */
|
||||
}
|
||||
|
||||
interface interfaceLoginSelectOccupant {
|
||||
availableOccupants: any;
|
||||
}
|
||||
|
||||
const LoginSelectOccupantCard: React.FC<interfaceLoginSelectOccupant> = async ({
|
||||
availableOccupants,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-full min-w-full">
|
||||
<h1 className="mt-10 text-center text-2xl/9 font-bold tracking-tight text-gray-900">
|
||||
Görev Seçimi Yapınız
|
||||
</h1>
|
||||
<div>
|
||||
<SelectOccupantFrom availableOccupants={availableOccupants} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginSelectOccupantCard;
|
||||
154
src/pages/LoginSelectOccupant/selectFrom.tsx
Normal file
154
src/pages/LoginSelectOccupant/selectFrom.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { loginSelectOccupant } from "@/apicalls/login/login";
|
||||
import Image from "next/image";
|
||||
|
||||
interface Building {
|
||||
build_uu_id: string;
|
||||
build_name: string;
|
||||
build_no: string;
|
||||
occupants: Array<any>;
|
||||
}
|
||||
|
||||
interface InterfaceSelectOccupanyFrom {
|
||||
availableOccupants: Record<string, Building>;
|
||||
}
|
||||
|
||||
const SelectOccupantFrom: React.FC<InterfaceSelectOccupanyFrom> = ({
|
||||
availableOccupants,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const [activeBuildingList, setActiveBuildingList] = React.useState<
|
||||
Building[]
|
||||
>([]);
|
||||
const [activeOccupantList, setActiveOccupantList] = React.useState<any[]>([]);
|
||||
const [isBuildingSelected, setIsBuildingSelected] = React.useState(false);
|
||||
const [selectedBuilding, setSelectedBuilding] = React.useState("");
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isBuildingSelected) {
|
||||
const uniqueBuildings = Object.values(availableOccupants).map(
|
||||
(value) => ({
|
||||
build_uu_id: value.build_uu_id,
|
||||
build_name: value.build_name,
|
||||
build_no: value.build_no,
|
||||
occupants: value.occupants,
|
||||
})
|
||||
);
|
||||
setActiveBuildingList(uniqueBuildings);
|
||||
}
|
||||
}, [availableOccupants, isBuildingSelected]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isBuildingSelected && selectedBuilding) {
|
||||
const selectedOccupants =
|
||||
Object.values(availableOccupants).find(
|
||||
(value) => value.build_uu_id === selectedBuilding
|
||||
)?.occupants || [];
|
||||
setActiveOccupantList(selectedOccupants);
|
||||
}
|
||||
}, [isBuildingSelected, selectedBuilding, availableOccupants]);
|
||||
|
||||
const onClickBuild = (data: Building) => {
|
||||
setSelectedBuilding(data.build_uu_id);
|
||||
setIsBuildingSelected(true);
|
||||
};
|
||||
|
||||
const onClick = (data: any) => {
|
||||
loginSelectOccupant({
|
||||
build_part_uu_id: data?.part_uu_id,
|
||||
occupant_uu_id: data?.uu_id,
|
||||
selectedBuilding: selectedBuilding,
|
||||
})
|
||||
.then((responseData: { completed: boolean }) => {
|
||||
console.log("responseData", responseData);
|
||||
console.log("responseData.completed", responseData?.completed);
|
||||
if (responseData?.completed) {
|
||||
router.replace("/dashboard");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Login error:", error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isBuildingSelected ? (
|
||||
<div className="3xl:grid 3xl:grid-cols-3 3xl:gap-4">
|
||||
{activeBuildingList.map((data: any) => (
|
||||
<div
|
||||
className="flex sm:h-56 hover:bg-white bg-emerald-800 m-3 ring-1 shadow-xl ring-emerald-700 p-3 h-64 rounded-2xl"
|
||||
onClick={() => onClickBuild(data)}
|
||||
key={data.build_uu_id}
|
||||
>
|
||||
<div className="w-1/4">
|
||||
<Image
|
||||
src={"/green-house.webp"}
|
||||
alt={`Evyos ${data.build_uu_id}`}
|
||||
className=" w-full h-full object-cover"
|
||||
width={300}
|
||||
height={300}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-3/4 m-5">
|
||||
<h2 className="text-lg font-bold mb-2">
|
||||
UUID : {data.build_uu_id}
|
||||
</h2>
|
||||
<h2 className="text-lg font-bold mb-2">
|
||||
Bina : {data.build_name}
|
||||
</h2>
|
||||
<h2 className="text-lg font-bold mb-2">
|
||||
Bina No : {data.build_no}
|
||||
</h2>
|
||||
{/* <h2 className="text-lg font-bold mb-2">
|
||||
Adres :{" "}
|
||||
{data.company_address
|
||||
? data.company_address
|
||||
: "Tanımlı Değil"}
|
||||
</h2> */}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="3xl:grid 3xl:grid-cols-3 3xl:gap-4">
|
||||
{activeOccupantList.map((data: any) => (
|
||||
<div
|
||||
className="flex sm:h-56 hover:bg-white bg-emerald-800 m-3 ring-1 shadow-xl ring-emerald-700 p-3 h-64 rounded-2xl"
|
||||
key={`${data.part_uu_id}-${data.uu_id}`}
|
||||
onClick={() => onClick(data)}
|
||||
>
|
||||
<div className="w-1/4">
|
||||
<Image
|
||||
src={"/green-house.webp"}
|
||||
alt={`Evyos ${data.part_uu_id}`}
|
||||
className=" w-full h-full object-cover"
|
||||
width={300}
|
||||
height={300}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-3/4 m-5">
|
||||
<h2 className="text-lg font-bold mb-2">
|
||||
UUID : {data.part_uu_id}
|
||||
</h2>
|
||||
<h2 className="text-lg font-bold mb-2">
|
||||
Bina : {data.part_name}
|
||||
</h2>
|
||||
<h2 className="text-lg font-bold mb-2">
|
||||
Daire Kat : {data.part_level}
|
||||
</h2>
|
||||
<h2 className="text-lg font-bold mb-2">
|
||||
Giriş Tipi : {data.code} - {data.description}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectOccupantFrom;
|
||||
23
src/pages/LoginViaEmail/page.tsx
Normal file
23
src/pages/LoginViaEmail/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import SignInForm from "./singInForm";
|
||||
import LeftSidePanel from "@/components/login/leftsidepanel";
|
||||
|
||||
const LoginWithEmail: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="absolute top-0 left-0 min-w-full min-h-full">
|
||||
<div className="mx-auto max-w-screen-2xl p-4 md:p-6 2xl:p-10">
|
||||
<div className="rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||
<div className="flex flex-wrap items-center">
|
||||
<LeftSidePanel textLabel="Evyos Yönetim Modülüne hoşgeldiniz. Lütfen mail adresinizi ve şifreniz ile giriş yapınız." />
|
||||
<SignInForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginWithEmail;
|
||||
17
src/pages/LoginViaEmail/schema.ts
Normal file
17
src/pages/LoginViaEmail/schema.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as z from "zod";
|
||||
|
||||
const loginSchema = z.object({
|
||||
loginEmailInput: z
|
||||
.string()
|
||||
.min(4, { message: "Email adresi minimum 4 karaterden oluşmalıdır" })
|
||||
.email("Geçerli bir mail adresi giriniz")
|
||||
.default(""),
|
||||
loginPassword: z
|
||||
.string()
|
||||
.min(5, { message: "Şifre 6 karakterden az olamaz" })
|
||||
.default(""),
|
||||
loginRememberMe: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
export type LoginFormSchema = z.infer<typeof loginSchema>;
|
||||
export { loginSchema };
|
||||
154
src/pages/LoginViaEmail/singInForm.tsx
Normal file
154
src/pages/LoginViaEmail/singInForm.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { Eye, EyeOff, Mail } from "lucide-react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { loginViaAccessKeys } from "@/apicalls/login/login";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
import { loginSchema, LoginFormSchema } from "./schema";
|
||||
import {
|
||||
GoogleButton,
|
||||
LoginButton,
|
||||
ChangeSignTypeButton,
|
||||
} from "@/components/login/buttons";
|
||||
|
||||
const SignInForm: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const [showPassword, setShowPassword] = React.useState(false);
|
||||
|
||||
const form = useForm<LoginFormSchema>({
|
||||
resolver: zodResolver(loginSchema),
|
||||
});
|
||||
|
||||
function onSubmit(values: LoginFormSchema) {
|
||||
loginViaAccessKeys({
|
||||
domain: "evyos.com.tr",
|
||||
accessKey: values.loginEmailInput,
|
||||
password: values.loginPassword,
|
||||
rememberMe: values.loginRememberMe,
|
||||
})
|
||||
.then((res: any) => {
|
||||
console.log("res", res);
|
||||
if (res?.completed) {
|
||||
setTimeout(() => {
|
||||
router.push("/login/select");
|
||||
}, 1000);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("error", error);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full border-stroke dark:border-strokedark xl:w-1/2 xl:border-l-2">
|
||||
<div className="w-full p-4 sm:p-12.5 xl:p-17.5">
|
||||
<h2 className="my-9 text-center text-2xl font-bold text-black dark:text-white sm:text-title-xl2">
|
||||
Mail ile Giriş Yapın
|
||||
</h2>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-5 max-w-3xl mx-auto py-10"
|
||||
>
|
||||
<div className="mb-4">
|
||||
<label className="mb-2.5 block font-medium text-black dark:text-white">
|
||||
Email
|
||||
</label>
|
||||
<div className="relative">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="loginEmailInput"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="example@example.net"
|
||||
className="w-full rounded-lg border border-stroke bg-transparent py-4 pl-6 pr-10 text-black outline-none focus:border-primary focus-visible:shadow-none dark:border-form-strokedark dark:bg-form-input dark:text-white dark:focus:border-primary"
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<span className="absolute right-4 top-4">
|
||||
<Mail />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<label className="mb-2.5 block font-medium text-black dark:text-white">
|
||||
Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="loginPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="Şifre giriniz"
|
||||
className="w-full rounded-lg border border-stroke bg-transparent py-4 pl-6 pr-10 text-black outline-none focus:border-primary focus-visible:shadow-none dark:border-form-strokedark dark:bg-form-input dark:text-white dark:focus:border-primary"
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className="absolute right-4 top-4"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{!showPassword ? <Eye /> : <EyeOff />}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="loginRememberMe"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-lg">Beni Hatırla</FormLabel>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
aria-readonly
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<LoginButton />
|
||||
<GoogleButton />
|
||||
</form>
|
||||
</Form>
|
||||
<ChangeSignTypeButton buttonType="phone" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignInForm;
|
||||
23
src/pages/LoginViaPhone/page.tsx
Normal file
23
src/pages/LoginViaPhone/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import SignPhoneInForm from "./singInForm";
|
||||
import LeftSidePanel from "@/components/login/leftsidepanel";
|
||||
|
||||
const LoginWithPhone: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="absolute top-0 left-0 min-w-full min-h-full">
|
||||
<div className="mx-auto max-w-screen-2xl p-4 md:p-6 2xl:p-10">
|
||||
<div className="rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||
<div className="flex flex-wrap items-center">
|
||||
<LeftSidePanel textLabel="Evyos Yönetim Modülüne hoşgeldiniz. Lütfen telefon ve şifreniz ile giriş yapınız." />
|
||||
<SignPhoneInForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginWithPhone;
|
||||
17
src/pages/LoginViaPhone/schema.ts
Normal file
17
src/pages/LoginViaPhone/schema.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as z from "zod";
|
||||
|
||||
const loginPhoneSchema = z.object({
|
||||
loginPhone: z
|
||||
.string()
|
||||
.min(10, { message: "Telefon numarası 10 karakterden az olamaz" })
|
||||
.max(14, { message: "Telefon numarası 14 karakterden fazla olamaz" })
|
||||
.default(""),
|
||||
loginPassword: z
|
||||
.string()
|
||||
.min(5, { message: "Şifre 6 karakterden az olamaz" })
|
||||
.default(""),
|
||||
loginRememberMe: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
export type LoginPhoneFormSchema = z.infer<typeof loginPhoneSchema>;
|
||||
export { loginPhoneSchema };
|
||||
153
src/pages/LoginViaPhone/singInForm.tsx
Normal file
153
src/pages/LoginViaPhone/singInForm.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { Eye, EyeOff, Mail } from "lucide-react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { loginViaAccessKeys } from "@/apicalls/login/login";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
import { LoginPhoneFormSchema, loginPhoneSchema } from "./schema";
|
||||
import {
|
||||
GoogleButton,
|
||||
LoginButton,
|
||||
ChangeSignTypeButton,
|
||||
} from "@/components/login/buttons";
|
||||
|
||||
const SignPhoneInForm: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const [showPassword, setShowPassword] = React.useState(false);
|
||||
|
||||
const form = useForm<LoginPhoneFormSchema>({
|
||||
resolver: zodResolver(loginPhoneSchema),
|
||||
});
|
||||
|
||||
function onSubmit(values: LoginPhoneFormSchema) {
|
||||
loginViaAccessKeys({
|
||||
domain: "evyos.com.tr",
|
||||
accessKey: values.loginPhone,
|
||||
password: values.loginPassword,
|
||||
rememberMe: values.loginRememberMe,
|
||||
})
|
||||
.then((res: any) => {
|
||||
if (res?.completed) {
|
||||
setTimeout(() => {
|
||||
router.push("/login/select");
|
||||
}, 1000);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("error", error);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full border-stroke dark:border-strokedark xl:w-1/2 xl:border-l-2">
|
||||
<div className="w-full p-4 sm:p-12.5 xl:p-17.5">
|
||||
<h2 className="my-9 text-center text-2xl font-bold text-black dark:text-white sm:text-title-xl2">
|
||||
Mail ile Giriş Yapın
|
||||
</h2>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-5 max-w-3xl mx-auto py-10"
|
||||
>
|
||||
<div className="mb-4">
|
||||
<label className="mb-2.5 block font-medium text-black dark:text-white">
|
||||
Email
|
||||
</label>
|
||||
<div className="relative">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="loginPhone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="example@example.net"
|
||||
className="w-full rounded-lg border border-stroke bg-transparent py-4 pl-6 pr-10 text-black outline-none focus:border-primary focus-visible:shadow-none dark:border-form-strokedark dark:bg-form-input dark:text-white dark:focus:border-primary"
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<span className="absolute right-4 top-4">
|
||||
<Mail />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<label className="mb-2.5 block font-medium text-black dark:text-white">
|
||||
Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="loginPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="Şifre giriniz"
|
||||
className="w-full rounded-lg border border-stroke bg-transparent py-4 pl-6 pr-10 text-black outline-none focus:border-primary focus-visible:shadow-none dark:border-form-strokedark dark:bg-form-input dark:text-white dark:focus:border-primary"
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className="absolute right-4 top-4"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{!showPassword ? <Eye /> : <EyeOff />}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="loginRememberMe"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-lg">Beni Hatırla</FormLabel>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
aria-readonly
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<LoginButton />
|
||||
<GoogleButton />
|
||||
</form>
|
||||
</Form>
|
||||
<ChangeSignTypeButton buttonType="email" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignPhoneInForm;
|
||||
Reference in New Issue
Block a user