Skip to content

Commit

Permalink
🚀 Handle user creation and login in Google strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
Simon-157 committed Jun 2, 2024
1 parent 6c6396d commit 1c4aee7
Show file tree
Hide file tree
Showing 11 changed files with 285 additions and 10 deletions.
4 changes: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
REACT_APP_RAPID_API_URL=https://judge0-ce.p.rapidapi.com/submissions
REACT_APP_RAPID_API_HOST=judge0-ce.p.rapidapi.com
REACT_APP_RAPID_API_KEY=0e58c401femsh4bc6914c8b658f0p1108a6jsn88ac8d898095

NODE_ENV=development
NEXT_PUBLIC_DEV_API_URL=http://localhost:8000
NEXT_PUBLIC_PROD_API_URL=https://wanderer.vercel.app
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@tensorflow/tfjs": "^4.17.0",
"@tensorflow/tfjs-backend-webgl": "^4.17.0",
"@types/node": "20.11.24",
"@types/react": "18.2.63",
"@types/react-dom": "18.2.19",
Expand All @@ -49,8 +47,10 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-loader-spinner": "^6.1.6",
"react-query": "^3.39.3",
"react-resizable-panels": "^2.0.18",
"sonner": "^1.4.41",
"socket.io-client": "^4.7.5",
"sonner": "^1.4.3",
"tailwind-merge": "^2.2.1",
"tailwindcss": "3.4.1",
"tailwindcss-animate": "^1.0.7",
Expand Down
85 changes: 80 additions & 5 deletions src/app/(auth)/login/form.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,88 @@
"use client";

'use client';
import Link from "next/link";
import Google from "@/components/shared/icons/google";
import Button from "@/components/ui/form-button";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { validateEmail, validatePassword } from "@/lib/utils";
import { api } from "@/lib/api";

// Custom hook for form state management
const useFormState = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");

return { email, password, setEmail, setPassword, error, setError };
}


const useLoginForm = () => {
const router = useRouter();

const handleGoogleLogin = () => {
const apiUrl = process.env.NODE_ENV === "production"
? process.env.NEXT_PUBLIC_PROD_API_URL
: process.env.NEXT_PUBLIC_DEV_API_URL;
const redirectUrl = `${apiUrl}/auth/google`;
window.location.href = redirectUrl;
};

const handleEmailLogin = async (email: string, password: string, error: string, setError: (error: string) => void) => {

if(!validateEmail(email)) {
setError("Please enter a valid email");
return;
}

if(!validatePassword(password)) {
setError("password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, and one number");
return;
}

try {
const res = await api.post("/auth/local", {
email,
password,
});

if (res.status === 200) {
const user = (await res.data()) as User;

// redirect to the dashboard
router.push("/dashboard");
} else {
setError("Failed to log in, please try again");
throw new Error("Failed to log in");
}
} catch (error) {
setError("Failed to log in, please try again");
console.error(error);
}
};


return { handleGoogleLogin, handleEmailLogin };
}


