Skip to content

Commit 1cce8be

Browse files
committed
add authentication feature
1 parent 7586bc2 commit 1cce8be

15 files changed

+1156
-26
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
# misc
1515
.DS_Store
16+
.env
1617
.env.local
1718
.env.development.local
1819
.env.test.local

package-lock.json

Lines changed: 767 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
"@types/node": "^16.18.50",
1515
"@types/react": "^18.2.21",
1616
"@types/react-dom": "^18.2.7",
17+
"firebase": "^10.4.0",
1718
"react": "^18.2.0",
1819
"react-dom": "^18.2.0",
20+
"react-router-dom": "^6.16.0",
1921
"react-scripts": "5.0.1",
2022
"typescript": "^4.9.5",
2123
"web-vitals": "^2.1.4"

src/App.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import React from "react";
2-
import { Container } from "@mui/material";
1+
import { Box, Container, CssBaseline } from "@mui/material";
32
import Navbar from "./components/Navbar";
43

54
export default function App() {
65
return (
7-
<>
6+
<Box>
7+
<CssBaseline />
88
<Navbar />
99
<Container>hello world!</Container>
10-
</>
10+
</Box>
1111
);
1212
}

src/auth/AuthGuard.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { ReactNode } from "react";
2+
import { Navigate } from "react-router-dom";
3+
import { useAuth } from "./auth.context";
4+
5+
interface AuthGuardProps {
6+
children: ReactNode;
7+
}
8+
9+
export default function AuthGuard({ children }: AuthGuardProps) {
10+
const { user } = useAuth();
11+
if (!user) {
12+
return <Navigate to="/login" />;
13+
}
14+
return <>{children}</>;
15+
}

src/auth/auth.context.tsx

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import {
2+
createContext,
3+
ReactNode,
4+
useCallback,
5+
useContext,
6+
useMemo,
7+
useState,
8+
} from "react";
9+
import {
10+
createUserWithEmailAndPassword,
11+
signInWithEmailAndPassword,
12+
signOut,
13+
User,
14+
} from "firebase/auth";
15+
import { auth } from "../firebase/firebase";
16+
import { useLocalStorage } from "./useLocalStorage";
17+
import { useNavigate } from "react-router-dom";
18+
19+
interface AuthContextData {
20+
user: User | undefined;
21+
error: string;
22+
signUp: (email: string, password: string) => void;
23+
login: (email: string, password: string) => void;
24+
logout: () => void;
25+
}
26+
27+
interface AuthContextProviderProps {
28+
children: ReactNode;
29+
}
30+
31+
const AuthContext = createContext<AuthContextData>({
32+
user: undefined,
33+
error: "",
34+
signUp: (email: string, password: string) => undefined,
35+
login: (email: string, password: string) => undefined,
36+
logout: () => undefined,
37+
});
38+
39+
export function AuthContextProvider({ children }: AuthContextProviderProps) {
40+
const navigate = useNavigate();
41+
const [user, setUser] = useLocalStorage("user", undefined);
42+
const [error, setError] = useState<string>("");
43+
44+
const signUp = useCallback(
45+
(email: string, password: string) => {
46+
createUserWithEmailAndPassword(auth, email, password)
47+
.then((u) => setUser(u.user))
48+
.catch((e) => setError(e.message));
49+
},
50+
[setUser]
51+
);
52+
53+
const login = useCallback(
54+
(email: string, password: string) => {
55+
signInWithEmailAndPassword(auth, email, password)
56+
.then((u) => {
57+
setUser(u.user);
58+
navigate("/");
59+
})
60+
.catch((e) => setError(e.message));
61+
},
62+
[setUser, navigate]
63+
);
64+
65+
const logout = useCallback(() => {
66+
signOut(auth)
67+
.then(() => setUser(undefined))
68+
.catch((e) => setError(e.message));
69+
}, [setUser]);
70+
71+
const authContextProviderValue = useMemo(
72+
() => ({ user, error, signUp, login, logout }),
73+
[user, error, signUp, login, logout]
74+
);
75+
76+
return (
77+
<AuthContext.Provider value={authContextProviderValue}>
78+
{children}
79+
</AuthContext.Provider>
80+
);
81+
}
82+
83+
export function useAuth() {
84+
return useContext(AuthContext);
85+
}

