next-auth-mfelfelani72 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/dist/components/Login.d.ts +14 -0
- package/dist/components/Login.js +56 -0
- package/dist/components/NewUiLogin.d.ts +10 -0
- package/dist/components/NewUiLogin.js +10 -0
- package/dist/components/UiLogin.d.ts +10 -0
- package/dist/components/UiLogin.js +10 -0
- package/dist/config/language.d.ts +11 -0
- package/dist/config/language.js +4 -0
- package/dist/dictionaries/en.json +8 -0
- package/dist/dictionaries/fa.json +8 -0
- package/dist/dictionaries/index.d.ts +15 -0
- package/dist/dictionaries/index.js +10 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.js +1 -0
- package/eslint.config.mjs +25 -0
- package/next.config.ts +7 -0
- package/package.json +30 -0
- package/pnpm-workspace.yaml +4 -0
- package/postcss.config.mjs +5 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/[lang]/layout.tsx +40 -0
- package/src/app/[lang]/login/page.tsx +27 -0
- package/src/app/api/auth/login/route.ts +38 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +26 -0
- package/src/app/layout.tsx +9 -0
- package/src/app/page.tsx +26 -0
- package/src/components/Login.tsx +101 -0
- package/src/components/NewUiLogin.tsx +140 -0
- package/src/components/UiLogin.tsx +140 -0
- package/src/config/language.ts +6 -0
- package/src/dictionaries/en.json +9 -0
- package/src/dictionaries/fa.json +9 -0
- package/src/dictionaries/index.ts +11 -0
- package/src/index.ts +1 -0
- package/src/types.ts +11 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +33 -0
- package/tsconfig.server.json +7 -0
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
|
2
|
+
|
|
3
|
+
## Getting Started
|
|
4
|
+
|
|
5
|
+
First, run the development server:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm run dev
|
|
9
|
+
# or
|
|
10
|
+
yarn dev
|
|
11
|
+
# or
|
|
12
|
+
pnpm dev
|
|
13
|
+
# or
|
|
14
|
+
bun dev
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
|
18
|
+
|
|
19
|
+
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
|
20
|
+
|
|
21
|
+
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
|
22
|
+
|
|
23
|
+
## Learn More
|
|
24
|
+
|
|
25
|
+
To learn more about Next.js, take a look at the following resources:
|
|
26
|
+
|
|
27
|
+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
|
28
|
+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
29
|
+
|
|
30
|
+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
|
31
|
+
|
|
32
|
+
## Deploy on Vercel
|
|
33
|
+
|
|
34
|
+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
|
35
|
+
|
|
36
|
+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { LoginProps } from "../types";
|
|
3
|
+
interface LoginComponentProps {
|
|
4
|
+
onSubmit: (formData: FormData) => void;
|
|
5
|
+
errors?: {
|
|
6
|
+
username?: string;
|
|
7
|
+
password?: string;
|
|
8
|
+
};
|
|
9
|
+
message?: string | null;
|
|
10
|
+
}
|
|
11
|
+
export default function Login({ loginRoute, onGoogleLogin, UiComponent, }: LoginProps & {
|
|
12
|
+
UiComponent?: React.ComponentType<LoginComponentProps>;
|
|
13
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
// Components
|
|
5
|
+
import UiLogin from "./UiLogin";
|
|
6
|
+
export default function Login({ loginRoute, onGoogleLogin, UiComponent, // ⬅️ اضافه شد
|
|
7
|
+
}) {
|
|
8
|
+
// states
|
|
9
|
+
const [errors, setErrors] = useState({});
|
|
10
|
+
const [message, setMessage] = useState(null);
|
|
11
|
+
const [loading, setLoading] = useState(false);
|
|
12
|
+
// functions
|
|
13
|
+
const handleSubmit = async (formData) => {
|
|
14
|
+
setMessage(null);
|
|
15
|
+
setErrors({});
|
|
16
|
+
const username = formData.get("username") || "";
|
|
17
|
+
const password = formData.get("password") || "";
|
|
18
|
+
const newErrors = {};
|
|
19
|
+
if (!username.trim())
|
|
20
|
+
newErrors.username = "Username is required";
|
|
21
|
+
else if (username.length < 3)
|
|
22
|
+
newErrors.username = "Username must be at least 3 characters";
|
|
23
|
+
if (!password)
|
|
24
|
+
newErrors.password = "Password is required";
|
|
25
|
+
else if (password.length < 6)
|
|
26
|
+
newErrors.password = "Password must be at least 6 characters";
|
|
27
|
+
if (Object.keys(newErrors).length > 0) {
|
|
28
|
+
setErrors(newErrors);
|
|
29
|
+
setMessage("Please fix the errors below.");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
setLoading(true);
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(loginRoute, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "Content-Type": "application/json" },
|
|
37
|
+
body: JSON.stringify({ username, password }),
|
|
38
|
+
});
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
setMessage((data === null || data === void 0 ? void 0 : data.message) || "Login failed");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
setMessage((data === null || data === void 0 ? void 0 : data.message) || "Login successful");
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
setMessage((err === null || err === void 0 ? void 0 : err.message) || "Network error");
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
setLoading(false);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
// ✅ اگر UiComponent پاس داده شده باشه، اون استفاده میشه
|
|
54
|
+
const RenderUi = UiComponent || UiLogin;
|
|
55
|
+
return (_jsxs(_Fragment, { children: [_jsx(RenderUi, { onSubmit: handleSubmit, errors: errors, message: message }), loading && (_jsx("p", { className: "text-center text-gray-200 mt-2", children: "Logging in..." })), onGoogleLogin && (_jsx("div", { className: "flex justify-center mt-4", children: _jsx("button", { onClick: onGoogleLogin, className: "px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition", children: "Sign in with Google" }) }))] }));
|
|
56
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface ClientLoginProps {
|
|
2
|
+
onSubmit: (formData: FormData) => void;
|
|
3
|
+
errors?: {
|
|
4
|
+
username?: string;
|
|
5
|
+
password?: string;
|
|
6
|
+
};
|
|
7
|
+
message?: string | null;
|
|
8
|
+
}
|
|
9
|
+
export default function UiLogin({ onSubmit, errors, message, }: ClientLoginProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import Image from "next/image";
|
|
4
|
+
export default function UiLogin({ onSubmit, errors, message, }) {
|
|
5
|
+
return (_jsx("div", { className: "fixed inset-0 flex items-center justify-center bg-gradient-to-r from-gray-100 via-white to-gray-200 p-6", children: _jsxs("div", { className: "w-full max-w-lg bg-white rounded-3xl shadow-2xl overflow-hidden border border-gray-200", children: [_jsxs("div", { className: "flex flex-col items-center p-8 border-b border-gray-100 bg-gradient-to-b from-gray-50 to-white", children: [_jsx(Image, { src: "/logo.png", alt: "Logo", width: 90, height: 90, className: "mb-4 rounded-full shadow-md" }), _jsx("h1", { className: "text-3xl font-extrabold text-gray-800", children: "Welcome Back" }), _jsx("p", { className: "text-gray-500 text-sm mt-1", children: "Login with your credentials to continue" })] }), _jsxs("div", { className: "p-8", children: [message && (_jsx("div", { role: "alert", className: "mb-4 text-center text-red-500 font-medium", children: message })), _jsxs("form", { onSubmit: (e) => {
|
|
6
|
+
e.preventDefault();
|
|
7
|
+
const formData = new FormData(e.currentTarget);
|
|
8
|
+
onSubmit(formData);
|
|
9
|
+
}, noValidate: true, className: "space-y-6", children: [_jsxs("div", { children: [_jsx("label", { htmlFor: "username", className: "block text-sm font-semibold text-gray-700 mb-1", children: "Username" }), _jsx("input", { id: "username", name: "username", type: "text", placeholder: "Enter your username", required: true, className: `w-full px-4 py-3 rounded-lg bg-gray-50 border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-400 text-gray-800 placeholder-gray-400 ${(errors === null || errors === void 0 ? void 0 : errors.username) ? "border-red-500" : ""}` }), (errors === null || errors === void 0 ? void 0 : errors.username) && (_jsx("p", { className: "mt-1 text-sm text-red-500", children: errors.username }))] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "password", className: "block text-sm font-semibold text-gray-700 mb-1", children: "Password" }), _jsx("input", { id: "password", name: "password", type: "password", placeholder: "Enter your password", required: true, className: `w-full px-4 py-3 rounded-lg bg-gray-50 border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-400 text-gray-800 placeholder-gray-400 ${(errors === null || errors === void 0 ? void 0 : errors.password) ? "border-red-500" : ""}` }), (errors === null || errors === void 0 ? void 0 : errors.password) && (_jsx("p", { className: "mt-1 text-sm text-red-500", children: errors.password }))] }), _jsx("button", { type: "submit", className: "w-full py-3 bg-indigo-600 text-white font-semibold rounded-lg shadow hover:bg-indigo-700 transition", children: "Log In" }), _jsxs("div", { className: "flex justify-center gap-4 mt-6", children: [_jsx("button", { type: "button", className: "flex items-center justify-center w-11 h-11 rounded-full bg-red-500 hover:bg-red-600 text-white font-bold", children: "G" }), _jsx("button", { type: "button", className: "flex items-center justify-center w-11 h-11 rounded-full bg-blue-600 hover:bg-blue-700 text-white font-bold", children: "F" }), _jsx("button", { type: "button", className: "flex items-center justify-center w-11 h-11 rounded-full bg-sky-400 hover:bg-sky-500 text-white font-bold", children: "T" })] }), _jsxs("p", { className: "text-center text-sm text-gray-600 mt-5", children: ["Don\u2019t have an account?", " ", _jsx("a", { href: "#", className: "text-indigo-600 hover:underline", children: "Sign up" })] })] })] })] }) }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface ClientLoginProps {
|
|
2
|
+
onSubmit: (formData: FormData) => void;
|
|
3
|
+
errors?: {
|
|
4
|
+
username?: string;
|
|
5
|
+
password?: string;
|
|
6
|
+
};
|
|
7
|
+
message?: string | null;
|
|
8
|
+
}
|
|
9
|
+
export default function UiLogin({ onSubmit, errors, message, }: ClientLoginProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import Image from "next/image";
|
|
4
|
+
export default function UiLogin({ onSubmit, errors, message, }) {
|
|
5
|
+
return (_jsx("div", { className: "fixed inset-0 overflow-hidden flex items-center justify-center bg-gradient-to-br from-purple-900 via-purple-800 to-black p-4", children: _jsxs("div", { className: "w-full max-w-md bg-white/20 backdrop-blur-md rounded-2xl shadow-xl p-8 border border-white/30", children: [_jsx("div", { className: "flex justify-center mb-6", children: _jsx(Image, { src: "/logo.png", alt: "Logo", width: 80, height: 80, className: "rounded-full shadow" }) }), _jsx("h1", { className: "text-2xl font-bold text-center text-white mb-2", children: "Welcome Back" }), _jsx("p", { className: "text-center text-gray-200 mb-6 text-sm", children: "Login with your credentials to continue" }), message && (_jsx("div", { role: "alert", className: "mb-4 text-center text-red-300 font-medium", children: message })), _jsxs("form", { onSubmit: (e) => {
|
|
6
|
+
e.preventDefault();
|
|
7
|
+
const formData = new FormData(e.currentTarget);
|
|
8
|
+
onSubmit(formData);
|
|
9
|
+
}, noValidate: true, className: "space-y-5", children: [_jsxs("div", { children: [_jsx("label", { htmlFor: "username", className: "block text-sm font-medium text-white mb-1", children: "Username" }), _jsx("input", { id: "username", name: "username", type: "text", placeholder: "Enter your username", required: true, className: `w-full px-4 py-2 rounded-lg bg-white/20 focus:bg-white/30 border focus:outline-none focus:ring-2 border-gray-300 focus:ring-purple-400 text-white placeholder-gray-200 ${(errors === null || errors === void 0 ? void 0 : errors.username) ? "border-red-500" : ""}` }), (errors === null || errors === void 0 ? void 0 : errors.username) && (_jsx("p", { className: "mt-1 text-sm text-red-300", children: errors.username }))] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "password", className: "block text-sm font-medium text-white mb-1", children: "Password" }), _jsx("input", { id: "password", name: "password", type: "password", placeholder: "Enter your password", required: true, className: `w-full px-4 py-2 rounded-lg bg-white/20 focus:bg-white/30 border focus:outline-none focus:ring-2 border-gray-300 focus:ring-purple-400 text-white placeholder-gray-200 ${(errors === null || errors === void 0 ? void 0 : errors.password) ? "border-red-500" : ""}` }), (errors === null || errors === void 0 ? void 0 : errors.password) && (_jsx("p", { className: "mt-1 text-sm text-red-300", children: errors.password }))] }), _jsx("button", { type: "submit", className: "w-full py-2 bg-purple-700/80 text-white rounded-lg hover:bg-purple-800 transition", children: "Log In" }), _jsxs("div", { className: "flex justify-center gap-4 mt-4", children: [_jsx("button", { type: "button", className: "flex items-center justify-center w-10 h-10 rounded-full bg-red-500 hover:bg-red-600 text-white transition font-bold text-lg", children: "G" }), _jsx("button", { type: "button", className: "flex items-center justify-center w-10 h-10 rounded-full bg-blue-600 hover:bg-blue-700 text-white transition font-bold text-lg", children: "F" }), _jsx("button", { type: "button", className: "flex items-center justify-center w-10 h-10 rounded-full bg-blue-400 hover:bg-blue-500 text-white transition font-bold text-lg", children: "T" })] }), _jsxs("p", { className: "text-center text-sm text-gray-200 mt-4", children: ["Don\u2019t have an account?", " ", _jsx("a", { href: "#", className: "underline hover:text-white", children: "Sign up" })] })] })] }) }));
|
|
10
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Login } from "./components/Login";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Login } from "./components/Login";
|
package/dist/types.d.ts
ADDED
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { dirname } from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { FlatCompat } from "@eslint/eslintrc";
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
|
|
8
|
+
const compat = new FlatCompat({
|
|
9
|
+
baseDirectory: __dirname,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const eslintConfig = [
|
|
13
|
+
...compat.extends("next/core-web-vitals", "next/typescript"),
|
|
14
|
+
{
|
|
15
|
+
ignores: [
|
|
16
|
+
"node_modules/**",
|
|
17
|
+
".next/**",
|
|
18
|
+
"out/**",
|
|
19
|
+
"build/**",
|
|
20
|
+
"next-env.d.ts",
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export default eslintConfig;
|
package/next.config.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "next-auth-mfelfelani72",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev --turbopack",
|
|
7
|
+
"build": "npm run build:dist",
|
|
8
|
+
"build:dist": "tsc -p tsconfig.build.json",
|
|
9
|
+
"build:server": "tsc -p tsconfig.server.json",
|
|
10
|
+
"start": "next start",
|
|
11
|
+
"lint": "eslint"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"cookie": "^1.0.2",
|
|
15
|
+
"next": "15.5.4",
|
|
16
|
+
"react": "19.1.0",
|
|
17
|
+
"react-dom": "19.1.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@eslint/eslintrc": "^3",
|
|
21
|
+
"@tailwindcss/postcss": "^4",
|
|
22
|
+
"@types/node": "^20",
|
|
23
|
+
"@types/react": "^19",
|
|
24
|
+
"@types/react-dom": "^19",
|
|
25
|
+
"eslint": "^9",
|
|
26
|
+
"eslint-config-next": "15.5.4",
|
|
27
|
+
"tailwindcss": "^4",
|
|
28
|
+
"typescript": "^5"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/public/file.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
package/public/globe.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
package/public/next.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { Geist } from "next/font/google";
|
|
3
|
+
|
|
4
|
+
// Constants and Interface
|
|
5
|
+
|
|
6
|
+
import { languages, Lang } from "../../config/language";
|
|
7
|
+
|
|
8
|
+
const geistSans = Geist({
|
|
9
|
+
variable: "--font-geist-sans",
|
|
10
|
+
subsets: ["latin"],
|
|
11
|
+
display: "swap",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
interface LangLayoutProps {
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
params: { lang: string } | Promise<{ lang: string }>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Functions
|
|
20
|
+
|
|
21
|
+
export async function generateStaticParams() {
|
|
22
|
+
return Object.keys(languages).map((lang) => ({ lang }));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default async function LangLayout({
|
|
26
|
+
children,
|
|
27
|
+
params,
|
|
28
|
+
}: LangLayoutProps) {
|
|
29
|
+
// const
|
|
30
|
+
|
|
31
|
+
const { lang: paramLang } = await params;
|
|
32
|
+
const lang = (paramLang in languages ? paramLang : "en") as Lang;
|
|
33
|
+
const dir = languages[lang].dir;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<html lang={lang} dir={dir} className={geistSans.variable}>
|
|
37
|
+
<body>{children}</body>
|
|
38
|
+
</html>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
|
|
3
|
+
export const metadata: Metadata = {
|
|
4
|
+
title: "Login",
|
|
5
|
+
description: "Login page",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// Components
|
|
9
|
+
|
|
10
|
+
import Login from "@/components/Login";
|
|
11
|
+
import NewUiLogin from "@/components/NewUiLogin";
|
|
12
|
+
|
|
13
|
+
// Interfaces
|
|
14
|
+
|
|
15
|
+
import type { LoginProps } from "@/types";
|
|
16
|
+
|
|
17
|
+
export default function Page({ loginRoute, onGoogleLogin }: LoginProps) {
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
<Login
|
|
21
|
+
loginRoute={"/api/auth/login"}
|
|
22
|
+
onGoogleLogin={onGoogleLogin}
|
|
23
|
+
UiComponent={NewUiLogin}
|
|
24
|
+
/>
|
|
25
|
+
</>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import cookie from "cookie";
|
|
3
|
+
|
|
4
|
+
export async function POST(req: NextRequest) {
|
|
5
|
+
try {
|
|
6
|
+
const body = await req.json();
|
|
7
|
+
|
|
8
|
+
const response = await fetch(`${process.env.API_URL}/login`, {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: { "Content-Type": "application/json" },
|
|
11
|
+
body: JSON.stringify(body),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const data = await response.json();
|
|
15
|
+
|
|
16
|
+
if (response.ok && data.accessToken) {
|
|
17
|
+
const headers = new Headers();
|
|
18
|
+
headers.append(
|
|
19
|
+
"Set-Cookie",
|
|
20
|
+
cookie.serialize("accessToken", data.accessToken, {
|
|
21
|
+
httpOnly: true,
|
|
22
|
+
secure: process.env.NODE_ENV === "production",
|
|
23
|
+
sameSite: "strict",
|
|
24
|
+
path: "/",
|
|
25
|
+
maxAge: 60 * 15, // 15 دقیقه
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
return NextResponse.json({ message: "Login successful" }, { headers });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return NextResponse.json(
|
|
32
|
+
{ message: data.message || "Login failed" },
|
|
33
|
+
{ status: response.status }
|
|
34
|
+
);
|
|
35
|
+
} catch (err: any) {
|
|
36
|
+
return NextResponse.json({ message: err.message }, { status: 500 });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--background: #ffffff;
|
|
5
|
+
--foreground: #171717;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
@theme inline {
|
|
9
|
+
--color-background: var(--background);
|
|
10
|
+
--color-foreground: var(--foreground);
|
|
11
|
+
--font-sans: var(--font-geist-sans);
|
|
12
|
+
--font-mono: var(--font-geist-mono);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@media (prefers-color-scheme: dark) {
|
|
16
|
+
:root {
|
|
17
|
+
--background: #0a0a0a;
|
|
18
|
+
--foreground: #ededed;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
body {
|
|
23
|
+
background: var(--background);
|
|
24
|
+
color: var(--foreground);
|
|
25
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
26
|
+
}
|
package/src/app/page.tsx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { redirect } from "next/navigation";
|
|
2
|
+
import { headers } from "next/headers";
|
|
3
|
+
|
|
4
|
+
// Constants
|
|
5
|
+
|
|
6
|
+
const locales = ["en", "fa"] as const;
|
|
7
|
+
const defaultLocale = "en";
|
|
8
|
+
|
|
9
|
+
export default async function RootPage() {
|
|
10
|
+
// const
|
|
11
|
+
const headerList = await headers();
|
|
12
|
+
const acceptLang = headerList.get("accept-language") || "";
|
|
13
|
+
const langs = acceptLang.split(",").map((l) => l.split(";")[0].trim());
|
|
14
|
+
|
|
15
|
+
let locale = defaultLocale;
|
|
16
|
+
|
|
17
|
+
for (const lang of langs) {
|
|
18
|
+
const base = lang.split("-")[0];
|
|
19
|
+
if (locales.includes(base as (typeof locales)[number])) {
|
|
20
|
+
locale = base;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
redirect(`/${locale}/${process.env.BASE_ROUTE}`);
|
|
26
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
|
|
5
|
+
// Components
|
|
6
|
+
import UiLogin from "./UiLogin";
|
|
7
|
+
|
|
8
|
+
// Interfaces
|
|
9
|
+
import type { LoginProps } from "../types";
|
|
10
|
+
|
|
11
|
+
interface LoginComponentProps {
|
|
12
|
+
onSubmit: (formData: FormData) => void;
|
|
13
|
+
errors?: { username?: string; password?: string };
|
|
14
|
+
message?: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function Login({
|
|
18
|
+
loginRoute,
|
|
19
|
+
onGoogleLogin,
|
|
20
|
+
UiComponent, // ⬅️ اضافه شد
|
|
21
|
+
}: LoginProps & { UiComponent?: React.ComponentType<LoginComponentProps> }) {
|
|
22
|
+
// states
|
|
23
|
+
const [errors, setErrors] = useState<{
|
|
24
|
+
username?: string;
|
|
25
|
+
password?: string;
|
|
26
|
+
}>({});
|
|
27
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
28
|
+
const [loading, setLoading] = useState(false);
|
|
29
|
+
|
|
30
|
+
// functions
|
|
31
|
+
const handleSubmit = async (formData: FormData) => {
|
|
32
|
+
setMessage(null);
|
|
33
|
+
setErrors({});
|
|
34
|
+
|
|
35
|
+
const username = (formData.get("username") as string) || "";
|
|
36
|
+
const password = (formData.get("password") as string) || "";
|
|
37
|
+
|
|
38
|
+
const newErrors: typeof errors = {};
|
|
39
|
+
|
|
40
|
+
if (!username.trim()) newErrors.username = "Username is required";
|
|
41
|
+
else if (username.length < 3)
|
|
42
|
+
newErrors.username = "Username must be at least 3 characters";
|
|
43
|
+
|
|
44
|
+
if (!password) newErrors.password = "Password is required";
|
|
45
|
+
else if (password.length < 6)
|
|
46
|
+
newErrors.password = "Password must be at least 6 characters";
|
|
47
|
+
|
|
48
|
+
if (Object.keys(newErrors).length > 0) {
|
|
49
|
+
setErrors(newErrors);
|
|
50
|
+
setMessage("Please fix the errors below.");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setLoading(true);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch(loginRoute, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { "Content-Type": "application/json" },
|
|
60
|
+
body: JSON.stringify({ username, password }),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const data = await res.json();
|
|
64
|
+
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
setMessage(data?.message || "Login failed");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setMessage(data?.message || "Login successful");
|
|
71
|
+
} catch (err: any) {
|
|
72
|
+
setMessage(err?.message || "Network error");
|
|
73
|
+
} finally {
|
|
74
|
+
setLoading(false);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// ✅ اگر UiComponent پاس داده شده باشه، اون استفاده میشه
|
|
79
|
+
const RenderUi = UiComponent || UiLogin;
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<>
|
|
83
|
+
<RenderUi onSubmit={handleSubmit} errors={errors} message={message} />
|
|
84
|
+
|
|
85
|
+
{loading && (
|
|
86
|
+
<p className="text-center text-gray-200 mt-2">Logging in...</p>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{onGoogleLogin && (
|
|
90
|
+
<div className="flex justify-center mt-4">
|
|
91
|
+
<button
|
|
92
|
+
onClick={onGoogleLogin}
|
|
93
|
+
className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition"
|
|
94
|
+
>
|
|
95
|
+
Sign in with Google
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
</>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Image from "next/image";
|
|
4
|
+
|
|
5
|
+
interface ClientLoginProps {
|
|
6
|
+
onSubmit: (formData: FormData) => void;
|
|
7
|
+
errors?: { username?: string; password?: string };
|
|
8
|
+
message?: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function UiLogin({
|
|
12
|
+
onSubmit,
|
|
13
|
+
errors,
|
|
14
|
+
message,
|
|
15
|
+
}: ClientLoginProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div className="fixed inset-0 flex items-center justify-center bg-gradient-to-r from-gray-100 via-white to-gray-200 p-6">
|
|
18
|
+
<div className="w-full max-w-lg bg-white rounded-3xl shadow-2xl overflow-hidden border border-gray-200">
|
|
19
|
+
{/* Header */}
|
|
20
|
+
<div className="flex flex-col items-center p-8 border-b border-gray-100 bg-gradient-to-b from-gray-50 to-white">
|
|
21
|
+
<Image
|
|
22
|
+
src="/logo.png"
|
|
23
|
+
alt="Logo"
|
|
24
|
+
width={90}
|
|
25
|
+
height={90}
|
|
26
|
+
className="mb-4 rounded-full shadow-md"
|
|
27
|
+
/>
|
|
28
|
+
<h1 className="text-3xl font-extrabold text-gray-800">
|
|
29
|
+
Welcome Back
|
|
30
|
+
</h1>
|
|
31
|
+
<p className="text-gray-500 text-sm mt-1">
|
|
32
|
+
Login with your credentials to continue
|
|
33
|
+
</p>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
{/* Body */}
|
|
37
|
+
<div className="p-8">
|
|
38
|
+
{message && (
|
|
39
|
+
<div
|
|
40
|
+
role="alert"
|
|
41
|
+
className="mb-4 text-center text-red-500 font-medium"
|
|
42
|
+
>
|
|
43
|
+
{message}
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
|
|
47
|
+
<form
|
|
48
|
+
onSubmit={(e) => {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
const formData = new FormData(e.currentTarget);
|
|
51
|
+
onSubmit(formData);
|
|
52
|
+
}}
|
|
53
|
+
noValidate
|
|
54
|
+
className="space-y-6"
|
|
55
|
+
>
|
|
56
|
+
<div>
|
|
57
|
+
<label
|
|
58
|
+
htmlFor="username"
|
|
59
|
+
className="block text-sm font-semibold text-gray-700 mb-1"
|
|
60
|
+
>
|
|
61
|
+
Username
|
|
62
|
+
</label>
|
|
63
|
+
<input
|
|
64
|
+
id="username"
|
|
65
|
+
name="username"
|
|
66
|
+
type="text"
|
|
67
|
+
placeholder="Enter your username"
|
|
68
|
+
required
|
|
69
|
+
className={`w-full px-4 py-3 rounded-lg bg-gray-50 border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-400 text-gray-800 placeholder-gray-400 ${
|
|
70
|
+
errors?.username ? "border-red-500" : ""
|
|
71
|
+
}`}
|
|
72
|
+
/>
|
|
73
|
+
{errors?.username && (
|
|
74
|
+
<p className="mt-1 text-sm text-red-500">{errors.username}</p>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div>
|
|
79
|
+
<label
|
|
80
|
+
htmlFor="password"
|
|
81
|
+
className="block text-sm font-semibold text-gray-700 mb-1"
|
|
82
|
+
>
|
|
83
|
+
Password
|
|
84
|
+
</label>
|
|
85
|
+
<input
|
|
86
|
+
id="password"
|
|
87
|
+
name="password"
|
|
88
|
+
type="password"
|
|
89
|
+
placeholder="Enter your password"
|
|
90
|
+
required
|
|
91
|
+
className={`w-full px-4 py-3 rounded-lg bg-gray-50 border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-400 text-gray-800 placeholder-gray-400 ${
|
|
92
|
+
errors?.password ? "border-red-500" : ""
|
|
93
|
+
}`}
|
|
94
|
+
/>
|
|
95
|
+
{errors?.password && (
|
|
96
|
+
<p className="mt-1 text-sm text-red-500">{errors.password}</p>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<button
|
|
101
|
+
type="submit"
|
|
102
|
+
className="w-full py-3 bg-indigo-600 text-white font-semibold rounded-lg shadow hover:bg-indigo-700 transition"
|
|
103
|
+
>
|
|
104
|
+
Log In
|
|
105
|
+
</button>
|
|
106
|
+
|
|
107
|
+
{/* Social Login */}
|
|
108
|
+
<div className="flex justify-center gap-4 mt-6">
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
className="flex items-center justify-center w-11 h-11 rounded-full bg-red-500 hover:bg-red-600 text-white font-bold"
|
|
112
|
+
>
|
|
113
|
+
G
|
|
114
|
+
</button>
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
className="flex items-center justify-center w-11 h-11 rounded-full bg-blue-600 hover:bg-blue-700 text-white font-bold"
|
|
118
|
+
>
|
|
119
|
+
F
|
|
120
|
+
</button>
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
className="flex items-center justify-center w-11 h-11 rounded-full bg-sky-400 hover:bg-sky-500 text-white font-bold"
|
|
124
|
+
>
|
|
125
|
+
T
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<p className="text-center text-sm text-gray-600 mt-5">
|
|
130
|
+
Don’t have an account?{" "}
|
|
131
|
+
<a href="#" className="text-indigo-600 hover:underline">
|
|
132
|
+
Sign up
|
|
133
|
+
</a>
|
|
134
|
+
</p>
|
|
135
|
+
</form>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Image from "next/image";
|
|
4
|
+
|
|
5
|
+
interface ClientLoginProps {
|
|
6
|
+
onSubmit: (formData: FormData) => void;
|
|
7
|
+
errors?: { username?: string; password?: string };
|
|
8
|
+
message?: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function UiLogin({
|
|
12
|
+
onSubmit,
|
|
13
|
+
errors,
|
|
14
|
+
message,
|
|
15
|
+
}: ClientLoginProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div className="fixed inset-0 overflow-hidden flex items-center justify-center bg-gradient-to-br from-purple-900 via-purple-800 to-black p-4">
|
|
18
|
+
<div className="w-full max-w-md bg-white/20 backdrop-blur-md rounded-2xl shadow-xl p-8 border border-white/30">
|
|
19
|
+
{/* Picture / Logo */}
|
|
20
|
+
<div className="flex justify-center mb-6">
|
|
21
|
+
<Image
|
|
22
|
+
src="/logo.png"
|
|
23
|
+
alt="Logo"
|
|
24
|
+
width={80}
|
|
25
|
+
height={80}
|
|
26
|
+
className="rounded-full shadow"
|
|
27
|
+
/>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
{/* Title */}
|
|
31
|
+
<h1 className="text-2xl font-bold text-center text-white mb-2">
|
|
32
|
+
Welcome Back
|
|
33
|
+
</h1>
|
|
34
|
+
<p className="text-center text-gray-200 mb-6 text-sm">
|
|
35
|
+
Login with your credentials to continue
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
{message && (
|
|
39
|
+
<div
|
|
40
|
+
role="alert"
|
|
41
|
+
className="mb-4 text-center text-red-300 font-medium"
|
|
42
|
+
>
|
|
43
|
+
{message}
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
|
|
47
|
+
{/* Login Form */}
|
|
48
|
+
<form
|
|
49
|
+
onSubmit={(e) => {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
const formData = new FormData(e.currentTarget);
|
|
52
|
+
onSubmit(formData);
|
|
53
|
+
}}
|
|
54
|
+
noValidate
|
|
55
|
+
className="space-y-5"
|
|
56
|
+
>
|
|
57
|
+
<div>
|
|
58
|
+
<label
|
|
59
|
+
htmlFor="username"
|
|
60
|
+
className="block text-sm font-medium text-white mb-1"
|
|
61
|
+
>
|
|
62
|
+
Username
|
|
63
|
+
</label>
|
|
64
|
+
<input
|
|
65
|
+
id="username"
|
|
66
|
+
name="username"
|
|
67
|
+
type="text"
|
|
68
|
+
placeholder="Enter your username"
|
|
69
|
+
required
|
|
70
|
+
className={`w-full px-4 py-2 rounded-lg bg-white/20 focus:bg-white/30 border focus:outline-none focus:ring-2 border-gray-300 focus:ring-purple-400 text-white placeholder-gray-200 ${
|
|
71
|
+
errors?.username ? "border-red-500" : ""
|
|
72
|
+
}`}
|
|
73
|
+
/>
|
|
74
|
+
{errors?.username && (
|
|
75
|
+
<p className="mt-1 text-sm text-red-300">{errors.username}</p>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div>
|
|
80
|
+
<label
|
|
81
|
+
htmlFor="password"
|
|
82
|
+
className="block text-sm font-medium text-white mb-1"
|
|
83
|
+
>
|
|
84
|
+
Password
|
|
85
|
+
</label>
|
|
86
|
+
<input
|
|
87
|
+
id="password"
|
|
88
|
+
name="password"
|
|
89
|
+
type="password"
|
|
90
|
+
placeholder="Enter your password"
|
|
91
|
+
required
|
|
92
|
+
className={`w-full px-4 py-2 rounded-lg bg-white/20 focus:bg-white/30 border focus:outline-none focus:ring-2 border-gray-300 focus:ring-purple-400 text-white placeholder-gray-200 ${
|
|
93
|
+
errors?.password ? "border-red-500" : ""
|
|
94
|
+
}`}
|
|
95
|
+
/>
|
|
96
|
+
{errors?.password && (
|
|
97
|
+
<p className="mt-1 text-sm text-red-300">{errors.password}</p>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<button
|
|
102
|
+
type="submit"
|
|
103
|
+
className="w-full py-2 bg-purple-700/80 text-white rounded-lg hover:bg-purple-800 transition"
|
|
104
|
+
>
|
|
105
|
+
Log In
|
|
106
|
+
</button>
|
|
107
|
+
|
|
108
|
+
{/* Social Login Buttons */}
|
|
109
|
+
<div className="flex justify-center gap-4 mt-4">
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
className="flex items-center justify-center w-10 h-10 rounded-full bg-red-500 hover:bg-red-600 text-white transition font-bold text-lg"
|
|
113
|
+
>
|
|
114
|
+
G
|
|
115
|
+
</button>
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
className="flex items-center justify-center w-10 h-10 rounded-full bg-blue-600 hover:bg-blue-700 text-white transition font-bold text-lg"
|
|
119
|
+
>
|
|
120
|
+
F
|
|
121
|
+
</button>
|
|
122
|
+
<button
|
|
123
|
+
type="button"
|
|
124
|
+
className="flex items-center justify-center w-10 h-10 rounded-full bg-blue-400 hover:bg-blue-500 text-white transition font-bold text-lg"
|
|
125
|
+
>
|
|
126
|
+
T
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<p className="text-center text-sm text-gray-200 mt-4">
|
|
131
|
+
Don’t have an account?{" "}
|
|
132
|
+
<a href="#" className="underline hover:text-white">
|
|
133
|
+
Sign up
|
|
134
|
+
</a>
|
|
135
|
+
</p>
|
|
136
|
+
</form>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Login } from "./components/Login";
|
package/src/types.ts
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./src/*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": [
|
|
26
|
+
"next-env.d.ts",
|
|
27
|
+
"**/*.ts",
|
|
28
|
+
"**/*.tsx",
|
|
29
|
+
".next/types/**/*.ts",
|
|
30
|
+
"tailwind.config.js"
|
|
31
|
+
],
|
|
32
|
+
"exclude": ["node_modules"]
|
|
33
|
+
}
|