diff --git a/.gitignore b/.gitignore index 5ef6a52..1c61c9b 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ /.next/ /out/ +.env + # production /build diff --git a/.temp.env b/.temp.env deleted file mode 100644 index 80b4fa8..0000000 --- a/.temp.env +++ /dev/null @@ -1,5 +0,0 @@ -BACKEND_URL="process.env.BACKEND_URL" - -GOOGLE_CLIENT_ID="" -GOOGLE_CLIENT_SECRET="" -NEXTAUTH_SECRET="" diff --git a/app/(protected)/dashboard/me/page.tsx b/app/(protected)/dashboard/me/page.tsx new file mode 100644 index 0000000..6862c12 --- /dev/null +++ b/app/(protected)/dashboard/me/page.tsx @@ -0,0 +1,6 @@ +const Me = async () => { + await new Promise((resolve) => setTimeout(resolve, 10000)); + return
Me
; +}; + +export default Me; diff --git a/app/(protected)/layout.tsx b/app/(protected)/layout.tsx index d6dfde0..f617885 100644 --- a/app/(protected)/layout.tsx +++ b/app/(protected)/layout.tsx @@ -2,8 +2,10 @@ import { useEffect } from "react"; import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; +import Loader from "@/components/ui/Loader"; +import Header from "@/components/Header"; -export default function MePage() { +export default function MePage({ children }: { children: React.ReactNode }) { const { data: session, status } = useSession(); const router = useRouter(); @@ -13,8 +15,14 @@ export default function MePage() { } }, [status, router]); - if (status === "loading") return

Loading...

; + if (status === "loading") return ; if (!session) return null; - return

Welcome, {session.user?.name}!

