Skip to content

Commit 96e2fad

Browse files
committed
Add login
1 parent fd440fc commit 96e2fad

File tree

11 files changed

+431
-8
lines changed

11 files changed

+431
-8
lines changed

.env.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,8 @@ NEXT_PUBLIC_PLACEHOLDER_SMALL_IMAGE_URL="https://res.cloudinary.com/placeholder-
44
NEXT_PUBLIC_PLACEHOLDER_LARGE_IMAGE_URL="https://via.placeholder.com/600"
55
NEXT_PUBLIC_ALGOLIA_APP_ID = "changeme"
66
NEXT_PUBLIC_ALGOLIA_PUBLIC_API_KEY = "changeme"
7-
NODE_ENV="development"
7+
NODE_ENV="development"
8+
NEXT_PUBLIC_AUTH_TOKEN_SS_KEY="auth-token"
9+
NEXT_PUBLIC_REFRESH_TOKEN_LS_KEY="refresh-token"
10+
NEXT_PUBLIC_SESSION_TOKEN_LS_KEY="session-token"
11+
NEXT_PUBLIC_AUTH_KEY_TIMEOUT="300000"

src/components/Header/Navbar.component.tsx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Link from 'next/link';
2-
2+
import { useEffect, useState } from 'react';
3+
import { hasCredentials } from '../../utils/auth';
34
import Cart from './Cart.component';
45
import AlgoliaSearchBox from '../AlgoliaSearch/AlgoliaSearchBox.component';
56
import MobileSearch from '../AlgoliaSearch/MobileSearch.component';
@@ -9,6 +10,12 @@ import MobileSearch from '../AlgoliaSearch/MobileSearch.component';
910
* Includes mobile menu.
1011
*/
1112
const Navbar = () => {
13+
const [loggedIn, setLoggedIn] = useState(false);
14+
15+
useEffect(() => {
16+
setLoggedIn(hasCredentials());
17+
}, []);
18+
1219
return (
1320
<header className="border-b border-gray-200">
1421
<nav id="header" className="top-0 z-50 w-full bg-white">
@@ -51,6 +58,35 @@ const Navbar = () => {
5158
</Link>
5259
<div className="flex items-center space-x-3">
5360
<AlgoliaSearchBox />
61+
{loggedIn ? (
62+
<Link href="/min-konto">
63+
<span className="text-base uppercase tracking-wider group relative">
64+
<span className="relative inline-block">
65+
<span className="absolute -bottom-1 left-0 w-0 h-px bg-gray-900 group-hover:w-full transition-all duration-500"></span>
66+
Min konto
67+
</span>
68+
</span>
69+
</Link>
70+
) : (
71+
<>
72+
<Link href="/logg-inn">
73+
<span className="text-base uppercase tracking-wider group relative">
74+
<span className="relative inline-block">
75+
<span className="absolute -bottom-1 left-0 w-0 h-px bg-gray-900 group-hover:w-full transition-all duration-500"></span>
76+
Logg inn
77+
</span>
78+
</span>
79+
</Link>
80+
<Link href="/registrer">
81+
<span className="text-base uppercase tracking-wider group relative">
82+
<span className="relative inline-block">
83+
<span className="absolute -bottom-1 left-0 w-0 h-px bg-gray-900 group-hover:w-full transition-all duration-500"></span>
84+
Registrer
85+
</span>
86+
</span>
87+
</Link>
88+
</>
89+
)}
5490
<Cart />
5591
</div>
5692
</div>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { useQuery } from '@apollo/client';
2+
import { GET_CUSTOMER_ORDERS } from '../../utils/gql/GQL_QUERIES';
3+
import LoadingSpinner from '../LoadingSpinner/LoadingSpinner.component';
4+
5+
interface Order {
6+
id: string;
7+
orderNumber: number;
8+
status: string;
9+
total: string;
10+
date: string;
11+
}
12+
13+
/**
14+
* Customer account component that displays user's orders
15+
* @function CustomerAccount
16+
* @returns {JSX.Element} - Rendered component with order history
17+
*/
18+
const CustomerAccount = () => {
19+
const { loading, error, data } = useQuery(GET_CUSTOMER_ORDERS);
20+
21+
if (loading) return <LoadingSpinner />;
22+
if (error) return <p>Error: {error.message}</p>;
23+
24+
const orders = data?.customer?.orders?.nodes;
25+
26+
return (
27+
<div>
28+
<h1 className="text-2xl font-bold mb-4">Mine ordre</h1>
29+
{orders && orders.length > 0 ? (
30+
<div className="overflow-x-auto">
31+
<table className="min-w-full bg-white">
32+
<thead>
33+
<tr>
34+
<th className="py-2 px-4 border-b">Ordrenummer</th>
35+
<th className="py-2 px-4 border-b">Dato</th>
36+
<th className="py-2 px-4 border-b">Status</th>
37+
<th className="py-2 px-4 border-b">Total</th>
38+
</tr>
39+
</thead>
40+
<tbody>
41+
{orders.map((order: Order) => (
42+
<tr key={order.id}>
43+
<td className="py-2 px-4 border-b">{order.orderNumber}</td>
44+
<td className="py-2 px-4 border-b">
45+
{new Date(order.date).toLocaleDateString()}
46+
</td>
47+
<td className="py-2 px-4 border-b">{order.status}</td>
48+
<td className="py-2 px-4 border-b">{order.total}</td>
49+
</tr>
50+
))}
51+
</tbody>
52+
</table>
53+
</div>
54+
) : (
55+
<p>Du har ingen ordre.</p>
56+
)}
57+
</div>
58+
);
59+
};
60+
61+
export default CustomerAccount;
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { useState } from 'react';
2+
import { useForm, FormProvider } from 'react-hook-form';
3+
import { login } from '../../utils/auth';
4+
import { InputField } from '../Input/InputField.component';
5+
import LoadingSpinner from '../LoadingSpinner/LoadingSpinner.component';
6+
import Button from '../UI/Button.component';
7+
import { useRouter } from 'next/router';
8+
9+
interface ILoginData {
10+
username: string;
11+
password: string;
12+
}
13+
14+
/**
15+
* User login component that handles user authentication
16+
* @function UserLogin
17+
* @returns {JSX.Element} - Rendered component with login form
18+
*/
19+
const UserLogin = () => {
20+
const methods = useForm<ILoginData>();
21+
const [loading, setLoading] = useState(false);
22+
const [error, setError] = useState<string | null>(null);
23+
const router = useRouter();
24+
25+
const onSubmit = async (data: ILoginData) => {
26+
setLoading(true);
27+
setError(null);
28+
try {
29+
const customer = await login(data.username, data.password);
30+
if (customer) {
31+
router.push('/min-konto');
32+
} else {
33+
throw new Error('Failed to login');
34+
}
35+
} catch (error: unknown) {
36+
if (error instanceof Error) {
37+
setError(error.message);
38+
} else {
39+
setError('An unknown error occurred.');
40+
}
41+
console.error('Login error:', error);
42+
} finally {
43+
setLoading(false);
44+
}
45+
};
46+
47+
return (
48+
<section className="text-gray-700 container p-4 py-2 mx-auto mb-[8rem] md:mb-0">
49+
<FormProvider {...methods}>
50+
<form onSubmit={methods.handleSubmit(onSubmit)}>
51+
<div className="mx-auto lg:w-1/2 flex flex-wrap">
52+
<InputField
53+
inputName="username"
54+
inputLabel="Brukernavn eller e-post"
55+
type="text"
56+
customValidation={{ required: true }}
57+
/>
58+
<InputField
59+
inputName="password"
60+
inputLabel="Passord"
61+
type="password"
62+
customValidation={{ required: true }}
63+
/>
64+
65+
{error && (
66+
<div className="w-full p-2 text-red-600 text-sm text-center">
67+
{error}
68+
</div>
69+
)}
70+
71+
<div className="w-full p-2">
72+
<div className="mt-4 flex justify-center">
73+
<Button variant="primary" buttonDisabled={loading}>
74+
{loading ? <LoadingSpinner /> : 'Logg inn'}
75+
</Button>
76+
</div>
77+
</div>
78+
</div>
79+
</form>
80+
</FormProvider>
81+
</section>
82+
);
83+
};
84+
85+
export default UserLogin;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { useRouter } from 'next/router';
2+
import { useEffect, ComponentType } from 'react';
3+
import { hasCredentials } from '../../utils/auth';
4+
5+
const withAuth = <P extends object>(WrappedComponent: ComponentType<P>) => {
6+
const Wrapper = (props: P) => {
7+
const router = useRouter();
8+
9+
useEffect(() => {
10+
if (!hasCredentials()) {
11+
router.push('/logg-inn');
12+
}
13+
}, [router]);
14+
15+
if (!hasCredentials()) {
16+
return null; // or a loading spinner
17+
}
18+
19+
return <WrappedComponent {...props} />;
20+
};
21+
22+
return Wrapper;
23+
};
24+
25+
export default withAuth;

src/pages/logg-inn.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Layout from '@/components/Layout/Layout.component';
2+
import UserLogin from '@/components/User/UserLogin.component';
3+
import type { NextPage } from 'next';
4+
5+
const LoginPage: NextPage = () => {
6+
return (
7+
<Layout title="Logg inn">
8+
<div className="container mx-auto px-4 py-8">
9+
<UserLogin />
10+
</div>
11+
</Layout>
12+
);
13+
};
14+
15+
export default LoginPage;

src/pages/min-konto.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Layout from '@/components/Layout/Layout.component';
2+
import CustomerAccount from '@/components/User/CustomerAccount.component';
3+
import type { NextPage } from 'next';
4+
import withAuth from '@/components/User/withAuth.component';
5+
6+
const CustomerAccountPage: NextPage = () => {
7+
return (
8+
<Layout title="Min konto">
9+
<div className="container mx-auto px-4 py-8">
10+
<CustomerAccount />
11+
</div>
12+
</Layout>
13+
);
14+
};
15+
16+
export default withAuth(CustomerAccountPage);

src/utils/apollo/ApolloClient.js

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
/*eslint complexity: ["error", 6]*/
22

3+
/* eslint-disable unicorn/no-thenable */
34
import {
45
ApolloClient,
56
InMemoryCache,
67
createHttpLink,
78
ApolloLink,
89
} from '@apollo/client';
10+
import { getAuthToken } from '../auth';
911

1012
const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
1113

1214
/**
1315
* Middleware operation
1416
* If we have a session token in localStorage, add it to the GraphQL request as a Session header.
1517
*/
16-
export const middleware = new ApolloLink((operation, forward) => {
18+
export const middleware = new ApolloLink(async (operation, forward) => {
1719
/**
1820
* If session data exist in local storage, set value as session header.
1921
* Here we also delete the session if it is older than 7 days
@@ -22,6 +24,8 @@ export const middleware = new ApolloLink((operation, forward) => {
2224
? JSON.parse(localStorage.getItem('woo-session'))
2325
: null;
2426

27+
const headers = {};
28+
2529
if (sessionData && sessionData.token && sessionData.createdTime) {
2630
const { token, createdTime } = sessionData;
2731

@@ -32,14 +36,21 @@ export const middleware = new ApolloLink((operation, forward) => {
3236
localStorage.setItem('woocommerce-cart', JSON.stringify({}));
3337
} else {
3438
// If it's not, use the token
35-
operation.setContext(() => ({
36-
headers: {
37-
'woocommerce-session': `Session ${token}`,
38-
},
39-
}));
39+
headers['woocommerce-session'] = `Session ${token}`;
4040
}
4141
}
4242

43+
if (process.browser) {
44+
const authToken = await getAuthToken();
45+
if (authToken) {
46+
headers.Authorization = `Bearer ${authToken}`;
47+
}
48+
}
49+
50+
operation.setContext({
51+
headers,
52+
});
53+
4354
return forward(operation);
4455
});
4556

0 commit comments

Comments
 (0)