forked from jsnbuchanan/crowd-funder-for-time-pwa
refactor(types): improve type safety and eliminate type assertions
- Replace type assertions with proper type guards in ProfileService - Add isAxiosError type guard and improve error handling - Clean up formatting and improve type safety in deepLinks service - Remove type assertions in AccountViewView Vue component - Improve code formatting and consistency across services
This commit is contained in:
@@ -140,18 +140,20 @@ export class ProfileService {
|
|||||||
logger.error("Unexpected response status when deleting profile:", {
|
logger.error("Unexpected response status when deleting profile:", {
|
||||||
status: response.status,
|
status: response.status,
|
||||||
statusText: response.statusText,
|
statusText: response.statusText,
|
||||||
data: response.data
|
data: response.data,
|
||||||
});
|
});
|
||||||
throw new Error(`Profile not deleted - HTTP ${response.status}: ${response.statusText}`);
|
throw new Error(
|
||||||
|
`Profile not deleted - HTTP ${response.status}: ${response.statusText}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.isApiError(error) && error.response) {
|
if (this.isApiError(error) && error.response) {
|
||||||
const response = error.response as any; // Type assertion for error response
|
const response = error.response;
|
||||||
logger.error("API error deleting profile:", {
|
logger.error("API error deleting profile:", {
|
||||||
status: response.status,
|
status: response.status,
|
||||||
statusText: response.statusText,
|
statusText: response.statusText,
|
||||||
data: response.data,
|
data: response.data,
|
||||||
url: (error as any).config?.url
|
url: this.getErrorUrl(error),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle specific HTTP status codes
|
// Handle specific HTTP status codes
|
||||||
@@ -163,7 +165,11 @@ export class ProfileService {
|
|||||||
return true; // Consider this a success if profile doesn't exist
|
return true; // Consider this a success if profile doesn't exist
|
||||||
} else if (response.status === 400) {
|
} else if (response.status === 400) {
|
||||||
logger.error("Bad request when deleting profile:", response.data);
|
logger.error("Bad request when deleting profile:", response.data);
|
||||||
throw new Error(`Profile deletion failed: ${response.data?.message || 'Bad request'}`);
|
const errorMessage =
|
||||||
|
typeof response.data === "string"
|
||||||
|
? response.data
|
||||||
|
: response.data?.message || "Bad request";
|
||||||
|
throw new Error(`Profile deletion failed: ${errorMessage}`);
|
||||||
} else if (response.status === 401) {
|
} else if (response.status === 401) {
|
||||||
logger.error("Unauthorized to delete profile");
|
logger.error("Unauthorized to delete profile");
|
||||||
throw new Error("You are not authorized to delete this profile");
|
throw new Error("You are not authorized to delete this profile");
|
||||||
@@ -244,11 +250,32 @@ export class ProfileService {
|
|||||||
/**
|
/**
|
||||||
* Type guard for API errors
|
* Type guard for API errors
|
||||||
*/
|
*/
|
||||||
private isApiError(
|
private isApiError(error: unknown): error is {
|
||||||
error: unknown,
|
response?: {
|
||||||
): error is { response?: { status?: number } } {
|
status?: number;
|
||||||
|
statusText?: string;
|
||||||
|
data?: { message?: string } | string;
|
||||||
|
};
|
||||||
|
} {
|
||||||
return typeof error === "object" && error !== null && "response" in error;
|
return typeof error === "object" && error !== null && "response" in error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract URL from AxiosError without type casting
|
||||||
|
*/
|
||||||
|
private getErrorUrl(error: unknown): string | undefined {
|
||||||
|
if (this.isAxiosError(error)) {
|
||||||
|
return error.config?.url;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for AxiosError
|
||||||
|
*/
|
||||||
|
private isAxiosError(error: unknown): error is AxiosError {
|
||||||
|
return error instanceof AxiosError;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -199,8 +199,10 @@ export class DeepLinkHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Continue with parameter validation as before...
|
// Continue with parameter validation as before...
|
||||||
const pathSchema = deepLinkPathSchemas[path as keyof typeof deepLinkPathSchemas];
|
const pathSchema =
|
||||||
const querySchema = deepLinkQuerySchemas[path as keyof typeof deepLinkQuerySchemas];
|
deepLinkPathSchemas[path as keyof typeof deepLinkPathSchemas];
|
||||||
|
const querySchema =
|
||||||
|
deepLinkQuerySchemas[path as keyof typeof deepLinkQuerySchemas];
|
||||||
|
|
||||||
let validatedPathParams: Record<string, string> = {};
|
let validatedPathParams: Record<string, string> = {};
|
||||||
let validatedQueryParams: Record<string, string> = {};
|
let validatedQueryParams: Record<string, string> = {};
|
||||||
@@ -235,7 +237,7 @@ export class DeepLinkHandler {
|
|||||||
await this.router.replace({
|
await this.router.replace({
|
||||||
name: routeName,
|
name: routeName,
|
||||||
params: validatedPathParams,
|
params: validatedPathParams,
|
||||||
query: validatedQueryParams
|
query: validatedQueryParams,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
|||||||
@@ -182,7 +182,9 @@
|
|||||||
@change="onLocationCheckboxChange"
|
@change="onLocationCheckboxChange"
|
||||||
/>
|
/>
|
||||||
<label for="includeUserProfileLocation">Include Location</label>
|
<label for="includeUserProfileLocation">Include Location</label>
|
||||||
<span class="text-xs text-slate-400 ml-2">(Debug: {{ isMapReady ? 'Map Ready' : 'Map Loading' }})</span>
|
<span class="text-xs text-slate-400 ml-2"
|
||||||
|
>(Debug: {{ isMapReady ? "Map Ready" : "Map Loading" }})</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="includeUserProfileLocation" class="mb-4 aspect-video">
|
<div v-if="includeUserProfileLocation" class="mb-4 aspect-video">
|
||||||
<p class="text-sm mb-2 text-slate-500">
|
<p class="text-sm mb-2 text-slate-500">
|
||||||
@@ -922,11 +924,14 @@ export default class AccountViewView extends Vue {
|
|||||||
// Fix Leaflet icon issues in modern bundlers
|
// Fix Leaflet icon issues in modern bundlers
|
||||||
// This prevents the "Cannot read properties of undefined (reading 'Default')" error
|
// This prevents the "Cannot read properties of undefined (reading 'Default')" error
|
||||||
if (L.Icon.Default) {
|
if (L.Icon.Default) {
|
||||||
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
delete (L.Icon.Default.prototype as { _getIconUrl?: unknown })
|
||||||
|
._getIconUrl;
|
||||||
L.Icon.Default.mergeOptions({
|
L.Icon.Default.mergeOptions({
|
||||||
iconRetinaUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon-2x.png',
|
iconRetinaUrl:
|
||||||
iconUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png',
|
"https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon-2x.png",
|
||||||
shadowUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-shadow.png',
|
iconUrl: "https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png",
|
||||||
|
shadowUrl:
|
||||||
|
"https://unpkg.com/leaflet@1.7.1/dist/images/marker-shadow.png",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1543,12 +1548,18 @@ export default class AccountViewView extends Vue {
|
|||||||
try {
|
try {
|
||||||
logger.debug("Map ready event fired, map object:", map);
|
logger.debug("Map ready event fired, map object:", map);
|
||||||
// doing this here instead of on the l-map element avoids a recentering after a drag then zoom at startup
|
// doing this here instead of on the l-map element avoids a recentering after a drag then zoom at startup
|
||||||
const zoom = this.userProfileLatitude && this.userProfileLongitude ? 12 : 2;
|
const zoom =
|
||||||
|
this.userProfileLatitude && this.userProfileLongitude ? 12 : 2;
|
||||||
const lat = this.userProfileLatitude || 0;
|
const lat = this.userProfileLatitude || 0;
|
||||||
const lng = this.userProfileLongitude || 0;
|
const lng = this.userProfileLongitude || 0;
|
||||||
map.setView([lat, lng], zoom);
|
map.setView([lat, lng], zoom);
|
||||||
this.isMapReady = true;
|
this.isMapReady = true;
|
||||||
logger.debug("Map ready state set to true, coordinates:", [lat, lng], "zoom:", zoom);
|
logger.debug(
|
||||||
|
"Map ready state set to true, coordinates:",
|
||||||
|
[lat, lng],
|
||||||
|
"zoom:",
|
||||||
|
zoom,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error in onMapReady:", error);
|
logger.error("Error in onMapReady:", error);
|
||||||
this.isMapReady = true; // Set to true even on error to prevent infinite loading
|
this.isMapReady = true; // Set to true even on error to prevent infinite loading
|
||||||
@@ -1710,7 +1721,10 @@ export default class AccountViewView extends Vue {
|
|||||||
|
|
||||||
onLocationCheckboxChange(): void {
|
onLocationCheckboxChange(): void {
|
||||||
try {
|
try {
|
||||||
logger.debug("Location checkbox changed, new value:", this.includeUserProfileLocation);
|
logger.debug(
|
||||||
|
"Location checkbox changed, new value:",
|
||||||
|
this.includeUserProfileLocation,
|
||||||
|
);
|
||||||
if (!this.includeUserProfileLocation) {
|
if (!this.includeUserProfileLocation) {
|
||||||
// Location checkbox was unchecked, clean up map state
|
// Location checkbox was unchecked, clean up map state
|
||||||
this.isMapReady = false;
|
this.isMapReady = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user