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.
Files changed (46) hide show
  1. package/README.md +36 -0
  2. package/dist/components/Login.d.ts +14 -0
  3. package/dist/components/Login.js +56 -0
  4. package/dist/components/NewUiLogin.d.ts +10 -0
  5. package/dist/components/NewUiLogin.js +10 -0
  6. package/dist/components/UiLogin.d.ts +10 -0
  7. package/dist/components/UiLogin.js +10 -0
  8. package/dist/config/language.d.ts +11 -0
  9. package/dist/config/language.js +4 -0
  10. package/dist/dictionaries/en.json +8 -0
  11. package/dist/dictionaries/fa.json +8 -0
  12. package/dist/dictionaries/index.d.ts +15 -0
  13. package/dist/dictionaries/index.js +10 -0
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +1 -0
  16. package/dist/types.d.ts +10 -0
  17. package/dist/types.js +1 -0
  18. package/eslint.config.mjs +25 -0
  19. package/next.config.ts +7 -0
  20. package/package.json +30 -0
  21. package/pnpm-workspace.yaml +4 -0
  22. package/postcss.config.mjs +5 -0
  23. package/public/file.svg +1 -0
  24. package/public/globe.svg +1 -0
  25. package/public/next.svg +1 -0
  26. package/public/vercel.svg +1 -0
  27. package/public/window.svg +1 -0
  28. package/src/app/[lang]/layout.tsx +40 -0
  29. package/src/app/[lang]/login/page.tsx +27 -0
  30. package/src/app/api/auth/login/route.ts +38 -0
  31. package/src/app/favicon.ico +0 -0
  32. package/src/app/globals.css +26 -0
  33. package/src/app/layout.tsx +9 -0
  34. package/src/app/page.tsx +26 -0
  35. package/src/components/Login.tsx +101 -0
  36. package/src/components/NewUiLogin.tsx +140 -0
  37. package/src/components/UiLogin.tsx +140 -0
  38. package/src/config/language.ts +6 -0
  39. package/src/dictionaries/en.json +9 -0
  40. package/src/dictionaries/fa.json +9 -0
  41. package/src/dictionaries/index.ts +11 -0
  42. package/src/index.ts +1 -0
  43. package/src/types.ts +11 -0
  44. package/tsconfig.build.json +11 -0
  45. package/tsconfig.json +33 -0
  46. 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
+ }
@@ -0,0 +1,11 @@
1
+ export declare const languages: {
2
+ readonly en: {
3
+ readonly dir: "ltr";
4
+ readonly name: "English";
5
+ };
6
+ readonly fa: {
7
+ readonly dir: "rtl";
8
+ readonly name: "فارسی";
9
+ };
10
+ };
11
+ export type Lang = keyof typeof languages;
@@ -0,0 +1,4 @@
1
+ export const languages = {
2
+ en: { dir: "ltr", name: "English" },
3
+ fa: { dir: "rtl", name: "فارسی" },
4
+ };
@@ -0,0 +1,8 @@
1
+ {
2
+ "login": {
3
+ "title": "login title"
4
+ },
5
+ "register": {
6
+ "title": "register title"
7
+ }
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "login": {
3
+ "title": "عنوان لاگین"
4
+ },
5
+ "register": {
6
+ "title": "عنوان ثبت نام"
7
+ }
8
+ }
@@ -0,0 +1,15 @@
1
+ export declare function getDictionary(lang: "en" | "fa"): Promise<{
2
+ login: {
3
+ title: string;
4
+ };
5
+ register: {
6
+ title: string;
7
+ };
8
+ } | {
9
+ login: {
10
+ title: string;
11
+ };
12
+ register: {
13
+ title: string;
14
+ };
15
+ }>;
@@ -0,0 +1,10 @@
1
+ import en from "./en.json";
2
+ import fa from "./fa.json";
3
+ const dictionaries = {
4
+ en,
5
+ fa,
6
+ };
7
+ export async function getDictionary(lang) {
8
+ var _a;
9
+ return (_a = dictionaries[lang]) !== null && _a !== void 0 ? _a : dictionaries["en"];
10
+ }
@@ -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";
@@ -0,0 +1,10 @@
1
+ export interface LoginMetadata {
2
+ title?: string;
3
+ description?: string;
4
+ logoUrl?: string;
5
+ }
6
+ export interface LoginProps {
7
+ loginRoute: string;
8
+ metadata?: LoginMetadata;
9
+ onGoogleLogin?: () => void;
10
+ }
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
@@ -0,0 +1,7 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ /* config options here */
5
+ };
6
+
7
+ export default nextConfig;
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
+ }
@@ -0,0 +1,4 @@
1
+ onlyBuiltDependencies:
2
+ - '@tailwindcss/oxide'
3
+ - sharp
4
+ - unrs-resolver
@@ -0,0 +1,5 @@
1
+ const config = {
2
+ plugins: ["@tailwindcss/postcss"],
3
+ };
4
+
5
+ export default config;
@@ -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>
@@ -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>
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ import "./globals.css";
2
+
3
+ export default function RootLayout({
4
+ children,
5
+ }: {
6
+ children: React.ReactNode;
7
+ }) {
8
+ return <>{children}</>;
9
+ }
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ export const languages = {
2
+ en: { dir: "ltr", name: "English" },
3
+ fa: { dir: "rtl", name: "فارسی" },
4
+ } as const;
5
+
6
+ export type Lang = keyof typeof languages;
@@ -0,0 +1,9 @@
1
+ {
2
+ "login": {
3
+ "title": "login title"
4
+ },
5
+
6
+ "register": {
7
+ "title": "register title"
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "login": {
3
+ "title": "عنوان لاگین"
4
+ },
5
+
6
+ "register": {
7
+ "title": "عنوان ثبت نام"
8
+ }
9
+ }
@@ -0,0 +1,11 @@
1
+ import en from "./en.json";
2
+ import fa from "./fa.json";
3
+
4
+ const dictionaries = {
5
+ en,
6
+ fa,
7
+ };
8
+
9
+ export async function getDictionary(lang: "en" | "fa") {
10
+ return dictionaries[lang] ?? dictionaries["en"];
11
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default as Login } from "./components/Login";
package/src/types.ts ADDED
@@ -0,0 +1,11 @@
1
+ export interface LoginMetadata {
2
+ title?: string;
3
+ description?: string;
4
+ logoUrl?: string;
5
+ }
6
+
7
+ export interface LoginProps {
8
+ loginRoute: string;
9
+ metadata?: LoginMetadata;
10
+ onGoogleLogin?: () => void;
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": false,
5
+ "declaration": true,
6
+ "outDir": "dist",
7
+ "jsx": "react-jsx"
8
+ },
9
+ "include": ["src"],
10
+ "exclude": ["app", "**/app/**", "next.config.*", "*.test.*", "*.spec.*"]
11
+ }
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
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist"
5
+ },
6
+ "exclude": ["next.config.*", "*.test.*", "*.spec.*"]
7
+ }