export default function LoginForm() {
const { email, password, setEmail, setPassword, error, setError } = useFormState();
const { handleGoogleLogin, handleEmailLogin } = useLoginForm();

const handleSubmit = () => {
handleEmailLogin(email, password, error, setError);
};


export default function RegisterForm() {
return (
<div className="flex flex-col space-y-3 bg-gray-50 px-4 py-8 sm:px-16">
<Button
text="Continue with Google"
icon={<Google className="h-4 w-4" />}
onClick={handleGoogleLogin}
/>
<form className="flex flex-col space-y-3">
<form className="flex flex-col space-y-3" onSubmit={handleSubmit}>
<div>
<div className="mb-4 mt-1 border-t border-gray-300" />
<input
Expand All @@ -21,6 +92,8 @@ export default function RegisterForm() {
type="email"
placeholder="Email"
autoComplete="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="mt-1 block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-black focus:outline-none focus:ring-black sm:text-sm"
/>
Expand All @@ -29,17 +102,19 @@ export default function RegisterForm() {
<input
id="password"
name="password"
autoFocus
type="password"
autoComplete="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="mt-1 block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-black focus:outline-none focus:ring-black sm:text-sm"
/>
</div>
<Button
text="Continue with Email"
variant="secondary"
onClick={handleSubmit}
/>
</form>
<p className="text-center text-sm text-gray-500">
Expand Down
11 changes: 10 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import "./global.css"
import UserContextProvider from '@/contexts/userContext'
import { ReactQueryClientProvider } from '@/contexts/reactQueryClientProvider';

const inter = Inter({
weight: '400',
Expand All @@ -19,7 +21,14 @@ export default function RootLayout({
}) {
return (
<html lang="en">
<body >{children}</body>
<body>
<ReactQueryClientProvider>
<UserContextProvider>
{children}
</UserContextProvider>
</ReactQueryClientProvider>
</body>
</html>

)
}
20 changes: 20 additions & 0 deletions src/contexts/reactQueryClientProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use client'

import { useState } from 'react'
import { QueryClient, QueryClientProvider } from 'react-query'
import { ReactQueryDevtools } from "react-query/devtools";


export const ReactQueryClientProvider = ({ children }: { children: React.ReactNode }) => {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
})
)
return <QueryClientProvider client={queryClient}>{children} <ReactQueryDevtools initialIsOpen={false}/></QueryClientProvider>
}
66 changes: 66 additions & 0 deletions src/contexts/socketContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"use client"
import { createContext, useContext , useState, useEffect} from "react";
import { ManagerOptions, Socket, SocketOptions, io } from "socket.io-client";
import { UserContext } from "./userContext";


type SocketContextProps = {
children: React.ReactNode
}


type SocketContext = {
socket: Socket | null,

}


export const SocketContext = createContext<SocketContext>({} as SocketContext);


export const SocketContextProvider = ({ children }: SocketContextProps) => {
const socketOptions: Partial<ManagerOptions & SocketOptions> = {
autoConnect: false,
reconnectionAttempts: 3,
withCredentials: true,
transports: ["polling"]
}


const {user, isUserLoading} = useContext(UserContext)
const [socket, setSocket] = useState<Socket | null>(null);
const [connected, setConnected] = useState(false);
const [reconnecting, setReconnecting] = useState(false);

useEffect(() => {
if (user && !isUserLoading) {
const newSocket = io(process.env.NEXT_PUBLIC_BACKEND_URL as string, socketOptions);
setSocket(newSocket);
}
}, [user, isUserLoading]);


useEffect(() => {
if (socket) {
socket.on("connect", () => {
setConnected(true);
setReconnecting(false);
});
socket.on("reconnect", () => {
setReconnecting(true);
});
socket.on("reconnect_error", () => {
setReconnecting(false);
});
socket.on("reconnect_failed", () => {
setReconnecting(false);
});
}
}, [socket]);

return (
<SocketContext.Provider value={{ socket }}>
{children}
</SocketContext.Provider>
)
}
43 changes: 43 additions & 0 deletions src/contexts/userContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"use client"

import { useQuery } from "react-query";
import { createContext } from "react";
import { api } from "@/lib/api";


type ContextProps = {
children: React.ReactNode
}


type UserContext = {
user: User,
isUserLoading: boolean
}


export const UserContext = createContext<UserContext>({} as UserContext)


const UserContextProvider = ({ children }: ContextProps) => {
const getUser = async () => {
const response = await api.get('/user')
return response.data
}
const { data: user, isLoading: isUserLoading } = useQuery('user', getUser, {
refetchOnWindowFocus: true,
staleTime: 1000 * 60 * 5,

})


return (
<UserContext.Provider value={{ user, isUserLoading }}>
{children}
</UserContext.Provider>
)
}


export default UserContextProvider;

6 changes: 6 additions & 0 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import axios from "axios";

export const api = axios.create({
baseURL: process.env.NODE_ENV === "production" ? process.env.NEXT_PUBLIC_PROD_API_URL : process.env.NEXT_PUBLIC_DEV_API_URL,
withCredentials: true,
})
16 changes: 16 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,19 @@ export function cn(...inputs: ClassValue[]) {
export const classnames = (...args: string[]): string => {
return args.join(" ");
};



export const validateEmail = (email: string) => {
const re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
};


export const validatePassword = (password: string) => {
const re = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,20}$/;
return re.test(password);
};


export const isBrowser = typeof window !== "undefined";
4 changes: 3 additions & 1 deletion src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ export type Judge0SubmissionOutput = {
}


export type languageOptionsType = typeof languageOptions
export type languageOptionsType = typeof languageOptions


34 changes: 34 additions & 0 deletions types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
type User = {
userId: string,
email: string,
firstName: string,
lastName: string,
role: string,
avatarUrl: string,
bio: string,
mfaEnabled: boolean,
verified: boolean,
phoneNumber: string

}


type PracticeSession = {
sessionId: string,
userId: string,
problemId: string,
createdAt: Date,

}


type Problem = {
problemId: string,
description: string,
difficulty: string,
title: string,
tags: string[],
examples: string[],
hints: string[],

}

0 comments on commit 1c4aee7

Please sign in to comment.