; + + return ( +
+
+
{children}
+
+ ); } diff --git a/app/(protected)/me/page.tsx b/app/(protected)/me/page.tsx deleted file mode 100644 index b356c14..0000000 --- a/app/(protected)/me/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -const Me = () => { - return
Me
; -}; - -export default Me; diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index 1453f25..c4e7a0c 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -14,11 +14,13 @@ export const authConfig = { password: { label: "Password", type: "password" }, }, async authorize(credentials) { - const res = await axios.post(`${process.env.BACKEND_URL}/login`, { + const res = await axios.post(`${process.env.NEXT_PUBLIC_API_URL}/login`, { + // const res = await axios.post(`http://localhost8080/login`, { username: credentials?.username, password: credentials?.password, }); + console.log("res", res.data); const user = res.data.data; if (res.status && user) { @@ -69,7 +71,7 @@ export const authConfig = { if (account?.provider === "google") { // Call your backend API to find or register the user try { - const res = await axios.post(`${process.env.BACKEND_URL}/api/v1/oauth`, { + const res = await axios.post(`${process.env.API_URL}/api/v1/oauth`, { email: profile?.email, name: profile?.name, googleId: profile?.sub, @@ -91,7 +93,7 @@ export const authConfig = { if (account?.provider === "github") { // Call your backend API to find or register the user try { - const res = await axios.post(`${process.env.BACKEND_URL}/api/v1/oauth`, { + const res = await axios.post(`${process.env.API_URL}/api/v1/oauth`, { email: profile?.email, name: profile?.name, githubId: profile?.sub, @@ -117,7 +119,7 @@ export const authConfig = { // Custom pages (optional) pages: { - signIn: "/auth/signin", // Custom sign-in page + signIn: "/signin", // Custom sign-in page }, } satisfies NextAuthOptions; diff --git a/app/api/auth/googleAuth.ts b/app/api/auth/googleAuth.ts deleted file mode 100644 index 9239dde..0000000 --- a/app/api/auth/googleAuth.ts +++ /dev/null @@ -1,13 +0,0 @@ -export async function authenticateGoogle(token: string) { - const res = await fetch(`${process.env.BACKEND_URL}/api/auth/google`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ token }), - }); - - if (!res.ok) { - throw new Error("Google authentication failed"); - } - - return res.json(); -} diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts deleted file mode 100644 index 8bf8d17..0000000 --- a/app/api/auth/login/route.ts +++ /dev/null @@ -1,49 +0,0 @@ -// app/api/login/route.js -import { NextResponse } from "next/server"; -import jwt from "jsonwebtoken"; - -// Secret key for JWT signing -const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; - -// Mock users array (replace with real DB in production) -let users = [ - { - id: "1", - name: "Test User", - email: "amr@me.com", - password: "123123", - profilePicture: null, - }, -]; // Sample user for testing - -export async function POST(req: Request) { - const { email, password } = await req.json(); - - // Simple validation - if (!email || !password) { - return NextResponse.json( - { error: "Email and password are required" }, - { status: 400 } - ); - } - - // Find the user and check password - const user = users.find((user) => user.email === email); - - if (!user || user.password !== password) { - return NextResponse.json({ error: "Invalid credentials" }, { status: 401 }); - } - - // Generate JWT token with user data - const token = jwt.sign( - { userId: user.id, name: user.name, email: user.email }, - JWT_SECRET, - { expiresIn: "1h" } - ); - - // Send back the response with the token and user data - return NextResponse.json( - { message: "Login successful", token, user }, - { status: 200 } - ); -} diff --git a/app/api/auth/signup/route.ts b/app/api/auth/signup/route.ts deleted file mode 100644 index ff26d79..0000000 --- a/app/api/auth/signup/route.ts +++ /dev/null @@ -1,52 +0,0 @@ -// app/api/signup/route.js -import { NextResponse } from "next/server"; -import jwt from "jsonwebtoken"; -import { User } from "@/types"; - -// Secret key for JWT signing -const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; - -// Mock users array (replace with real DB in production) -let users: User[] = []; - -export async function POST(req: Request) { - const { name, email, password, profilePicture } = await req.json(); - - // Simple validation - if (!name || !email || !password) { - return NextResponse.json( - { error: "Name, email, and password are required" }, - { status: 400 } - ); - } - - // Check if the user already exists - const existingUser = users.find((user) => user.email === email); - if (existingUser) { - return NextResponse.json({ error: "User already exists" }, { status: 400 }); - } - - // Create a new user object - const newUser = { - id: Date.now().toString(), - name, - email, - profilePicture: profilePicture || null, // Optional profile picture - }; - - // Store the user - users.push(newUser); - - // Generate JWT token with user data - const token = jwt.sign( - { userId: newUser.id, name: newUser.name, email: newUser.email }, - JWT_SECRET, - { expiresIn: "1h" } - ); - - // Send back the response with the token and user data - return NextResponse.json( - { message: "User created", token, user: newUser }, - { status: 201 } - ); -} diff --git a/app/layout.tsx b/app/layout.tsx index fae4bca..597eebd 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,8 +1,8 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; -import Header from "@/components/Header"; import AuthProvider from "@/components/auth/AuthProvider"; +import { Toaster } from "react-hot-toast"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -25,17 +25,15 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - -
- -
-
{children}
- -
- - + + +
+ +
{children}
+
+ +
+ + ); } diff --git a/app/loading.tsx b/app/loading.tsx new file mode 100644 index 0000000..d64546d --- /dev/null +++ b/app/loading.tsx @@ -0,0 +1,9 @@ +import Loader from "@/components/ui/Loader"; + +export default function Loading() { + return ( +
+ +
+ ); +} diff --git a/app/login/page.tsx b/app/login/page.tsx index 8f84777..65192fa 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,21 +1,11 @@ -import { GalleryVerticalEnd } from "lucide-react"; - import { LoginForm } from "@/components/auth/login-form"; export default function LoginPage() { return (
- -
- -
- Acme Inc. -
); } - - diff --git a/app/page.tsx b/app/page.tsx index 7fa4e02..e7f887e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,10 @@ +import Header from "@/components/Header"; export default function Home() { return ( - -

Welcome to your Next.js app!

+
+
+

Welcome to your Next.js app!

+
); } diff --git a/app/signup/page.tsx b/app/signup/page.tsx index 115be6d..4afd6f2 100644 --- a/app/signup/page.tsx +++ b/app/signup/page.tsx @@ -2,16 +2,11 @@ import { GalleryVerticalEnd } from "lucide-react"; import { SignupForm } from "@/components/auth/signup-form"; + export default function SignPage() { return (
-
- -
- -
- Acme Inc. -
+
diff --git a/auth.config.ts b/auth.config.ts deleted file mode 100644 index e69de29..0000000 diff --git a/components/Header.tsx b/components/Header.tsx index de21113..14f44d7 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -1,47 +1,40 @@ "use client"; -import { useEffect } from "react"; -import useUserStore from "../zustand/userStore"; + import { Button } from "./ui/button"; import { useRouter } from "next/navigation"; import useAuthenticate from "../hooks/useAuthenticate"; +import { Skeleton } from "@/components/ui/skeleton"; export default function Header() { - const { session, status, isSessionExpired, signOut } = useAuthenticate(); - const { user, clearUser } = useUserStore(); - const router = useRouter(); - - useEffect(() => { - if (isSessionExpired) { - clearUser(); - } - }, [isSessionExpired, clearUser]); - - if (status === "loading") { - return null; - } - - return ( -
- -
- ); + return ( +
+ +
+ ); } diff --git a/components/auth/login-form.tsx b/components/auth/login-form.tsx index dbfe3b9..3a76e2e 100644 --- a/components/auth/login-form.tsx +++ b/components/auth/login-form.tsx @@ -14,17 +14,26 @@ import { useRouter } from "next/navigation"; import { useSession, signIn } from "next-auth/react"; import { AuthSignInButton } from "@/components/ui/AuthSignInButton"; import { AuthSignupButton } from "@/components/ui/AuthSignupButton"; +import { GalleryVerticalEnd } from "lucide-react"; +import Link from "next/link"; +import Loader from "../ui/Loader"; +import toast from "react-hot-toast"; +import { useEffect } from "react"; export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<"div">) { const router = useRouter(); - const { data: session } = useSession(); - if (session) { - router.push("/"); - } - console.log(session); + const { status } = useSession(); + useEffect(() => { + if (status === "authenticated") { + router.replace("/"); + } + }, [status, router]); + + if (status === "loading") return ; + const handleLogin = async (event: React.FormEvent) => { event.preventDefault(); @@ -33,109 +42,111 @@ export function LoginForm({ const password = formData.get("password") as string | null; if (!username || !password) { - return alert("Please enter both email and password."); + return toast.error("Please fill in all fields."); } - try { signIn("credentials", { username, password }); } catch (error) { console.error("Login failed", error); - alert("Invalid credentials or server error."); + toast.error("Login failed"); } }; - return ( -
- - - Welcome back - - Login with your Github or Google account - - - -
-
-
- - - - - - Login with Github - - - - - - Login with Google - -
-
- - Or continue with - -
-
-
- - -
-
- - -
- -
-
- Don't have an account?{" "} - - Sign up - -
-
-
-
-
-
- By clicking continue, you agree to our Terms of Service{" "} - and Privacy Policy. -
+
+ + + + +
+ +
+

Skillify.io

+ +
+ + Login with your Github or Google account + +
+ +
+
+
+ + + + + + Login with Github + + + + + + Login with Google + +
+
+ + Or continue with + +
+
+
+ + +
+
+ + +
+ +
+
+ Don't have an account?{" "} + + Sign up + +
+
+
+
+
+
+ By clicking continue, you agree to our Terms of Service and{" "} + Privacy Policy.
+
); } diff --git a/components/auth/signup-form.tsx b/components/auth/signup-form.tsx index 65a28eb..e4c2af4 100644 --- a/components/auth/signup-form.tsx +++ b/components/auth/signup-form.tsx @@ -2,186 +2,169 @@ import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import useUserStore from "../../zustand/userStore"; import axios from "axios"; -import useAuthenticate from "@/hooks/useAuthenticate"; import { useRouter } from "next/navigation"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { AuthSignupButton } from "@/components/ui/AuthSignupButton"; +import { Role } from "@/types"; +import { GalleryVerticalEnd} from "lucide-react"; +import Link from "next/link"; +import toast from "react-hot-toast"; export function SignupForm({ - className, - ...props + className, + ...props }: React.ComponentPropsWithoutRef<"div">) { - const { signIn } = useAuthenticate(); + const router = useRouter(); - const handleSignup = async (formData: FormData) => { - const name = formData.get("name"); - const email = formData.get("email"); - const password = formData.get("password"); - const role = formData.get("role"); - console.log(role); - const router = useRouter(); - try { - const req = await axios.post("/api/v1/auth/signup", { - name, - email, - password, - role, - }); - router.replace("/login"); - } catch (err) { - alert(`singupError: ` + err); - } - }; - // const { data, user } = req.data; - return ( -
- - Welcome back - - - Signup with your Github or Google account - - - - - - As recruter - - - As a candidate - - - - - - - - - - -
- By clicking continue, you agree to our Terms of Service{" "} - and Privacy Policy. + const handleSignup = async (formData: FormData) => { + const username = formData.get("username"); + const email = formData.get("email"); + const password = formData.get("password"); + const role = formData.get("role"); + try { + const req = await axios.post(`${process.env.NEXT_PUBLIC_API_URL}/register`, { + username, + email, + password, + role, + }); + if (req.status === 200) toast.success("Signup successful"); + setTimeout(() => { + router.replace("/login"); + }, 2000); + } catch (err) { + toast.error("Signup failed"); + } + }; + return ( +
+ + + +
+
-
- ); +

Skillify.io

+ + + + + Signup to our platform and get started + + + + + As a candidate + + + As a recruter + + + + + + + + + + +
+ By clicking continue, you agree to our Terms of Service and{" "} + Privacy Policy. +
+
+ ); } -function SignUpContent({ role, handleSignup }) { - const { signIn } = useAuthenticate(); - return ( - -
-
-
- - - - - - Signup with Github - - - - - - Signup with Google - -
-
- - Or continue with - -
-
-
- - -
-
- - -
-
- - - {role === "recruiter" && ( - <> - - - - )} -
- +function SignUpContent({ + role, + handleSignup, +}: { + role: Role; + handleSignup: (formData: FormData) => void; +}) { + return ( + + +
+
+ + Or continue with + +
+
+
+ + +
+
+ + +
+
+ + + {role === Role.ROLE_RECRUITER && ( + <> + + + + )} +
+ - -
-
- Alread have an account?{" "} - - Sign in - -
-
- -
- ); + +
+
+ Alread have an account?{" "} + + Sign in + +
+
+ +
+ ); } diff --git a/components/ui/Loader.tsx b/components/ui/Loader.tsx new file mode 100644 index 0000000..b29146f --- /dev/null +++ b/components/ui/Loader.tsx @@ -0,0 +1,12 @@ +// components/Loading.tsx +"use client"; + +import { PulseLoader } from "react-spinners"; + +export default function Loader() { + return ( +
+ +
+ ); +} diff --git a/components/ui/skeleton.tsx b/components/ui/skeleton.tsx new file mode 100644 index 0000000..d7e45f7 --- /dev/null +++ b/components/ui/skeleton.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/middleware.ts b/middleware.ts index 252c9bc..62fdbe9 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,17 +1,21 @@ -import { authConfig } from "@/app/api/auth/[...nextauth]/route"; -import NextAuth from "next-auth"; +// import { authConfig } from "@/app/api/auth/[...nextauth]/route"; +// import NextAuth from "next-auth"; -const { auth } = NextAuth(authConfig); +// const { auth } = NextAuth(authConfig); -export default auth((req: any) => { - const isAuthenticated = !!req.auth; +// export default auth((req: any) => { +// const isAuthenticated = !!req.auth; - if (!isAuthenticated) { - return Response.redirect(new URL("/auth/signin", req.url)); - } -}); +// if (!isAuthenticated) { +// return Response.redirect(new URL("/login", req.url)); +// } +// }); -// Apply middleware to specific routes -export const config = { - matcher: ["/dashboard/:path*"], // Protect all routes under /dashboard -}; +// // Apply middleware to specific routes +// export const config = { +// matcher: ["/dashboard/:path*"], // Protect all routes under /dashboard +// }; + +export default function Middleware() { + return null; +} diff --git a/package-lock.json b/package-lock.json index e835c07..374e63d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,8 @@ "next-auth": "^4.24.11", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hot-toast": "^2.5.1", + "react-spinners": "^0.15.0", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "zustand": "^5.0.3" @@ -2316,7 +2318,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -3574,6 +3575,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -5335,6 +5345,23 @@ "react": "^19.0.0" } }, + "node_modules/react-hot-toast": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.1.tgz", + "integrity": "sha512-54Gq1ZD1JbmAb4psp9bvFHjS7lje+8ubboUmvKZkCsQBLH6AOpZ9JemfRvIdHcfb9AZXRaFLrb3qUobGYDJhFQ==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -5342,6 +5369,16 @@ "dev": true, "license": "MIT" }, + "node_modules/react-spinners": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.15.0.tgz", + "integrity": "sha512-ZO3/fNB9Qc+kgpG3SfdlMnvTX6LtLmTnOogb3W6sXIaU/kZ1ydEViPfZ06kSOaEsor58C/tzXw2wROGQu3X2pA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/package.json b/package.json index 6383968..425db73 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "next-auth": "^4.24.11", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hot-toast": "^2.5.1", + "react-spinners": "^0.15.0", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "zustand": "^5.0.3" diff --git a/types.ts b/types.ts index 7b9528b..14398b7 100644 --- a/types.ts +++ b/types.ts @@ -13,3 +13,11 @@ export interface UserStoreState { setExpiration: (expiresAt: number | null) => void; clearUser: () => void; } + + + +export enum Role { + ROLE_ADMIN = 'ROLE_ADMIN', + ROLE_RECRUITER = 'ROLE_RECRUITER', + ROLE_CANDIDATE = 'ROLE_CANDIDATE', +}