Skip to content

Commit 628913f

Browse files
authored
Merge branch 'main' into add_tiptap_doc_loading_indicator
2 parents e0af9ec + a71886d commit 628913f

File tree

4 files changed

+145
-19
lines changed

4 files changed

+145
-19
lines changed

src/app/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Link from "next/link";
33

44
import { cn } from "@/components/lib/utils";
55
import { buttonVariants } from "@/components/ui/button";
6-
import { UserAuthForm } from "@/components/user-auth-form";
6+
import { UserAuthForm } from "@/components/ui/login/user-auth-form";
77
import { siteConfig } from "@/site.config";
88
import { Icons } from "@/components/ui/icons";
99

src/components/user-auth-form.tsx renamed to src/components/ui/login/user-auth-form.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as React from "react";
44

55
import { cn } from "@/components/lib/utils";
66
import { useUserSessionMutation } from "@/lib/api/user-sessions";
7+
import { EntityApiError } from "@/lib/api/entity-api";
78
import { useAuthStore } from "@/lib/providers/auth-store-provider";
89
import { useRouter } from "next/navigation";
910
import { Icons } from "@/components/ui/icons";
@@ -49,8 +50,25 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
4950
})
5051
.catch((err) => {
5152
setIsLoading(false);
52-
console.error("Login failed, err: " + err);
53-
setError(err.message);
53+
console.error("Login failed:", err);
54+
55+
// Enhanced error handling with EntityApiError
56+
if (err instanceof EntityApiError) {
57+
// Handle specific HTTP status codes for better UX
58+
if (err.status === 401) {
59+
setError("Invalid email or password. Please try again.");
60+
} else if (err.status === 429) {
61+
setError("Too many login attempts. Please wait before trying again.");
62+
} else if (err.isServerError()) {
63+
setError("Server error occurred. Please try again later.");
64+
} else if (err.isNetworkError()) {
65+
setError("Network error. Please check your connection and try again.");
66+
} else {
67+
setError(err.message);
68+
}
69+
} else {
70+
setError(err.message || "An unexpected error occurred");
71+
}
5472
});
5573
}
5674

src/lib/api/entity-api.ts

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { siteConfig } from "@/site.config";
2-
import { Id } from "@/types/general";
2+
import { Id, EntityApiError } from "@/types/general";
33
import axios from "axios";
44
import { useState } from "react";
55
import useSWR, { KeyedMutator, SWRConfiguration, useSWRConfig } from "swr";
66

