first commit
This commit is contained in:
commit
f9de1c9090
43
.gitignore
vendored
Normal file
43
.gitignore
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
.vercel
|
||||||
36
README.md
Normal file
36
README.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
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).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||||
14
eslint.config.mjs
Normal file
14
eslint.config.mjs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { dirname } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { FlatCompat } from "@eslint/eslintrc";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
});
|
||||||
|
|
||||||
|
const eslintConfig = [...compat.extends("next/core-web-vitals")];
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
7
jsconfig.json
Normal file
7
jsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
next.config.mjs
Normal file
4
next.config.mjs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
6003
package-lock.json
generated
Normal file
6003
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
package.json
Normal file
25
package.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "employee_portal",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --turbopack",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.7",
|
||||||
|
"appwrite": "^18.1.1",
|
||||||
|
"next": "15.1.8",
|
||||||
|
"postcss": "^8.5.3",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"tailwindcss": "^4.1.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3",
|
||||||
|
"eslint": "^9",
|
||||||
|
"eslint-config-next": "15.1.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.mjs
Normal file
6
postcss.config.mjs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
1
public/file.svg
Normal file
1
public/file.svg
Normal file
@ -0,0 +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>
|
||||||
|
After Width: | Height: | Size: 391 B |
1
public/globe.svg
Normal file
1
public/globe.svg
Normal file
@ -0,0 +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>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
1
public/next.svg
Normal file
1
public/next.svg
Normal file
@ -0,0 +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>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
public/vercel.svg
Normal file
1
public/vercel.svg
Normal file
@ -0,0 +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>
|
||||||
|
After Width: | Height: | Size: 128 B |
1
public/window.svg
Normal file
1
public/window.svg
Normal file
@ -0,0 +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>
|
||||||
|
After Width: | Height: | Size: 385 B |
475
src/app/components/AuthForm.js
Normal file
475
src/app/components/AuthForm.js
Normal file
@ -0,0 +1,475 @@
|
|||||||
|
'use client';
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useAuth } from "../context/AuthContext";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
// import { ID } from "../lib/appwrite";
|
||||||
|
|
||||||
|
export default function AuthPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { user, loading: authLoading, isAuthenticated, login, register,sendPasswordResetEmail, resetPassword } = useAuth();
|
||||||
|
const [isLoginForm, setIsLoginForm] = useState(true);
|
||||||
|
const [darkMode, setDarkMode] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [showResetForm, setShowResetForm] = useState(false);
|
||||||
|
const [resetEmail, setResetEmail] = useState("");
|
||||||
|
const [resetSent, setResetSent] = useState(false);
|
||||||
|
// Form states
|
||||||
|
const [loginData, setLoginData] = useState({
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
rememberMe: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [signupData, setSignupData] = useState({
|
||||||
|
fname: "",
|
||||||
|
lname: "",
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
agreeTerms: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState({
|
||||||
|
agreeTerms: "",
|
||||||
|
general: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------reset password------------------------
|
||||||
|
const handleForgotPassword = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setErrors({ ...errors, general: "" });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendPasswordResetEmail(resetEmail);
|
||||||
|
setResetSent(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Reset error:", error);
|
||||||
|
setErrors({
|
||||||
|
...errors,
|
||||||
|
general: "Failed to send reset email. Please try again."
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// ------------------------------------------
|
||||||
|
// Redirect if already authenticated
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authLoading && isAuthenticated) {
|
||||||
|
router.push("/pages/dashboard");
|
||||||
|
}
|
||||||
|
}, [isAuthenticated, authLoading, router]);
|
||||||
|
|
||||||
|
const handleLoginChange = (e) => {
|
||||||
|
const { name, value, type, checked } = e.target;
|
||||||
|
setLoginData({
|
||||||
|
...loginData,
|
||||||
|
[name]: type === "checkbox" ? checked : value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSignupChange = (e) => {
|
||||||
|
const { name, value, type, checked } = e.target;
|
||||||
|
setSignupData({
|
||||||
|
...signupData,
|
||||||
|
[name]: type === "checkbox" ? checked : value,
|
||||||
|
});
|
||||||
|
if (name === "agreeTerms") {
|
||||||
|
setErrors({ ...errors, agreeTerms: "" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateSignupForm = () => {
|
||||||
|
let isValid = true;
|
||||||
|
const newErrors = { ...errors };
|
||||||
|
|
||||||
|
if (!signupData.agreeTerms) {
|
||||||
|
newErrors.agreeTerms = "You must agree to the terms and conditions";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signupData.password.length < 8) {
|
||||||
|
newErrors.general = "Password must be at least 8 characters";
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(newErrors);
|
||||||
|
return isValid;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setErrors({ ...errors, general: "" });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await login(loginData.email, loginData.password);
|
||||||
|
router.push("/pages/dashboard");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Login error:", error);
|
||||||
|
let errorMessage = 'Login failed. Please try again.';
|
||||||
|
|
||||||
|
if (error.type === 'user_invalid_credentials') {
|
||||||
|
errorMessage = 'Invalid email or password';
|
||||||
|
} else if (error.type === 'general_argument_invalid') {
|
||||||
|
errorMessage = 'Invalid email format';
|
||||||
|
} else if (error.code === 401) {
|
||||||
|
errorMessage = 'Authentication failed. Please try again.';
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors({ ...errors, general: errorMessage });
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSignup = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!validateSignupForm()) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setErrors({ ...errors, general: "" });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await register(
|
||||||
|
signupData.email,
|
||||||
|
signupData.password,
|
||||||
|
`${signupData.fname} ${signupData.lname}`
|
||||||
|
);
|
||||||
|
router.push("/pages/dashboard");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Signup error:", error);
|
||||||
|
let errorMessage = 'Signup failed. Please try again.';
|
||||||
|
|
||||||
|
if (error.type === 'user_already_exists') {
|
||||||
|
errorMessage = 'Email already registered';
|
||||||
|
} else if (error.type === 'general_argument_invalid') {
|
||||||
|
errorMessage = 'Invalid email format';
|
||||||
|
} else if (error.code === 401) {
|
||||||
|
errorMessage = 'Authentication failed. Please try again.';
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors({ ...errors, general: errorMessage });
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (authLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-900">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900 dark:border-gray-100 mx-auto"></div>
|
||||||
|
<p className="mt-4 text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
Loading...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAuthenticated) {
|
||||||
|
return null; // Will redirect from useEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`min-h-auto ${darkMode ? "dark bg-gray-900" : "bg-white"}`}>
|
||||||
|
<div className="flex flex-col items-center justify-center p-6">
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-800 dark:text-white/90">
|
||||||
|
{showResetForm ? "Reset Password" : isLoginForm ? "Sign In" : "Sign Up"}
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{showResetForm
|
||||||
|
? resetSent
|
||||||
|
? "Check your email for reset instructions"
|
||||||
|
: "Enter your email to receive a reset link"
|
||||||
|
: isLoginForm
|
||||||
|
? "Enter your email and password to sign in"
|
||||||
|
: "Create your account to get started"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{errors.general && (
|
||||||
|
<div className="mb-4 p-3 text-sm text-red-600 bg-red-50 rounded-lg dark:bg-red-900/20 dark:text-red-300">
|
||||||
|
{errors.general}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showResetForm ? (
|
||||||
|
// Password Reset Form
|
||||||
|
<div className="space-y-4">
|
||||||
|
{resetSent ? (
|
||||||
|
<div className="p-4 bg-green-50 text-green-800 rounded-lg dark:bg-green-900/20 dark:text-green-300">
|
||||||
|
We've sent a password reset link to your email. Please check your inbox.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleForgotPassword} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Email Address*
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={resetEmail}
|
||||||
|
onChange={(e) => setResetEmail(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-700 dark:text-white"
|
||||||
|
placeholder="your@email.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full py-2 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? "Sending..." : "Send Reset Link"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowResetForm(false);
|
||||||
|
setResetSent(false);
|
||||||
|
setResetEmail("");
|
||||||
|
setErrors({ general: "" });
|
||||||
|
}}
|
||||||
|
className="w-full text-center text-sm text-blue-600 hover:underline dark:text-blue-400"
|
||||||
|
>
|
||||||
|
Back to {isLoginForm ? "login" : "sign up"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) :isLoginForm ? (
|
||||||
|
// Login Form
|
||||||
|
<form onSubmit={handleLogin} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Email*
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
value={loginData.email}
|
||||||
|
onChange={handleLoginChange}
|
||||||
|
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-700 dark:text-white"
|
||||||
|
placeholder="your@email.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Password*
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
name="password"
|
||||||
|
value={loginData.password}
|
||||||
|
onChange={handleLoginChange}
|
||||||
|
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-700 dark:text-white"
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
minLength="8"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 dark:text-gray-400"
|
||||||
|
>
|
||||||
|
{showPassword ? "Hide" : "Show"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="flex items-center space-x-2 text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="rememberMe"
|
||||||
|
checked={loginData.rememberMe}
|
||||||
|
onChange={handleLoginChange}
|
||||||
|
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||||
|
/>
|
||||||
|
<span>Remember me</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setShowResetForm(true);
|
||||||
|
setIsLoginForm(false);
|
||||||
|
}}
|
||||||
|
className="text-sm text-blue-600 hover:underline dark:text-blue-400"
|
||||||
|
>
|
||||||
|
Forgot password?
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full py-2 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? "Signing in..." : "Sign in"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
// Sign Up Form
|
||||||
|
<form onSubmit={handleSignup} className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
First Name*
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="fname"
|
||||||
|
value={signupData.fname}
|
||||||
|
onChange={handleSignupChange}
|
||||||
|
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-700 dark:text-white"
|
||||||
|
placeholder="John"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Last Name*
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="lname"
|
||||||
|
value={signupData.lname}
|
||||||
|
onChange={handleSignupChange}
|
||||||
|
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-700 dark:text-white"
|
||||||
|
placeholder="Doe"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Email*
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
value={signupData.email}
|
||||||
|
onChange={handleSignupChange}
|
||||||
|
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-700 dark:text-white"
|
||||||
|
placeholder="your@email.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Password* (min 8 characters)
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
name="password"
|
||||||
|
value={signupData.password}
|
||||||
|
onChange={handleSignupChange}
|
||||||
|
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-700 dark:text-white"
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
minLength="8"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 dark:text-gray-400"
|
||||||
|
>
|
||||||
|
{showPassword ? "Hide" : "Show"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="flex items-start space-x-2 text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="agreeTerms"
|
||||||
|
checked={signupData.agreeTerms}
|
||||||
|
onChange={handleSignupChange}
|
||||||
|
className="mt-1 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
I agree to the <Link href="/terms" className="text-blue-600 hover:underline dark:text-blue-400">Terms</Link> and <Link href="/privacy" className="text-blue-600 hover:underline dark:text-blue-400">Privacy Policy</Link>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{errors.agreeTerms && (
|
||||||
|
<p className="mt-1 text-sm text-red-600 dark:text-red-400">
|
||||||
|
{errors.agreeTerms}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full py-2 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? "Creating account..." : "Sign up"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!showResetForm && (
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{isLoginForm ? "Don't have an account?" : "Already have an account?"}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsLoginForm(!isLoginForm);
|
||||||
|
setErrors({ agreeTerms: "", general: "" });
|
||||||
|
}}
|
||||||
|
className="ml-1 text-blue-600 hover:underline dark:text-blue-400 font-medium"
|
||||||
|
>
|
||||||
|
{isLoginForm ? "Sign up" : "Sign in"}
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
{/* {isLoginForm && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setShowResetForm(true);
|
||||||
|
setIsLoginForm(false);
|
||||||
|
}}
|
||||||
|
className="mt-2 text-sm text-blue-600 hover:underline dark:text-blue-400"
|
||||||
|
>
|
||||||
|
Forgot password?
|
||||||
|
</button>
|
||||||
|
)} */}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dark mode toggle */}
|
||||||
|
<button
|
||||||
|
onClick={() => setDarkMode(!darkMode)}
|
||||||
|
className="fixed bottom-6 right-6 p-3 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200"
|
||||||
|
aria-label="Toggle dark mode"
|
||||||
|
>
|
||||||
|
{darkMode ? (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
202
src/app/components/BasicTableOne.js
Normal file
202
src/app/components/BasicTableOne.js
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Table, TableBody, TableCell, TableHeader, TableRow } from "../ui/Table";
|
||||||
|
import Badge from "../ui/Badge";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
// Table data
|
||||||
|
const tableData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
user: {
|
||||||
|
image: "/images/user/user-17.jpg",
|
||||||
|
name: "Lindsey Curtis",
|
||||||
|
role: "Web Designer",
|
||||||
|
},
|
||||||
|
projectName: "Agency Website",
|
||||||
|
team: {
|
||||||
|
images: [
|
||||||
|
"/images/user/user-22.jpg",
|
||||||
|
"/images/user/user-23.jpg",
|
||||||
|
"/images/user/user-24.jpg",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
budget: "3.9K",
|
||||||
|
status: "Active",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
user: {
|
||||||
|
image: "/images/user/user-18.jpg",
|
||||||
|
name: "Kaiya George",
|
||||||
|
role: "Project Manager",
|
||||||
|
},
|
||||||
|
projectName: "Technology",
|
||||||
|
team: {
|
||||||
|
images: ["/images/user/user-25.jpg", "/images/user/user-26.jpg"],
|
||||||
|
},
|
||||||
|
budget: "24.9K",
|
||||||
|
status: "Pending",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
user: {
|
||||||
|
image: "/images/user/user-17.jpg",
|
||||||
|
name: "Zain Geidt",
|
||||||
|
role: "Content Writing",
|
||||||
|
},
|
||||||
|
projectName: "Blog Writing",
|
||||||
|
team: {
|
||||||
|
images: ["/images/user/user-27.jpg"],
|
||||||
|
},
|
||||||
|
budget: "12.7K",
|
||||||
|
status: "Active",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
user: {
|
||||||
|
image: "/images/user/user-20.jpg",
|
||||||
|
name: "Abram Schleifer",
|
||||||
|
role: "Digital Marketer",
|
||||||
|
},
|
||||||
|
projectName: "Social Media",
|
||||||
|
team: {
|
||||||
|
images: [
|
||||||
|
"/images/user/user-28.jpg",
|
||||||
|
"/images/user/user-29.jpg",
|
||||||
|
"/images/user/user-30.jpg",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
budget: "2.8K",
|
||||||
|
status: "Cancel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
user: {
|
||||||
|
image: "/images/user/user-21.jpg",
|
||||||
|
name: "Carla George",
|
||||||
|
role: "Front-end Developer",
|
||||||
|
},
|
||||||
|
projectName: "Website",
|
||||||
|
team: {
|
||||||
|
images: [
|
||||||
|
"/images/user/user-31.jpg",
|
||||||
|
"/images/user/user-32.jpg",
|
||||||
|
"/images/user/user-33.jpg",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
budget: "4.5K",
|
||||||
|
status: "Active",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function BasicTableOne() {
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-white/[0.05] dark:bg-white/[0.03]">
|
||||||
|
<div className="max-w-full overflow-x-auto">
|
||||||
|
<div className="min-w-[1102px]">
|
||||||
|
<Table>
|
||||||
|
<TableHeader className="border-b border-gray-100 dark:border-white/[0.05]">
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
isHeader
|
||||||
|
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
|
||||||
|
>
|
||||||
|
User
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
isHeader
|
||||||
|
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
|
||||||
|
>
|
||||||
|
Project Name
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
isHeader
|
||||||
|
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
|
||||||
|
>
|
||||||
|
Team
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
isHeader
|
||||||
|
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
|
||||||
|
>
|
||||||
|
Status
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
isHeader
|
||||||
|
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
|
||||||
|
>
|
||||||
|
Budget
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
|
||||||
|
<TableBody className="divide-y divide-gray-100 dark:divide-white/[0.05]">
|
||||||
|
{tableData.map((order) => (
|
||||||
|
<TableRow key={order.id}>
|
||||||
|
<TableCell className="px-5 py-4 sm:px-6 text-start">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 overflow-hidden rounded-full">
|
||||||
|
<Image
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
src={order.user.image}
|
||||||
|
alt={order.user.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="block font-medium text-gray-800 text-theme-sm dark:text-white/90">
|
||||||
|
{order.user.name}
|
||||||
|
</span>
|
||||||
|
<span className="block text-gray-500 text-theme-xs dark:text-gray-400">
|
||||||
|
{order.user.role}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-4 py-3 text-gray-500 text-start text-theme-sm dark:text-gray-400">
|
||||||
|
{order.projectName}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-4 py-3 text-gray-500 text-start text-theme-sm dark:text-gray-400">
|
||||||
|
<div className="flex -space-x-2">
|
||||||
|
{order.team.images.map((teamImage, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="w-6 h-6 overflow-hidden border-2 border-white rounded-full dark:border-gray-900"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
src={teamImage}
|
||||||
|
alt={`Team member ${index + 1}`}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-4 py-3 text-gray-500 text-start text-theme-sm dark:text-gray-400">
|
||||||
|
<Badge
|
||||||
|
size="sm"
|
||||||
|
color={
|
||||||
|
order.status === "Active"
|
||||||
|
? "success"
|
||||||
|
: order.status === "Pending"
|
||||||
|
? "warning"
|
||||||
|
: "error"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{order.status}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-4 py-3 text-gray-500 text-theme-sm dark:text-gray-400">
|
||||||
|
{order.budget}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
350
src/app/components/Navbar.js
Normal file
350
src/app/components/Navbar.js
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
"use client";
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { account } from "../lib/appwrite";
|
||||||
|
import { useTheme } from "../context/ThemeContext";
|
||||||
|
import { useSidebar } from "../context/SidebarContext"; // Add this import
|
||||||
|
|
||||||
|
const Navbar = () => {
|
||||||
|
const { darkMode, toggleDarkMode } = useTheme();
|
||||||
|
const dropdownRef = useRef(null);
|
||||||
|
const [isMenuOpen, setMenuOpen] = useState(false);
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
|
const [notifying, setNotifying] = useState(true); // assuming initial notifying state is true
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const { isOpen, toggleSidebar } = useSidebar(); // Use the sidebar context
|
||||||
|
|
||||||
|
// ------fetch user-----------------
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUser = async () => {
|
||||||
|
try {
|
||||||
|
const userData = await account.get();
|
||||||
|
setUser(userData);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("User not logged in:", error.message);
|
||||||
|
setUser(null);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUser();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSignOut = async () => {
|
||||||
|
try {
|
||||||
|
await account.deleteSession('current');
|
||||||
|
// Redirect to login page or home page after sign out
|
||||||
|
window.location.href = '/'; // or your preferred redirect
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error signing out:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// -----------------------------------------------------
|
||||||
|
|
||||||
|
const handleClick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDropdownOpen(!dropdownOpen);
|
||||||
|
setNotifying(false);
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event) => {
|
||||||
|
if ((event.metaKey || event.ctrlKey) && event.key === "k") {
|
||||||
|
event.preventDefault();
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, []);
|
||||||
|
// ----------------------------------------------
|
||||||
|
const UserDropdown = () => {
|
||||||
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
|
const dropdownRef = useRef(null);
|
||||||
|
|
||||||
|
// Close dropdown when clicking outside
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event) => {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||||
|
setDropdownOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className={`${darkMode ? 'bg-gray-900 border-gray-700 shadow-gray-800/30' : 'bg-white border-gray-200 shadow-md'} sticky top-0 z-50 w-full`}>
|
||||||
|
<div className="flex items-center justify-between px-2 py-2">
|
||||||
|
{/* Left side - Menu Button */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
onClick={toggleSidebar}
|
||||||
|
className={`z-99999 flex h-10 w-10 items-center justify-center rounded-lg border-gray-200 text-gray-500 dark:border-gray-800 dark:text-gray-400 lg:h-11 lg:w-11 lg:border ${isMenuOpen
|
||||||
|
? 'lg:bg-transparent dark:lg:bg-transparent bg-gray-100 dark:bg-gray-800'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* Hamburger icon (shown when sidebar is closed) */}
|
||||||
|
<svg
|
||||||
|
className={`fill-current ${isMenuOpen ? 'hidden' : 'block lg:hidden'}`}
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M3.25 6C3.25 5.58579 3.58579 5.25 4 5.25L20 5.25C20.4142 5.25 20.75 5.58579 20.75 6C20.75 6.41421 20.4142 6.75 20 6.75L4 6.75C3.58579 6.75 3.25 6.41422 3.25 6ZM3.25 18C3.25 17.5858 3.58579 17.25 4 17.25L20 17.25C20.4142 17.25 20.75 17.5858 20.75 18C20.75 18.4142 20.4142 18.75 20 18.75L4 18.75C3.58579 18.75 3.25 18.4142 3.25 18ZM4 11.25C3.58579 11.25 3.25 11.5858 3.25 12C3.25 12.4142 3.58579 12.75 4 12.75L12 12.75C12.4142 12.75 12.75 12.4142 12.75 12C12.75 11.5858 12.4142 11.25 12 11.25L4 11.25Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Close icon (shown when sidebar is open) */}
|
||||||
|
<svg
|
||||||
|
className={`fill-current ${isMenuOpen ? 'block lg:hidden' : 'hidden'}`}
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M6.21967 7.28131C5.92678 6.98841 5.92678 6.51354 6.21967 6.22065C6.51256 5.92775 6.98744 5.92775 7.28033 6.22065L11.999 10.9393L16.7176 6.22078C17.0105 5.92789 17.4854 5.92788 17.7782 6.22078C18.0711 6.51367 18.0711 6.98855 17.7782 7.28144L13.0597 12L17.7782 16.7186C18.0711 17.0115 18.0711 17.4863 17.7782 17.7792C17.4854 18.0721 17.0105 18.0721 16.7176 17.7792L11.999 13.0607L7.28033 17.7794C6.98744 18.0722 6.51256 18.0722 6.21967 17.7794C5.92678 17.4865 5.92678 17.0116 6.21967 16.7187L10.9384 12L6.21967 7.28131Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Desktop menu icon (always shown on desktop when sidebar is closed) */}
|
||||||
|
<svg
|
||||||
|
className={`hidden fill-current lg:block ${isMenuOpen ? 'hidden' : 'block'}`}
|
||||||
|
width="16"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 16 12"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M0.583252 1C0.583252 0.585788 0.919038 0.25 1.33325 0.25H14.6666C15.0808 0.25 15.4166 0.585786 15.4166 1C15.4166 1.41421 15.0808 1.75 14.6666 1.75L1.33325 1.75C0.919038 1.75 0.583252 1.41422 0.583252 1ZM0.583252 11C0.583252 10.5858 0.919038 10.25 1.33325 10.25L14.6666 10.25C15.0808 10.25 15.4166 10.5858 15.4166 11C15.4166 11.4142 15.0808 11.75 14.6666 11.75L1.33325 11.75C0.919038 11.75 0.583252 11.4142 0.583252 11ZM1.33325 5.25C0.919038 5.25 0.583252 5.58579 0.583252 6C0.583252 6.41421 0.919038 6.75 1.33325 6.75L7.99992 6.75C8.41413 6.75 8.74992 6.41421 8.74992 6C8.74992 5.58579 8.41413 5.25 7.99992 5.25L1.33325 5.25Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* Center - Search */}
|
||||||
|
<div className="hidden lg:block mx-4 flex-1 max-w-xl">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
|
<svg className="fill-gray-500 dark:fill-gray-400" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fillRule="evenodd" clipRule="evenodd" d="M3.04175 9.37363C3.04175 5.87693 5.87711 3.04199 9.37508 3.04199C12.8731 3.04199 15.7084 5.87693 15.7084 9.37363C15.7084 12.8703 12.8731 15.7053 9.37508 15.7053C5.87711 15.7053 3.04175 12.8703 3.04175 9.37363ZM9.37508 1.54199C5.04902 1.54199 1.54175 5.04817 1.54175 9.37363C1.54175 13.6991 5.04902 17.2053 9.37508 17.2053C11.2674 17.2053 13.003 16.5344 14.357 15.4176L17.177 18.238C17.4699 18.5309 17.9448 18.5309 18.2377 18.238C18.5306 17.9451 18.5306 17.4703 18.2377 17.1774L15.418 14.3573C16.5365 13.0033 17.2084 11.2669 17.2084 9.37363C17.2084 5.04817 13.7011 1.54199 9.37508 1.54199Z" fill=""></path>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
placeholder="Search..."
|
||||||
|
className="w-full p-2 pl-10 bg-gray-50 border border-gray-300 rounded-lg dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
⌘K
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right side - Icons and User */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
onClick={toggleDarkMode}
|
||||||
|
className={`
|
||||||
|
p-2 rounded-full
|
||||||
|
${darkMode ? 'bg-gray-700 hover:bg-gray-600' : 'bg-gray-100 hover:bg-gray-200'}
|
||||||
|
flex items-center justify-center
|
||||||
|
w-8 h-8 sm:w-10 sm:h-10
|
||||||
|
transition-all duration-200
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50
|
||||||
|
`}
|
||||||
|
aria-label={darkMode ? "Switch to light mode" : "Switch to dark mode"}
|
||||||
|
>
|
||||||
|
{darkMode ? (
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
className="text-yellow-400"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M9.99998 1.5415C10.4142 1.5415 10.75 1.87729 10.75 2.2915V3.5415C10.75 3.95572 10.4142 4.2915 9.99998 4.2915C9.58577 4.2915 9.24998 3.95572 9.24998 3.5415V2.2915C9.24998 1.87729 9.58577 1.5415 9.99998 1.5415ZM10.0009 6.79327C8.22978 6.79327 6.79402 8.22904 6.79402 10.0001C6.79402 11.7712 8.22978 13.207 10.0009 13.207C11.772 13.207 13.2078 11.7712 13.2078 10.0001C13.2078 8.22904 11.772 6.79327 10.0009 6.79327ZM5.29402 10.0001C5.29402 7.40061 7.40135 5.29327 10.0009 5.29327C12.6004 5.29327 14.7078 7.40061 14.7078 10.0001C14.7078 12.5997 12.6004 14.707 10.0009 14.707C7.40135 14.707 5.29402 12.5997 5.29402 10.0001ZM15.9813 5.08035C16.2742 4.78746 16.2742 4.31258 15.9813 4.01969C15.6884 3.7268 15.2135 3.7268 14.9207 4.01969L14.0368 4.90357C13.7439 5.19647 13.7439 5.67134 14.0368 5.96423C14.3297 6.25713 14.8045 6.25713 15.0974 5.96423L15.9813 5.08035ZM18.4577 10.0001C18.4577 10.4143 18.1219 10.7501 17.7077 10.7501H16.4577C16.0435 10.7501 15.7077 10.4143 15.7077 10.0001C15.7077 9.58592 16.0435 9.25013 16.4577 9.25013H17.7077C18.1219 9.25013 18.4577 9.58592 18.4577 10.0001ZM14.9207 15.9806C15.2135 16.2735 15.6884 16.2735 15.9813 15.9806C16.2742 15.6877 16.2742 15.2128 15.9813 14.9199L15.0974 14.036C14.8045 13.7431 14.3297 13.7431 14.0368 14.036C13.7439 14.3289 13.7439 14.8038 14.0368 15.0967L14.9207 15.9806ZM9.99998 15.7088C10.4142 15.7088 10.75 16.0445 10.75 16.4588V17.7088C10.75 18.123 10.4142 18.4588 9.99998 18.4588C9.58577 18.4588 9.24998 18.123 9.24998 17.7088V16.4588C9.24998 16.0445 9.58577 15.7088 9.99998 15.7088ZM5.96356 15.0972C6.25646 14.8043 6.25646 14.3295 5.96356 14.0366C5.67067 13.7437 5.1958 13.7437 4.9029 14.0366L4.01902 14.9204C3.72613 15.2133 3.72613 15.6882 4.01902 15.9811C4.31191 16.274 4.78679 16.274 5.07968 15.9811L5.96356 15.0972ZM4.29224 10.0001C4.29224 10.4143 3.95645 10.7501 3.54224 10.7501H2.29224C1.87802 10.7501 1.54224 10.4143 1.54224 10.0001C1.54224 9.58592 1.87802 9.25013 2.29224 9.25013H3.54224C3.95645 9.25013 4.29224 9.58592 4.29224 10.0001ZM4.9029 5.9637C5.1958 6.25659 5.67067 6.25659 5.96356 5.9637C6.25646 5.6708 6.25646 5.19593 5.96356 4.90303L5.07968 4.01915C4.78679 3.72626 4.31191 3.72626 4.01902 4.01915C3.72613 4.31204 3.72613 4.78692 4.01902 5.07981L4.9029 5.9637Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
className="text-gray-600 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M17.4547 11.97L18.1799 12.1611C18.265 11.8383 18.1265 11.4982 17.8401 11.3266C17.5538 11.1551 17.1885 11.1934 16.944 11.4207L17.4547 11.97ZM8.0306 2.5459L8.57989 3.05657C8.80718 2.81209 8.84554 2.44682 8.67398 2.16046C8.50243 1.8741 8.16227 1.73559 7.83948 1.82066L8.0306 2.5459ZM12.9154 13.0035C9.64678 13.0035 6.99707 10.3538 6.99707 7.08524H5.49707C5.49707 11.1823 8.81835 14.5035 12.9154 14.5035V13.0035ZM16.944 11.4207C15.8869 12.4035 14.4721 13.0035 12.9154 13.0035V14.5035C14.8657 14.5035 16.6418 13.7499 17.9654 12.5193L16.944 11.4207ZM16.7295 11.7789C15.9437 14.7607 13.2277 16.9586 10.0003 16.9586V18.4586C13.9257 18.4586 17.2249 15.7853 18.1799 12.1611L16.7295 11.7789ZM10.0003 16.9586C6.15734 16.9586 3.04199 13.8433 3.04199 10.0003H1.54199C1.54199 14.6717 5.32892 18.4586 10.0003 18.4586V16.9586ZM3.04199 10.0003C3.04199 6.77289 5.23988 4.05695 8.22173 3.27114L7.83948 1.82066C4.21532 2.77574 1.54199 6.07486 1.54199 10.0003H3.04199ZM6.99707 7.08524C6.99707 5.52854 7.5971 4.11366 8.57989 3.05657L7.48132 2.03522C6.25073 3.35885 5.49707 5.13487 5.49707 7.08524H6.99707Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="hover:text-dark-900 relative flex h-11 w-11 items-center justify-center rounded-full border border-gray-200 bg-white text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
|
||||||
|
onClick={handleClick}
|
||||||
|
type="button"
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded={dropdownOpen}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`absolute right-0 top-0.5 z-10 h-2 w-2 rounded-full bg-orange-400 flex ${!notifying ? 'hidden' : 'flex'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="absolute -z-1 inline-flex h-full w-full animate-ping rounded-full bg-orange-400 opacity-75"></span>
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
className="fill-current"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M10.75 2.29248C10.75 1.87827 10.4143 1.54248 10 1.54248C9.58583 1.54248 9.25004 1.87827 9.25004 2.29248V2.83613C6.08266 3.20733 3.62504 5.9004 3.62504 9.16748V14.4591H3.33337C2.91916 14.4591 2.58337 14.7949 2.58337 15.2091C2.58337 15.6234 2.91916 15.9591 3.33337 15.9591H4.37504H15.625H16.6667C17.0809 15.9591 17.4167 15.6234 17.4167 15.2091C17.4167 14.7949 17.0809 14.4591 16.6667 14.4591H16.375V9.16748C16.375 5.9004 13.9174 3.20733 10.75 2.83613V2.29248ZM14.875 14.4591V9.16748C14.875 6.47509 12.6924 4.29248 10 4.29248C7.30765 4.29248 5.12504 6.47509 5.12504 9.16748V14.4591H14.875ZM8.00004 17.7085C8.00004 18.1228 8.33583 18.4585 8.75004 18.4585H11.25C11.6643 18.4585 12 18.1228 12 17.7085C12 17.2943 11.6643 16.9585 11.25 16.9585H8.75004C8.33583 16.9585 8.00004 17.2943 8.00004 17.7085Z"
|
||||||
|
fill=""
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* --------------dropdown---------------- */}
|
||||||
|
<div className="relative" ref={dropdownRef}>
|
||||||
|
<button
|
||||||
|
className="flex items-center text-gray-700 dark:text-gray-400"
|
||||||
|
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||||
|
>
|
||||||
|
<span className="mr-3 h-10 w-10 overflow-hidden rounded-full">
|
||||||
|
{user?.name ? (
|
||||||
|
<div className="h-full w-full flex items-center justify-center bg-pink-200 text-red-800 font-medium">
|
||||||
|
{user.name.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src="/images/user/owner.jpg"
|
||||||
|
alt="User"
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="mr-1 block text-sm text-blue-800 font-medium">
|
||||||
|
{loading ? "Loading..." : user?.name || "Guest"}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
className={`stroke-gray-500 dark:stroke-gray-400 transition-transform ${dropdownOpen ? "rotate-180" : ""}`}
|
||||||
|
width="18"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 18 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M4.3125 8.65625L9 13.3437L13.6875 8.65625"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* ----------Dropdown ------------------*/}
|
||||||
|
{dropdownOpen && (
|
||||||
|
<div className="absolute right-0 mt-4 flex w-[260px] flex-col rounded-2xl border border-gray-200 bg-white p-3 shadow-lg dark:border-gray-800 dark:bg-gray-800">
|
||||||
|
<div>
|
||||||
|
<span className="block text-sm font-medium text-gray-700 dark:text-gray-400">
|
||||||
|
{user?.name || "Guest User"}
|
||||||
|
</span>
|
||||||
|
<span className="mt-1 block text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{user?.email || "No email available"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="flex flex-col gap-1 border-b border-gray-200 pb-3 pt-4 dark:border-gray-700">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="/profile"
|
||||||
|
className="group flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||||
|
>
|
||||||
|
<svg className="fill-gray-500 group-hover:fill-gray-700 dark:fill-gray-400 dark:group-hover:fill-gray-300" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fillRule="evenodd" clipRule="evenodd" d="M12 3.5C7.30558 3.5 3.5 7.30558 3.5 12C3.5 14.1526 4.3002 16.1184 5.61936 17.616C6.17279 15.3096 8.24852 13.5955 10.7246 13.5955H13.2746C15.7509 13.5955 17.8268 15.31 18.38 17.6167C19.6996 16.119 20.5 14.153 20.5 12C20.5 7.30558 16.6944 3.5 12 3.5ZM17.0246 18.8566V18.8455C17.0246 16.7744 15.3457 15.0955 13.2746 15.0955H10.7246C8.65354 15.0955 6.97461 16.7744 6.97461 18.8455V18.856C8.38223 19.8895 10.1198 20.5 12 20.5C13.8798 20.5 15.6171 19.8898 17.0246 18.8566ZM2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM11.9991 7.25C10.8847 7.25 9.98126 8.15342 9.98126 9.26784C9.98126 10.3823 10.8847 11.2857 11.9991 11.2857C13.1135 11.2857 14.0169 10.3823 14.0169 9.26784C14.0169 8.15342 13.1135 7.25 11.9991 7.25ZM8.48126 9.26784C8.48126 7.32499 10.0563 5.75 11.9991 5.75C13.9419 5.75 15.5169 7.32499 15.5169 9.26784C15.5169 11.2107 13.9419 12.7857 11.9991 12.7857C10.0563 12.7857 8.48126 11.2107 8.48126 9.26784Z" />
|
||||||
|
</svg>
|
||||||
|
Edit profile
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="/account-settings"
|
||||||
|
className="group flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||||
|
>
|
||||||
|
<svg className="fill-gray-500 group-hover:fill-gray-700 dark:fill-gray-400 dark:group-hover:fill-gray-300" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fillRule="evenodd" clipRule="evenodd" d="M10.4858 3.5L13.5182 3.5C13.9233 3.5 14.2518 3.82851 14.2518 4.23377C14.2518 5.9529 16.1129 7.02795 17.602 6.1682C17.9528 5.96567 18.4014 6.08586 18.6039 6.43667L20.1203 9.0631C20.3229 9.41407 20.2027 9.86286 19.8517 10.0655C18.3625 10.9253 18.3625 13.0747 19.8517 13.9345C20.2026 14.1372 20.3229 14.5859 20.1203 14.9369L18.6039 17.5634C18.4013 17.9142 17.9528 18.0344 17.602 17.8318C16.1129 16.9721 14.2518 18.0471 14.2518 19.7663C14.2518 20.1715 13.9233 20.5 13.5182 20.5H10.4858C10.0804 20.5 9.75182 20.1714 9.75182 19.766C9.75182 18.0461 7.88983 16.9717 6.40067 17.8314C6.04945 18.0342 5.60037 17.9139 5.39767 17.5628L3.88167 14.937C3.67903 14.586 3.79928 14.1372 4.15026 13.9346C5.63949 13.0748 5.63946 10.9253 4.15025 10.0655C3.79926 9.86282 3.67901 9.41401 3.88165 9.06303L5.39764 6.43725C5.60034 6.08617 6.04943 5.96581 6.40065 6.16858C7.88982 7.02836 9.75182 5.9539 9.75182 4.23399C9.75182 3.82862 10.0804 3.5 10.4858 3.5ZM13.5182 2L10.4858 2C9.25201 2 8.25182 3.00019 8.25182 4.23399C8.25182 4.79884 7.64013 5.15215 7.15065 4.86955C6.08213 4.25263 4.71559 4.61859 4.0986 5.68725L2.58261 8.31303C1.96575 9.38146 2.33183 10.7477 3.40025 11.3645C3.88948 11.647 3.88947 12.3531 3.40026 12.6355C2.33184 13.2524 1.96578 14.6186 2.58263 15.687L4.09863 18.3128C4.71562 19.3814 6.08215 19.7474 7.15067 19.1305C7.64015 18.8479 8.25182 19.2012 8.25182 19.766C8.25182 20.9998 9.25201 22 10.4858 22H13.5182C14.7519 22 15.7518 20.9998 15.7518 19.7663C15.7518 19.2015 16.3632 18.8487 16.852 19.1309C17.9202 19.7476 19.2862 19.3816 19.9029 18.3134L21.4193 15.6869C22.0361 14.6185 21.6701 13.2523 20.6017 12.6355C20.1125 12.3531 20.1125 11.647 20.6017 11.3645C21.6701 10.7477 22.0362 9.38152 21.4193 8.3131L19.903 5.68667C19.2862 4.61842 17.9202 4.25241 16.852 4.86917C16.3632 5.15138 15.7518 4.79856 15.7518 4.23377C15.7518 3.00024 14.7519 2 13.5182 2ZM9.6659 11.9999C9.6659 10.7103 10.7113 9.66493 12.0009 9.66493C13.2905 9.66493 14.3359 10.7103 14.3359 11.9999C14.3359 13.2895 13.2905 14.3349 12.0009 14.3349C10.7113 14.3349 9.6659 13.2895 9.6659 11.9999ZM12.0009 8.16493C9.88289 8.16493 8.1659 9.88191 8.1659 11.9999C8.1659 14.1179 9.88289 15.8349 12.0009 15.8349C14.1189 15.8349 15.8359 14.1179 15.8359 11.9999C15.8359 9.88191 14.1189 8.16493 12.0009 8.16493Z" />
|
||||||
|
</svg>
|
||||||
|
Account settings
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="/support"
|
||||||
|
className="group flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||||
|
>
|
||||||
|
<svg className="fill-gray-500 group-hover:fill-gray-700 dark:fill-gray-400 dark:group-hover:fill-gray-300" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fillRule="evenodd" clipRule="evenodd" d="M3.5 12C3.5 7.30558 7.30558 3.5 12 3.5C16.6944 3.5 20.5 7.30558 20.5 12C20.5 16.6944 16.6944 20.5 12 20.5C7.30558 20.5 3.5 16.6944 3.5 12ZM12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM11.0991 7.52507C11.0991 8.02213 11.5021 8.42507 11.9991 8.42507H12.0001C12.4972 8.42507 12.9001 8.02213 12.9001 7.52507C12.9001 7.02802 12.4972 6.62507 12.0001 6.62507H11.9991C11.5021 6.62507 11.0991 7.02802 11.0991 7.52507ZM12.0001 17.3714C11.5859 17.3714 11.2501 17.0356 11.2501 16.6214V10.9449C11.2501 10.5307 11.5859 10.1949 12.0001 10.1949C12.4143 10.1949 12.7501 10.5307 12.7501 10.9449V16.6214C12.7501 17.0356 12.4143 17.3714 12.0001 17.3714Z" />
|
||||||
|
</svg>
|
||||||
|
Support
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<button
|
||||||
|
onClick={handleSignOut}
|
||||||
|
className="group mt-3 flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||||
|
>
|
||||||
|
<svg className="fill-gray-500 group-hover:fill-gray-700 dark:group-hover:fill-gray-300" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fillRule="evenodd" clipRule="evenodd" d="M15.1007 19.247C14.6865 19.247 14.3507 18.9112 14.3507 18.497L14.3507 14.245H12.8507V18.497C12.8507 19.7396 13.8581 20.747 15.1007 20.747H18.5007C19.7434 20.747 20.7507 19.7396 20.7507 18.497L20.7507 5.49609C20.7507 4.25345 19.7433 3.24609 18.5007 3.24609H15.1007C13.8581 3.24609 12.8507 4.25345 12.8507 5.49609V9.74501L14.3507 9.74501V5.49609C14.3507 5.08188 14.6865 4.74609 15.1007 4.74609L18.5007 4.74609C18.9149 4.74609 19.2507 5.08188 19.2507 5.49609L19.2507 18.497C19.2507 18.9112 18.9149 19.247 18.5007 19.247H15.1007ZM3.25073 11.9984C3.25073 12.2144 3.34204 12.4091 3.48817 12.546L8.09483 17.1556C8.38763 17.4485 8.86251 17.4487 9.15549 17.1559C9.44848 16.8631 9.44863 16.3882 9.15583 16.0952L5.81116 12.7484L16.0007 12.7484C16.4149 12.7484 16.7507 12.4127 16.7507 11.9984C16.7507 11.5842 16.4149 11.2484 16.0007 11.2484L5.81528 11.2484L9.15585 7.90554C9.44864 7.61255 9.44847 7.13767 9.15547 6.84488C8.86248 6.55209 8.3876 6.55226 8.09481 6.84525L3.52309 11.4202C3.35673 11.5577 3.25073 11.7657 3.25073 11.9984Z" />
|
||||||
|
</svg>
|
||||||
|
Sign out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* -------------------------------- */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default Navbar;
|
||||||
26
src/app/components/ProtectedRoute.js
Normal file
26
src/app/components/ProtectedRoute.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// components/ProtectedRoute.js
|
||||||
|
"use client";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useAuth } from "../context/AuthContext";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export default function ProtectedRoute({ children }) {
|
||||||
|
const { user, loading } = useAuth();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loading && !user) {
|
||||||
|
router.push("/auth");
|
||||||
|
}
|
||||||
|
}, [user, loading, router]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user ? children : null;
|
||||||
|
}
|
||||||
121
src/app/components/Sidebar.js
Normal file
121
src/app/components/Sidebar.js
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useTheme } from "../context/ThemeContext";
|
||||||
|
import { useSidebar } from "../context/SidebarContext";
|
||||||
|
|
||||||
|
const Sidebar = () => {
|
||||||
|
const { darkMode } = useTheme();
|
||||||
|
const { isCollapsed } = useSidebar();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className={`h-full border-r flex flex-col transition-all duration-200 ease-in-out
|
||||||
|
${isCollapsed ? 'w-20' : 'w-[290px]'}
|
||||||
|
${darkMode ? 'bg-gray-900 border-gray-800' : 'bg-white border-gray-200'}`}>
|
||||||
|
|
||||||
|
{/* Logo - Only shown when expanded */}
|
||||||
|
{!isCollapsed && (
|
||||||
|
<div className="py-8 flex justify-start px-4">
|
||||||
|
<Link href="/">
|
||||||
|
<Image
|
||||||
|
src={darkMode ? "/images/logo/logo-dark.svg" : "/images/logo/logo.svg"}
|
||||||
|
alt="Logo"
|
||||||
|
width={150}
|
||||||
|
height={40}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<div className="flex flex-col overflow-y-auto no-scrollbar">
|
||||||
|
<nav className="mb-6 px-2">
|
||||||
|
{/* Menu heading - Only shown when expanded */}
|
||||||
|
{!isCollapsed && (
|
||||||
|
<h2 className="mb-4 text-xs uppercase leading-[20px] text-gray-400 dark:text-gray-500 px-2">
|
||||||
|
Menu
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{/* Dashboard */}
|
||||||
|
<Link
|
||||||
|
href="/pages/dashboard"
|
||||||
|
className={`flex items-center w-full p-2 rounded-lg group
|
||||||
|
${darkMode
|
||||||
|
? 'text-gray-300 hover:text-white hover:bg-gray-800'
|
||||||
|
: 'text-gray-600 hover:text-brand-500 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/icons/grid.svg"
|
||||||
|
alt="Dashboard"
|
||||||
|
className={`w-5 h-5 ${isCollapsed ? 'mx-auto' : 'mr-3'} ${darkMode ? 'filter invert' : ''}`}
|
||||||
|
/>
|
||||||
|
{!isCollapsed && <span className="flex-1 text-left">Dashboard</span>}
|
||||||
|
{isCollapsed && (
|
||||||
|
<span className={`absolute left-full ml-2 px-2 py-1 text-xs rounded-md shadow-lg
|
||||||
|
${darkMode ? 'bg-gray-800 text-white' : 'bg-white text-gray-900'}
|
||||||
|
opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap`}>
|
||||||
|
Dashboard
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* table01 page */}
|
||||||
|
<Link
|
||||||
|
href="/pages/table01"
|
||||||
|
className={`flex items-center w-full p-2 rounded-lg group
|
||||||
|
${darkMode
|
||||||
|
? 'text-gray-300 hover:text-white hover:bg-gray-800'
|
||||||
|
: 'text-gray-600 hover:text-brand-500 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/file.svg"
|
||||||
|
alt="table01"
|
||||||
|
className={`w-5 h-5 ${isCollapsed ? 'mx-auto' : 'mr-3'} ${darkMode ? 'filter invert' : ''}`}
|
||||||
|
/>
|
||||||
|
{!isCollapsed && <span className="flex-1 text-left">table01</span>}
|
||||||
|
{isCollapsed && (
|
||||||
|
<span className={`absolute left-full ml-2 px-2 py-1 text-xs rounded-md shadow-lg
|
||||||
|
${darkMode ? 'bg-gray-800 text-white' : 'bg-white text-gray-900'}
|
||||||
|
opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap`}>
|
||||||
|
table01
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
|
||||||
|
{/* TemporaryTable */}
|
||||||
|
<Link
|
||||||
|
href="/pages/TemporaryTable"
|
||||||
|
className={`flex items-center w-full p-2 rounded-lg group
|
||||||
|
${darkMode
|
||||||
|
? 'text-gray-300 hover:text-white hover:bg-gray-800'
|
||||||
|
: 'text-gray-600 hover:text-brand-500 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/file.svg"
|
||||||
|
alt="TemporaryTable"
|
||||||
|
className={`w-5 h-5 ${isCollapsed ? 'mx-auto' : 'mr-3'} ${darkMode ? 'filter invert' : ''}`}
|
||||||
|
/>
|
||||||
|
{!isCollapsed && <span className="flex-1 text-left">TemporaryTable</span>}
|
||||||
|
{isCollapsed && (
|
||||||
|
<span className={`absolute left-full ml-2 px-2 py-1 text-xs rounded-md shadow-lg
|
||||||
|
${darkMode ? 'bg-gray-800 text-white' : 'bg-white text-gray-900'}
|
||||||
|
opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap`}>
|
||||||
|
TemporaryTable
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sidebar;
|
||||||
116
src/app/context/AuthContext.js
Normal file
116
src/app/context/AuthContext.js
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
// context/AuthContext.js
|
||||||
|
"use client";
|
||||||
|
import { createContext, useContext, useState, useEffect } from "react";
|
||||||
|
import { account,ID } from "../lib/appwrite";
|
||||||
|
|
||||||
|
const AuthContext = createContext();
|
||||||
|
|
||||||
|
export const AuthProvider = ({ children }) => {
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkSession();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const checkSession = async () => {
|
||||||
|
try {
|
||||||
|
const currentUser = await account.get();
|
||||||
|
setUser(currentUser);
|
||||||
|
} catch (error) {
|
||||||
|
setUser(null);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const login = async (email, password) => {
|
||||||
|
try {
|
||||||
|
await account.createEmailPasswordSession(email, password);
|
||||||
|
const currentUser = await account.get();
|
||||||
|
setUser(currentUser);
|
||||||
|
return currentUser;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const register = async (email, password, name) => {
|
||||||
|
try {
|
||||||
|
await account.create(ID.unique(), email, password, name);
|
||||||
|
await account.createEmailPasswordSession(email, password);
|
||||||
|
const currentUser = await account.get();
|
||||||
|
setUser(currentUser);
|
||||||
|
return currentUser;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
try {
|
||||||
|
await account.deleteSession("current");
|
||||||
|
setUser(null);
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// --------password reset------------------------
|
||||||
|
const sendPasswordResetEmail = async (email) => {
|
||||||
|
try {
|
||||||
|
await account.createRecovery(
|
||||||
|
email,
|
||||||
|
`${window.location.origin}/reset-password` // Your reset password URL
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Password reset error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetPassword = async (userId, secret, newPassword, confirmPassword) => {
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
throw new Error("Passwords don't match");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await account.updateRecovery(
|
||||||
|
userId,
|
||||||
|
secret,
|
||||||
|
newPassword,
|
||||||
|
confirmPassword
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Password update error:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider
|
||||||
|
value={{
|
||||||
|
user,
|
||||||
|
loading,
|
||||||
|
isAuthenticated: !!user,
|
||||||
|
login,
|
||||||
|
register,
|
||||||
|
logout,
|
||||||
|
sendPasswordResetEmail,
|
||||||
|
resetPassword,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAuth = () => {
|
||||||
|
const context = useContext(AuthContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useAuth must be used within an AuthProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
32
src/app/context/SidebarContext.js
Normal file
32
src/app/context/SidebarContext.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// src/app/context/SidebarContext.js
|
||||||
|
'use client';
|
||||||
|
import { createContext, useContext, useState } from "react";
|
||||||
|
|
||||||
|
const SidebarContext = createContext();
|
||||||
|
|
||||||
|
export function SidebarProvider({ children }) {
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(false); // Changed from isOpen to isCollapsed
|
||||||
|
|
||||||
|
const toggleSidebar = () => setIsCollapsed(!isCollapsed);
|
||||||
|
const collapseSidebar = () => setIsCollapsed(true);
|
||||||
|
const expandSidebar = () => setIsCollapsed(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarContext.Provider value={{
|
||||||
|
isCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
collapseSidebar,
|
||||||
|
expandSidebar
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</SidebarContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSidebar() {
|
||||||
|
const context = useContext(SidebarContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useSidebar must be used within a SidebarProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
40
src/app/context/ThemeContext.js
Normal file
40
src/app/context/ThemeContext.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// context/ThemeContext.js
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { createContext, useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const ThemeContext = createContext();
|
||||||
|
|
||||||
|
export function ThemeProvider({ children }) {
|
||||||
|
const [darkMode, setDarkMode] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check for saved theme in localStorage
|
||||||
|
const savedTheme = localStorage.getItem("theme");
|
||||||
|
const initialDarkMode = savedTheme === "dark" ||
|
||||||
|
(!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
|
setDarkMode(initialDarkMode);
|
||||||
|
document.documentElement.classList.toggle("dark", initialDarkMode);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleDarkMode = () => {
|
||||||
|
const newDarkMode = !darkMode;
|
||||||
|
setDarkMode(newDarkMode);
|
||||||
|
localStorage.setItem("theme", newDarkMode ? "dark" : "light");
|
||||||
|
document.documentElement.classList.toggle("dark", newDarkMode);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ darkMode, toggleDarkMode }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
const context = useContext(ThemeContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useTheme must be used within a ThemeProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
BIN
src/app/favicon.ico
Normal file
BIN
src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
2
src/app/globals.css
Normal file
2
src/app/globals.css
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
58
src/app/layout.js
Normal file
58
src/app/layout.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import "./globals.css";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import Navbar from "./components/Navbar";
|
||||||
|
import Sidebar from "./components/Sidebar";
|
||||||
|
import { AuthProvider } from "./context/AuthContext";
|
||||||
|
import { ThemeProvider,useTheme } from "./context/ThemeContext";
|
||||||
|
import { SidebarProvider } from './context/SidebarContext';
|
||||||
|
|
||||||
|
function LayoutContent({ children }) {
|
||||||
|
const { darkMode } = useTheme();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const isAuthPage = pathname === '/' || pathname.startsWith('/login');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<body className={isAuthPage ? "" : "flex h-screen overflow-hidden"}>
|
||||||
|
<AuthProvider>
|
||||||
|
{!isAuthPage ? (
|
||||||
|
<>
|
||||||
|
<Sidebar />
|
||||||
|
<div className="flex flex-col flex-1 overflow-hidden">
|
||||||
|
<Navbar />
|
||||||
|
<main
|
||||||
|
className={`flex-1 overflow-y-auto p-4 ${
|
||||||
|
darkMode
|
||||||
|
? "bg-gray-800 text-white"
|
||||||
|
: "bg-white text-gray-800"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
|
</AuthProvider>
|
||||||
|
</body>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({ children }) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Employee Portal</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" href="/images/brand/brand-08.svg" />
|
||||||
|
</head>
|
||||||
|
<ThemeProvider>
|
||||||
|
<SidebarProvider>
|
||||||
|
<LayoutContent>{children}</LayoutContent>
|
||||||
|
</SidebarProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/app/lib/api/services.js
Normal file
16
src/app/lib/api/services.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// src/app/lib/api/jsonPlaceholder.js
|
||||||
|
export async function fetchJsonPlaceholderData(dataType) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://jsonplaceholder.typicode.com/${dataType}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
return Array.isArray(json) ? json.slice(0, 5) : [];
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching data:", err);
|
||||||
|
throw err; // Re-throw the error to handle it in the component
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/app/lib/appwrite.js
Normal file
31
src/app/lib/appwrite.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// src/app/lib/appwrite.js
|
||||||
|
import { Client, Account, ID } from "appwrite";
|
||||||
|
|
||||||
|
// Validate environment variables at build time
|
||||||
|
const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT;
|
||||||
|
const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID;
|
||||||
|
|
||||||
|
if (!endpoint || !projectId) {
|
||||||
|
throw new Error(`
|
||||||
|
Appwrite configuration error:
|
||||||
|
NEXT_PUBLIC_APPWRITE_ENDPOINT = ${endpoint}
|
||||||
|
NEXT_PUBLIC_APPWRITE_PROJECT_ID = ${projectId}
|
||||||
|
Please check your environment variables
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize client
|
||||||
|
const client = new Client()
|
||||||
|
.setEndpoint(endpoint)
|
||||||
|
.setProject(projectId);
|
||||||
|
|
||||||
|
const account = new Account(client);
|
||||||
|
|
||||||
|
// Debug logs (remove in production)
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
console.log('Appwrite initialized with:');
|
||||||
|
console.log('Endpoint:', endpoint);
|
||||||
|
console.log('Project ID:', projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { client, account, ID };
|
||||||
21
src/app/login/page.js
Normal file
21
src/app/login/page.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// src/app/login/page.js
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
// Remove the explicit dynamic export since we're handling it via the config
|
||||||
|
const AuthForm = dynamic(
|
||||||
|
() => import('../components/AuthForm'),
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
loading: () => <div className="min-h-screen flex justify-center items-center">Loading...</div>
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen flex justify-center items-center">
|
||||||
|
<AuthForm />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
56
src/app/not-found.js
Normal file
56
src/app/not-found.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// app/not-found.js (not in a 'page.js' file)
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative z-1 flex min-h-screen flex-col items-center justify-center overflow-hidden p-6">
|
||||||
|
{/* Metadata - App Router style */}
|
||||||
|
<title>404 Error Page | TailAdmin</title>
|
||||||
|
<meta name="description" content="Page not found" />
|
||||||
|
|
||||||
|
{/* Centered Content */}
|
||||||
|
<div className="mx-auto w-full max-w-[242px] text-center sm:max-w-[472px]">
|
||||||
|
<h1 className="mb-8 text-4xl font-bold text-gray-800 dark:text-white/90">
|
||||||
|
ERROR
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Optimized Images */}
|
||||||
|
<Image
|
||||||
|
src="/images/error/404.svg"
|
||||||
|
alt="404"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
className="dark:hidden mx-auto"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
src="/images/error/404-dark.svg"
|
||||||
|
alt="404"
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
className="hidden dark:block mx-auto"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p className="mb-6 mt-10 text-base text-gray-700 dark:text-gray-400 sm:text-lg">
|
||||||
|
We can't seem to find the page you are looking for!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="inline-flex items-center justify-center rounded-lg border border-gray-300 bg-white px-5 py-3.5 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200"
|
||||||
|
>
|
||||||
|
Back to Home Page
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<p className="absolute bottom-6 left-1/2 -translate-x-1/2 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
© {new Date().getFullYear()} - TailAdmin
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
src/app/page.js
Normal file
13
src/app/page.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// src/app/page.js
|
||||||
|
'use client'; // Add this at the top
|
||||||
|
import AuthForm from "./components/AuthForm";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||||
|
<AuthForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
168
src/app/page.module.css
Normal file
168
src/app/page.module.css
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
.page {
|
||||||
|
--gray-rgb: 0, 0, 0;
|
||||||
|
--gray-alpha-200: rgba(var(--gray-rgb), 0.08);
|
||||||
|
--gray-alpha-100: rgba(var(--gray-rgb), 0.05);
|
||||||
|
|
||||||
|
--button-primary-hover: #383838;
|
||||||
|
--button-secondary-hover: #f2f2f2;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 20px 1fr 20px;
|
||||||
|
align-items: center;
|
||||||
|
justify-items: center;
|
||||||
|
min-height: 100svh;
|
||||||
|
padding: 80px;
|
||||||
|
gap: 64px;
|
||||||
|
font-family: var(--font-geist-sans);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.page {
|
||||||
|
--gray-rgb: 255, 255, 255;
|
||||||
|
--gray-alpha-200: rgba(var(--gray-rgb), 0.145);
|
||||||
|
--gray-alpha-100: rgba(var(--gray-rgb), 0.06);
|
||||||
|
|
||||||
|
--button-primary-hover: #ccc;
|
||||||
|
--button-secondary-hover: #1a1a1a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32px;
|
||||||
|
grid-row-start: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main ol {
|
||||||
|
font-family: var(--font-geist-mono);
|
||||||
|
padding-left: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 24px;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
list-style-position: inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main li:not(:last-of-type) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main code {
|
||||||
|
font-family: inherit;
|
||||||
|
background: var(--gray-alpha-100);
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctas {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctas a {
|
||||||
|
appearance: none;
|
||||||
|
border-radius: 128px;
|
||||||
|
height: 48px;
|
||||||
|
padding: 0 20px;
|
||||||
|
border: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition:
|
||||||
|
background 0.2s,
|
||||||
|
color 0.2s,
|
||||||
|
border-color 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.primary {
|
||||||
|
background: var(--foreground);
|
||||||
|
color: var(--background);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.secondary {
|
||||||
|
border-color: var(--gray-alpha-200);
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
grid-row-start: 3;
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer img {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enable hover only on non-touch devices */
|
||||||
|
@media (hover: hover) and (pointer: fine) {
|
||||||
|
a.primary:hover {
|
||||||
|
background: var(--button-primary-hover);
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.secondary:hover {
|
||||||
|
background: var(--button-secondary-hover);
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.page {
|
||||||
|
padding: 32px;
|
||||||
|
padding-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main ol {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctas {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctas a {
|
||||||
|
font-size: 14px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.secondary {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.logo {
|
||||||
|
filter: invert();
|
||||||
|
}
|
||||||
|
}
|
||||||
218
src/app/pages/TemporaryTable/page.js
Normal file
218
src/app/pages/TemporaryTable/page.js
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
'use client';
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Table, TableBody, TableCell, TableHeader, TableRow } from "../../ui/Table";
|
||||||
|
import Badge from "../../ui/Badge";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { fetchJsonPlaceholderData } from "../../lib/api/services";
|
||||||
|
|
||||||
|
export default function DataTable() {
|
||||||
|
const [data, setData] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [dataType, setDataType] = useState('todos');
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const result = await fetchJsonPlaceholderData(dataType);
|
||||||
|
setData(result);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching data:", err);
|
||||||
|
setError(err.message);
|
||||||
|
setData([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
}, [dataType]);
|
||||||
|
|
||||||
|
const renderTableContent = () => {
|
||||||
|
// ... (keep the existing switch case for headers)
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTableRows = () => {
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="text-center py-4">
|
||||||
|
Loading...
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="text-center py-4 text-red-500">
|
||||||
|
Error: {error}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.length) {
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="text-center py-4">
|
||||||
|
No data available
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.map((item) => {
|
||||||
|
if (!item) return null;
|
||||||
|
|
||||||
|
switch (dataType) {
|
||||||
|
case 'todos':
|
||||||
|
return (
|
||||||
|
<TableRow key={item.id}>
|
||||||
|
<TableCell className="px-5 py-4 sm:px-6 text-start">
|
||||||
|
{item.userId || 'N/A'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-4 py-3 text-gray-500 text-start text-theme-sm dark:text-gray-400">
|
||||||
|
{item.title || 'No title'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-4 py-3 text-gray-500 text-start text-theme-sm dark:text-gray-400">
|
||||||
|
<Badge
|
||||||
|
size="sm"
|
||||||
|
color={item.completed ? "success" : "error"}
|
||||||
|
>
|
||||||
|
{item.completed ? "Completed" : "Pending"}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
case 'comments':
|
||||||
|
return (
|
||||||
|
<TableRow key={item.id}>
|
||||||
|
<TableCell className="px-5 py-4 sm:px-6 text-start">
|
||||||
|
{item.name || 'Anonymous'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-4 py-3 text-gray-500 text-start text-theme-sm dark:text-gray-400">
|
||||||
|
{item.email || 'No email'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-4 py-3 text-gray-500 text-start text-theme-sm dark:text-gray-400">
|
||||||
|
{item.body ? `${item.body.substring(0, 50)}...` : 'No content'}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
case 'albums':
|
||||||
|
return (
|
||||||
|
<TableRow key={item.id}>
|
||||||
|
<TableCell className="px-5 py-4 sm:px-6 text-start">
|
||||||
|
{item.userId || 'N/A'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-4 py-3 text-gray-500 text-start text-theme-sm dark:text-gray-400">
|
||||||
|
{item.title || 'Untitled'}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
case 'photos':
|
||||||
|
return (
|
||||||
|
<TableRow key={item.id}>
|
||||||
|
<TableCell className="px-5 py-4 sm:px-6 text-start">
|
||||||
|
{item.albumId || 'N/A'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-4 py-3 text-gray-500 text-start text-theme-sm dark:text-gray-400">
|
||||||
|
{item.title || 'Untitled'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-4 py-3 text-gray-500 text-start text-theme-sm dark:text-gray-400">
|
||||||
|
<div className="w-10 h-10 overflow-hidden rounded-full">
|
||||||
|
{item.thumbnailUrl ? (
|
||||||
|
<Image
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
src={item.thumbnailUrl}
|
||||||
|
alt={item.title || 'Photo'}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full bg-gray-200 flex items-center justify-center">
|
||||||
|
<span className="text-gray-500">No image</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
case 'users':
|
||||||
|
return (
|
||||||
|
<TableRow key={item.id}>
|
||||||
|
<TableCell className="px-5 py-4 sm:px-6 text-start">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 overflow-hidden rounded-full bg-gray-200 flex items-center justify-center">
|
||||||
|
<span className="text-gray-600 font-medium">
|
||||||
|
{item.name ? item.name.charAt(0) : '?'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="block font-medium text-gray-800 text-theme-sm dark:text-white/90">
|
||||||
|
{item.name || 'Unknown'}
|
||||||
|
</span>
|
||||||
|
<span className="block text-gray-500 text-theme-xs dark:text-gray-400">
|
||||||
|
@{item.username || 'user'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-4 py-3 text-gray-500 text-start text-theme-sm dark:text-gray-400">
|
||||||
|
{item.email || 'No email'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-4 py-3 text-gray-500 text-start text-theme-sm dark:text-gray-400">
|
||||||
|
{item.company?.name || 'No company'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="px-4 py-3 text-gray-500 text-theme-sm dark:text-gray-400">
|
||||||
|
{item.phone || 'No phone'}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-white/[0.05] dark:bg-white/[0.03]">
|
||||||
|
<div className="flex justify-between items-center p-4 border-b border-gray-100 dark:border-white/[0.05]">
|
||||||
|
<h3 className="font-medium text-gray-800 dark:text-white/90">
|
||||||
|
{dataType.charAt(0).toUpperCase() + dataType.slice(1)} Data
|
||||||
|
</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{['todos', 'comments', 'albums', 'photos', 'users'].map((type) => (
|
||||||
|
<button
|
||||||
|
key={type}
|
||||||
|
onClick={() => setDataType(type)}
|
||||||
|
className={`px-3 py-1 text-sm rounded-md ${
|
||||||
|
dataType === type
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{type.charAt(0).toUpperCase() + type.slice(1)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="max-w-full overflow-x-auto">
|
||||||
|
<div className="min-w-[1102px]">
|
||||||
|
<Table>
|
||||||
|
<TableHeader className="border-b border-gray-100 dark:border-white/[0.05]">
|
||||||
|
<TableRow>
|
||||||
|
{renderTableContent()}
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody className="divide-y divide-gray-100 dark:divide-white/[0.05]">
|
||||||
|
{renderTableRows()}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
src/app/pages/dashboard/page.js
Normal file
33
src/app/pages/dashboard/page.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// pages/dashboard.js
|
||||||
|
'use client';
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { account } from "../../lib/appwrite";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
export default function Dashboard() {
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkAuth = async () => {
|
||||||
|
try {
|
||||||
|
const currentUser = await account.get();
|
||||||
|
setUser(currentUser);
|
||||||
|
} catch (err) {
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
checkAuth();
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return <div className="p-4">Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<h1>Welcome, {user.name}</h1>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
125
src/app/pages/reset-password/page.js
Normal file
125
src/app/pages/reset-password/page.js
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState, Suspense } from 'react';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
import { useAuth } from '../../context/AuthContext';
|
||||||
|
|
||||||
|
function ResetPasswordContent() {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const { resetPassword } = useAuth();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [success, setSuccess] = useState(false);
|
||||||
|
|
||||||
|
// Get userId and secret from URL
|
||||||
|
const userId = searchParams.get('userId');
|
||||||
|
const secret = searchParams.get('secret');
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
password: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await resetPassword(
|
||||||
|
userId,
|
||||||
|
secret,
|
||||||
|
formData.password,
|
||||||
|
formData.confirmPassword
|
||||||
|
);
|
||||||
|
setSuccess(true);
|
||||||
|
setTimeout(() => window.location.href = '/', 3000);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || 'Password reset failed');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[e.target.name]: e.target.value
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!userId || !secret) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-10">
|
||||||
|
<h2 className="text-xl font-bold">Invalid reset link</h2>
|
||||||
|
<p>The password reset link is invalid or has expired.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-10">
|
||||||
|
<h2 className="text-xl font-bold">Password reset successful!</h2>
|
||||||
|
<p>You will be redirected to the login page shortly.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-md mx-auto p-6">
|
||||||
|
<h1 className="text-2xl font-bold mb-6">Reset Password</h1>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 p-3 text-sm text-red-600 bg-red-50 rounded-lg">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block mb-1">New Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full p-2 border rounded"
|
||||||
|
required
|
||||||
|
minLength="8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block mb-1">Confirm Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="confirmPassword"
|
||||||
|
value={formData.confirmPassword}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full p-2 border rounded"
|
||||||
|
required
|
||||||
|
minLength="8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full py-2 px-4 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? 'Resetting...' : 'Reset Password'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ResetPassword() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
|
<ResetPasswordContent />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
14
src/app/pages/table01/page.js
Normal file
14
src/app/pages/table01/page.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// app/pages/Table01/pages.js
|
||||||
|
import React from 'react';
|
||||||
|
import BasicTableOne from '../../components/BasicTableOne';
|
||||||
|
|
||||||
|
const table01 = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Page with TableOne</h1>
|
||||||
|
<BasicTableOne />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default table01;
|
||||||
60
src/app/ui/Badge.js
Normal file
60
src/app/ui/Badge.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
|
||||||
|
const Badge = ({
|
||||||
|
variant = "light",
|
||||||
|
color = "primary",
|
||||||
|
size = "md",
|
||||||
|
startIcon,
|
||||||
|
endIcon,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const baseStyles =
|
||||||
|
"inline-flex items-center px-2.5 py-0.5 justify-center gap-1 rounded-full font-medium";
|
||||||
|
|
||||||
|
// Define size styles
|
||||||
|
const sizeStyles = {
|
||||||
|
sm: "text-theme-xs", // Smaller padding and font size
|
||||||
|
md: "text-sm", // Default padding and font size
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define color styles for variants
|
||||||
|
const variants = {
|
||||||
|
light: {
|
||||||
|
primary:
|
||||||
|
"bg-brand-50 text-brand-500 dark:bg-brand-500/15 dark:text-brand-400",
|
||||||
|
success:
|
||||||
|
"bg-success-50 text-success-600 dark:bg-success-500/15 dark:text-success-500",
|
||||||
|
error:
|
||||||
|
"bg-error-50 text-error-600 dark:bg-error-500/15 dark:text-error-500",
|
||||||
|
warning:
|
||||||
|
"bg-warning-50 text-warning-600 dark:bg-warning-500/15 dark:text-orange-400",
|
||||||
|
info: "bg-blue-light-50 text-blue-light-500 dark:bg-blue-light-500/15 dark:text-blue-light-500",
|
||||||
|
light: "bg-gray-100 text-gray-700 dark:bg-white/5 dark:text-white/80",
|
||||||
|
dark: "bg-gray-500 text-white dark:bg-white/5 dark:text-white",
|
||||||
|
},
|
||||||
|
solid: {
|
||||||
|
primary: "bg-brand-500 text-white dark:text-white",
|
||||||
|
success: "bg-success-500 text-white dark:text-white",
|
||||||
|
error: "bg-error-500 text-white dark:text-white",
|
||||||
|
warning: "bg-warning-500 text-white dark:text-white",
|
||||||
|
info: "bg-blue-light-500 text-white dark:text-white",
|
||||||
|
light: "bg-gray-400 dark:bg-white/5 text-white dark:text-white/80",
|
||||||
|
dark: "bg-gray-700 text-white dark:text-white",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get styles based on size and color variant
|
||||||
|
const sizeClass = sizeStyles[size];
|
||||||
|
const colorStyles = variants[variant][color];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={`${baseStyles} ${sizeClass} ${colorStyles}`}>
|
||||||
|
{startIcon && <span className="mr-1">{startIcon}</span>}
|
||||||
|
{children}
|
||||||
|
{endIcon && <span className="ml-1">{endIcon}</span>}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Badge;
|
||||||
33
src/app/ui/Table.js
Normal file
33
src/app/ui/Table.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import React, { ReactNode } from "react";
|
||||||
|
|
||||||
|
// Table Component
|
||||||
|
const Table = ({ children, className }) => {
|
||||||
|
return <table className={`min-w-full ${className}`}>{children}</table>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TableHeader Component
|
||||||
|
const TableHeader = ({ children, className }) => {
|
||||||
|
return <thead className={className}>{children}</thead>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TableBody Component
|
||||||
|
const TableBody = ({ children, className }) => {
|
||||||
|
return <tbody className={className}>{children}</tbody>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TableRow Component
|
||||||
|
const TableRow = ({ children, className }) => {
|
||||||
|
return <tr className={className}>{children}</tr>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TableCell Component
|
||||||
|
const TableCell = ({
|
||||||
|
children,
|
||||||
|
isHeader = false,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
const CellTag = isHeader ? "th" : "td";
|
||||||
|
return <CellTag className={` ${className}`}>{children}</CellTag>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Table, TableHeader, TableBody, TableRow, TableCell };
|
||||||
Loading…
Reference in New Issue
Block a user