refac: add loading button - extract fetch function in authen

This commit is contained in:
Sosokker 2025-02-13 23:16:39 +07:00
parent 6fbabde002
commit ee434d4c5a
6 changed files with 202 additions and 41 deletions

View File

@ -0,0 +1,48 @@
import axios from "axios";
import axiosInstance from "./config";
export interface LoginResponse {
Token: string;
message?: string;
}
export interface RegisterResponse {
token: string;
message?: string;
}
/**
* Registers a new user by sending a POST request to the backend.
*/
export async function registerUser(email: string, password: string): Promise<RegisterResponse> {
try {
const response = await axiosInstance.post("/auth/register", {
Email: email,
Password: password,
});
return response.data;
} catch (error: any) {
if (axios.isAxiosError(error)) {
throw new Error(error.response?.data?.message || "Failed to register.");
}
throw error;
}
}
/**
* Logs in a user by sending a POST request to the backend.
*/
export async function loginUser(email: string, password: string): Promise<LoginResponse> {
try {
const response = await axiosInstance.post("/auth/login", {
Email: email,
Password: password,
});
return response.data;
} catch (error: any) {
if (axios.isAxiosError(error)) {
throw new Error(error.response?.data?.message || "Failed to log in.");
}
throw error;
}
}

28
frontend/api/config.ts Normal file
View File

@ -0,0 +1,28 @@
import axios from "axios";
const axiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8000",
headers: {
"Content-Type": "application/json",
},
});
axiosInstance.interceptors.request.use(
(config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
return Promise.reject(error);
}
);
export default axiosInstance;

View File