src/auth/useLocalStorage.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useState } from "react";
2+
import { User } from "firebase/auth";
3+
4+
export function useLocalStorage(
5+
keyName: string,
6+
defaultValue: User | undefined
7+
) {
8+
const [storedValue, setStoredValue] = useState(() => {
9+
try {
10+
const value = window.localStorage.getItem(keyName);
11+
if (value) {
12+
return JSON.parse(value);
13+
} else {
14+
window.localStorage.setItem(keyName, JSON.stringify(defaultValue));
15+
return defaultValue;
16+
}
17+
} catch (err) {
18+
return defaultValue;
19+
}
20+
});
21+
22+
const setValue = (newValue: User | undefined) => {
23+
try {
24+
window.localStorage.setItem(keyName, JSON.stringify(newValue));
25+
} catch (err) {}
26+
setStoredValue(newValue);
27+
};
28+
29+
return [storedValue, setValue];
30+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ReactNode } from "react";
2+
import { Container } from "@mui/material";
3+
4+
interface CenteredContainerProps {
5+
children: ReactNode;
6+
}
7+
8+
export default function CenteredContainer({
9+
children,
10+
}: CenteredContainerProps) {
11+
return (
12+
<Container
13+
sx={{
14+
display: "flex",
15+
alignItems: "center",
16+
justifyContent: "center",
17+
height: "90%",
18+
flexDirection: "column",
19+
}}
20+
>
21+
{children}
22+
</Container>
23+
);
24+
}

src/components/Navbar.tsx

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
11
import * as React from "react";
2-
import AppBar from "@mui/material/AppBar";
3-
import Box from "@mui/material/Box";
4-
import Toolbar from "@mui/material/Toolbar";
5-
import IconButton from "@mui/material/IconButton";
6-
import Typography from "@mui/material/Typography";
7-
import Menu from "@mui/material/Menu";
2+
import {
3+
AppBar,
4+
Box,
5+
Button,
6+
Container,
7+
IconButton,
8+
Menu,
9+
MenuItem,
10+
Toolbar,
11+
Typography,
12+
} from "@mui/material";
813
import MenuIcon from "@mui/icons-material/Menu";
9-
import Container from "@mui/material/Container";
10-
import Button from "@mui/material/Button";
11-
import MenuItem from "@mui/material/MenuItem";
1214
import AdbIcon from "@mui/icons-material/Adb";
1315

1416
const pages = ["Products", "Pricing", "Blog"];
15-
const authPages = ["Login", "Sign Up"];
17+
const authPages = [
18+
{
19+
name: "Login",
20+
link: "/login",
21+
},
22+
{
23+
name: "Sign Up",
24+
link: "/signup",
25+
},
26+
];
1627
const settings = ["Profile", "Account", "Dashboard", "Logout"];
1728

1829
function Navbar() {
@@ -95,6 +106,11 @@ function Navbar() {
95106
<Typography textAlign="center">{page}</Typography>
96107
</MenuItem>
97108
))}
109+
{authPages.map((page) => (
110+
<MenuItem key={page.name} href={page.link}>
111+
<Typography textAlign="center">{page.name}</Typography>
112+
</MenuItem>
113+
))}
98114
</Menu>
99115
</Box>
100116
<AdbIcon sx={{ display: { xs: "flex", md: "none" }, mr: 1 }} />
@@ -132,11 +148,12 @@ function Navbar() {
132148
<Box sx={{ flexGrow: 1, display: { xs: "none", md: "flex" } }}>
133149
{authPages.map((page) => (
134150
<Button
135-
key={page}
136-
onClick={handleCloseNavMenu}
151+
key={page.name}
152+
// onClick={handleCloseNavMenu}
153+
href={page.link}
137154
sx={{ my: 2, color: "white", display: "block" }}
138155
>
139-
{page}
156+
{page.name}
140157
</Button>
141158
))}
142159
</Box>

src/components/PasswordField.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useState } from "react";
2+
import {
3+
FormControl,
4+
IconButton,
5+
InputAdornment,
6+
InputLabel,
7+
OutlinedInput,
8+
} from "@mui/material";
9+
import { Visibility, VisibilityOff } from "@mui/icons-material";
10+
11+
interface PasswordFieldProps {
12+
password: string;
13+
setPassword: (password: string) => void;
14+
}
15+
16+
export default function PasswordField({
17+
password,
18+
setPassword,
19+
}: PasswordFieldProps) {
20+
const [showPassword, setShowPassword] = useState(false);
21+
22+
const handleClickShowPassword = () => setShowPassword(!showPassword);
23+
24+
return (
25+
<FormControl sx={{ m: 1, width: "25ch" }} variant="outlined">
26+
<InputLabel htmlFor="outlined-adornment-password">password</InputLabel>
27+
<OutlinedInput
28+
id="outlined-adornment-password"
29+
type={showPassword ? "text" : "password"}
30+
endAdornment={
31+
<InputAdornment position="end">
32+
<IconButton
33+
aria-label="toggle password visibility"
34+
onClick={handleClickShowPassword}
35+
edge="end"
36+
>
37+
{showPassword ? <VisibilityOff /> : <Visibility />}
38+
</IconButton>
39+
</InputAdornment>
40+
}
41+
label="password"
42+
value={password}
43+
onChange={(e) => setPassword(e.currentTarget.value)}
44+
/>
45+
</FormControl>
46+
);
47+
}

0 commit comments

Comments
 (0)