7+
// Re-export EntityApiError for easy access
8+
export { EntityApiError } from "@/types/general";
9+
710
export namespace EntityApi {
811
interface ApiResponse<T> {
912
status_code: number;
@@ -77,14 +80,15 @@ export namespace EntityApi {
7780
* @param data Optional payload data required for POST/PUT operations
7881
* @param config Optional post/put/delete configuration object
7982
* @returns Promise resolving to response data of type R
80-
* @throws Error for invalid methods or missing required payload data
83+
* @throws EntityApiError for axios errors, Error for invalid methods or missing required payload data
8184
*
8285
* @remarks This function:
8386
* - Enforces RESTful conventions for mutation operations
8487
* - Handles payload data type validation through generics
8588
* - Applies consistent request configuration (credentials, timeout, headers)
8689
* - Extracts and returns only the data portion from API responses
8790
* - Throws explicit errors for invalid method/data combinations
91+
* - Wraps axios errors in EntityApiError for enhanced error handling
8892
*
8993
* @usage
9094
* - POST/PUT: Requires data payload matching type T
@@ -105,18 +109,28 @@ export namespace EntityApi {
105109
...config,
106110
};
107111

108-
let response;
109-
if (method === "delete") {
110-
response = await axios.delete<ApiResponse<R>>(url, combinedConfig);
111-
} else if (method === "put" && data) {
112-
response = await axios.put<ApiResponse<R>>(url, data, combinedConfig);
113-
} else if (data) {
114-
response = await axios.post<ApiResponse<R>>(url, data, combinedConfig);
115-
} else {
116-
throw new Error("Invalid method or missing data");
117-
}
112+
try {
113+
let response;
114+
if (method === "delete") {
115+
response = await axios.delete<ApiResponse<R>>(url, combinedConfig);
116+
} else if (method === "put" && data) {
117+
response = await axios.put<ApiResponse<R>>(url, data, combinedConfig);
118+
} else if (data) {
119+
response = await axios.post<ApiResponse<R>>(url, data, combinedConfig);
120+
} else {
121+
throw new Error("Invalid method or missing data");
122+
}
118123

119-
return response.data.data;
124+
return response.data.data;
125+
} catch (error) {
126+
// Wrap axios errors in EntityApiError for enhanced error handling
127+
if (axios.isAxiosError(error)) {
128+
throw new EntityApiError(method, url, error);
129+
}
130+
131+
// Re-throw non-axios errors as-is
132+
throw error;
133+
}
120134
};
121135

122136
/**
@@ -414,9 +428,9 @@ export namespace EntityApi {
414428
mutate((key) => typeof key === "string" && key.includes(baseUrl));
415429
return result;
416430
} catch (err) {
417-
setError(
418-
err instanceof Error ? err : new Error("Unknown error occurred")
419-
);
431+
// Handle both EntityApiError and regular Error types
432+
const error = err instanceof Error ? err : new Error("Unknown error occurred");
433+
setError(error);
420434
throw err;
421435
} finally {
422436
setIsLoading(false);

src/types/general.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,102 @@
11
import { DateTime } from "ts-luxon";
2+
import { AxiosError, AxiosResponse } from "axios";
3+
import axios from "axios";
24

35
// A type alias for each entity's Id field
46
export type Id = string;
57

8+
/**
9+
* Enhanced error type for Entity API operations that preserves axios error information
10+
* while providing a more user-friendly interface for error handling.
11+
*/
12+
export class EntityApiError extends Error {
13+
public readonly method: string;
14+
public readonly url: string;
15+
public readonly status?: number;
16+
public readonly statusText?: string;
17+
public readonly response?: AxiosResponse;
18+
public readonly request?: any;
19+
public readonly data?: any;
20+
public readonly originalError: AxiosError | Error;
21+
22+
constructor(
23+
method: string,
24+
url: string,
25+
originalError: AxiosError | Error,
26+
message?: string
27+
) {
28+
// Create a descriptive error message
29+
const errorMessage =
30+
message || EntityApiError.createErrorMessage(method, url, originalError);
31+
super(errorMessage);
32+
33+
this.name = "EntityApiError";
34+
this.method = method.toUpperCase();
35+
this.url = url;
36+
this.originalError = originalError;
37+
38+
// Extract axios-specific properties if available
39+
if (
40+
originalError &&
41+
"isAxiosError" in originalError &&
42+
originalError.isAxiosError
43+
) {
44+
const axiosError = originalError as AxiosError;
45+
this.status = axiosError.response?.status;
46+
this.statusText = axiosError.response?.statusText;
47+
this.response = axiosError.response;
48+
this.request = axiosError.request;
49+
this.data = axiosError.response?.data;
50+
}
51+
}
52+
53+
/**
54+
* Creates a user-friendly error message from the original error
55+
*/
56+
private static createErrorMessage(
57+
method: string,
58+
url: string,
59+
error: AxiosError | Error
60+
): string {
61+
if (axios.isAxiosError(error)) {
62+
const status = error.response?.status;
63+
const statusText = error.response?.statusText;
64+
const serverMessage = error.response?.data?.message;
65+
66+
if (serverMessage) {
67+
return `${method.toUpperCase()} ${url} failed: ${serverMessage}`;
68+
} else if (status && statusText) {
69+
return `${method.toUpperCase()} ${url} failed: ${status} ${statusText}`;
70+
} else {
71+
return `${method.toUpperCase()} ${url} failed: ${error.message}`;
72+
}
73+
} else {
74+
return `${method.toUpperCase()} ${url} failed: ${error.message}`;
75+
}
76+
}
77+
78+
/**
79+
* Returns true if this error represents a client error (4xx status codes)
80+
*/
81+
public isClientError(): boolean {
82+
return this.status !== undefined && this.status >= 400 && this.status < 500;
83+
}
84+
85+
/**
86+
* Returns true if this error represents a server error (5xx status codes)
87+
*/
88+
public isServerError(): boolean {
89+
return this.status !== undefined && this.status >= 500;
90+
}
91+
92+
/**
93+
* Returns true if this error represents a network error (no response received)
94+
*/
95+
public isNetworkError(): boolean {
96+
return this.status === undefined && "isAxiosError" in this.originalError;
97+
}
98+
}
99+
6100
// A sorting type that can be used by any of our custom types when stored
7101
// as arrays
8102
export enum SortOrder {

0 commit comments

Comments
 (0)