@ -18,8 +18,11 @@ import { z } from "zod";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { loginUser } from "@/api/authentication";
export default function SigninPage() {
const [serverError, setServerError] = useState(null);
const [serverError, setServerError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const {
@ -35,32 +38,20 @@ export default function SigninPage() {
});
const onSubmit = async (values: z.infer<typeof signInSchema>) => {
setServerError(null); // reset previous errors
setServerError(null);
setIsLoading(true);
try {
const response = await fetch("http://localhost:8000/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
Email: values.email,
Password: values.password,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || "Failed to log in.");
}
const data = await loginUser(values.email, values.password);
localStorage.setItem("token", data.Token);
localStorage.setItem("user", values.email);
router.push("/setup");
} catch (error) {
} catch (error: any) {
console.error("Error logging in:", error);
setServerError(error.message);
} finally {
setIsLoading(false);
}
};
@ -99,8 +90,8 @@ export default function SigninPage() {
{errors.password && <p className="text-red-600 text-sm">{errors.password.message}</p>}
</div>
<Button type="submit" className="mt-5 rounded-full">
Log in
<Button type="submit" className="mt-5 rounded-full" disabled={isLoading}>
{isLoading ? "Logging in..." : "Log in"}
</Button>
</form>

View File

@ -17,9 +17,11 @@ import { z } from "zod";
import { useRouter } from "next/navigation";
import { registerUser } from "@/api/authentication";
export default function SignupPage() {
const [serverError, setServerError] = useState(null);
const [successMessage, setSuccessMessage] = useState(null);
const [serverError, setServerError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const router = useRouter();
const {
@ -38,28 +40,13 @@ export default function SignupPage() {
const onSubmit = async (values: z.infer<typeof signUpSchema>) => {
setServerError(null);
setSuccessMessage(null);
try {
const response = await fetch("http://localhost:8000/auth/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
Email: values.email,
Password: values.password,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || "Failed to register.");
}
const data = await registerUser(values.email, values.password);
localStorage.setItem("token", data.token);
localStorage.setItem("user", values.email);
// Assume registration returns a token or user data.
console.log("Registration successful:", data);
setSuccessMessage("Registration successful! You can now sign in.");

View File

@ -12,6 +12,7 @@
"@hookform/resolvers": "^4.0.0",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-label": "^2.1.2",
@ -20,6 +21,7 @@
"@radix-ui/react-tooltip": "^1.1.8",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/react-query": "^5.66.0",
"axios": "^1.7.9",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.475.0",

View File

@ -17,6 +17,9 @@ importers:
'@radix-ui/react-checkbox':
specifier: ^1.1.4
version: 1.1.4(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-collapsible':
specifier: ^1.1.3
version: 1.1.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-dialog':
specifier: ^1.1.6
version: 1.1.6(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@ -41,6 +44,9 @@ importers:
'@tanstack/react-query':
specifier: ^5.66.0
version: 5.66.0(react@19.0.0)
axios:
specifier: ^1.7.9
version: 1.7.9
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@ -443,6 +449,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-collapsible@1.1.3':
resolution: {integrity: sha512-jFSerheto1X03MUC0g6R7LedNW9EEGWdg9W1+MlpkMLwGkgkbUXLPBH/KIuWKXUoeYRVY11llqbTBDzuLg7qrw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-collection@1.1.2':
resolution: {integrity: sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==}
peerDependencies:
@ -939,6 +958,9 @@ packages:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
available-typed-arrays@1.0.7:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
@ -947,6 +969,9 @@ packages:
resolution: {integrity: sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==}
engines: {node: '>=4'}
axios@1.7.9:
resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==}
axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'}
@ -1027,6 +1052,10 @@ packages:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
@ -1093,6 +1122,10 @@ packages:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
detect-libc@2.0.3:
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
engines: {node: '>=8'}
@ -1318,6 +1351,15 @@ packages:
flatted@3.3.2:
resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==}
follow-redirects@1.15.9:
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
for-each@0.3.5:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
@ -1326,6 +1368,10 @@ packages:
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
engines: {node: '>=14'}
form-data@4.0.1:
resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==}
engines: {node: '>= 6'}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@ -1649,6 +1695,14 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
@ -1896,6 +1950,9 @@ packages:
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@ -2582,6 +2639,22 @@ snapshots:
'@types/react': 19.0.8
'@types/react-dom': 19.0.3(@types/react@19.0.8)
'@radix-ui/react-collapsible@1.1.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/primitive': 1.1.1
'@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.0.0)
'@radix-ui/react-context': 1.1.1(@types/react@19.0.8)(react@19.0.0)
'@radix-ui/react-id': 1.1.0(@types/react@19.0.8)(react@19.0.0)
'@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.8)(react@19.0.0)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.8)(react@19.0.0)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
optionalDependencies:
'@types/react': 19.0.8
'@types/react-dom': 19.0.3(@types/react@19.0.8)
'@radix-ui/react-collection@1.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.0.0)
@ -3106,12 +3179,22 @@ snapshots:
async-function@1.0.0: {}
asynckit@0.4.0: {}
available-typed-arrays@1.0.7:
dependencies:
possible-typed-array-names: 1.1.0
axe-core@4.10.2: {}
axios@1.7.9:
dependencies:
follow-redirects: 1.15.9
form-data: 4.0.1
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
axobject-query@4.1.0: {}
balanced-match@1.0.2: {}
@ -3201,6 +3284,10 @@ snapshots:
color-string: 1.9.1
optional: true
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
commander@4.1.1: {}
concat-map@0.0.1: {}
@ -3259,6 +3346,8 @@ snapshots:
has-property-descriptors: 1.0.2
object-keys: 1.1.1
delayed-stream@1.0.0: {}
detect-libc@2.0.3:
optional: true
@ -3632,6 +3721,8 @@ snapshots:
flatted@3.3.2: {}
follow-redirects@1.15.9: {}
for-each@0.3.5:
dependencies:
is-callable: 1.2.7
@ -3641,6 +3732,12 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
form-data@4.0.1:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
fsevents@2.3.3:
optional: true
@ -3970,6 +4067,12 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.1
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.11
@ -4212,6 +4315,8 @@ snapshots:
object-assign: 4.1.1
react-is: 16.13.1
proxy-from-env@1.1.0: {}
punycode@2.3.1: {}
queue-microtask@1.2.3: {}