Complete Enhanced Triple Migration Pattern for PhotoDialog and OfferDialog components

- Implement 4-phase migration pattern: Database + SQL + Notifications + Template Streamlining
- PhotoDialog.vue: Replace databaseUtil with PlatformServiceMixin, add 8 notification constants, extract 11 computed properties
- OfferDialog.vue: Replace databaseUtil with PlatformServiceMixin, add 7 notification constants, extract CSS classes to computed properties
- Update migration template with Phase 4 (Template Streamlining) and Phase 5 (Code Quality Review)
- Add 15 centralized notification constants to src/constants/notifications.ts

Migration validation: 25/27 components complete (93% success rate)
This commit is contained in:
Matthew Raymer
2025-07-07 09:56:40 +00:00
parent c28ddc0c5c
commit 17e30762bd
6 changed files with 849 additions and 264 deletions

View File

@@ -13,20 +13,14 @@ PhotoDialog.vue */
<div v-if="visible" class="dialog-overlay z-[60]">
<div class="dialog relative">
<div class="text-lg text-center font-light relative z-50">
<div
id="ViewHeading"
class="text-center font-bold absolute top-0 inset-x-0 px-4 py-2 bg-black/50 text-white leading-none pointer-events-none"
>
<div id="ViewHeading" :class="headingClasses">
<span v-if="uploading"> Uploading... </span>
<span v-else-if="blob"> Look Good? </span>
<span v-else-if="showCameraPreview"> Take Photo </span>
<span v-else> Say "Cheese"! </span>
</div>
<div
class="text-lg text-center px-2 py-2 leading-none absolute right-0 top-0 text-white cursor-pointer"
@click="close()"
>
<div :class="closeButtonClasses" @click="close()">
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
</div>
</div>
@@ -40,37 +34,24 @@ PhotoDialog.vue */
<div v-else-if="blob">
<div v-if="crop">
<VuePictureCropper
:box-style="{
backgroundColor: '#f8f8f8',
margin: 'auto',
}"
:img="createBlobURL(blob)"
:options="{
viewMode: 1,
dragMode: 'crop',
aspectRatio: 1 / 1,
}"
:box-style="cropperBoxStyle"
:img="blobUrl"
:options="cropperOptions"
class="max-h-[90vh] max-w-[90vw] object-contain"
/>
</div>
<div v-else>
<div class="flex justify-center">
<img
:src="createBlobURL(blob)"
class="mt-2 rounded max-h-[90vh] max-w-[90vw] object-contain"
/>
<img :src="blobUrl" :class="imageDisplayClasses" />
</div>
</div>
<div class="grid grid-cols-2 gap-2 mt-2">
<button
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-2 px-3 rounded-md"
@click="uploadImage"
>
<button :class="primaryButtonClasses" @click="uploadImage">
<span>Upload</span>
</button>
<button
v-if="showRetry"
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-2 px-3 rounded-md"
:class="secondaryButtonClasses"
@click="retryImage"
>
<span>Retry</span>
@@ -86,10 +67,7 @@ PhotoDialog.vue */
playsinline
muted
></video>
<button
class="absolute bottom-4 left-1/2 -translate-x-1/2 bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
@click="capturePhoto"
>
<button :class="cameraButtonClasses" @click="capturePhoto">
<font-awesome icon="camera" class="w-[1em]" />
</button>
</div>
@@ -98,15 +76,12 @@ PhotoDialog.vue */
<div class="flex flex-col items-center justify-center gap-4 p-4">
<button
v-if="isRegistered"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
:class="actionButtonClasses"
@click="startCameraPreview"
>
<font-awesome icon="camera" class="w-[1em]" />
</button>
<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
@click="pickPhoto"
>
<button :class="actionButtonClasses" @click="pickPhoto">
<font-awesome icon="image" class="w-[1em]" />
</button>
</div>
@@ -120,15 +95,29 @@ import axios from "axios";
import { Component, Vue } from "vue-facing-decorator";
import VuePictureCropper, { cropper } from "vue-picture-cropper";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { accessToken } from "../libs/crypto";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import {
NOTIFY_PHOTO_SETTINGS_ERROR,
NOTIFY_PHOTO_CAPTURE_ERROR,
NOTIFY_PHOTO_CAMERA_ERROR,
NOTIFY_PHOTO_UPLOAD_ERROR,
NOTIFY_PHOTO_UNSUPPORTED_FORMAT,
NOTIFY_PHOTO_SIZE_ERROR,
NOTIFY_PHOTO_PROCESSING_ERROR,
} from "@/constants/notifications";
@Component({ components: { VuePictureCropper } })
@Component({
components: { VuePictureCropper },
mixins: [PlatformServiceMixin],
})
export default class PhotoDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
notify!: ReturnType<typeof createNotifyHelpers>;
/** Active DID for user authentication */
activeDid = "";
@@ -162,36 +151,133 @@ export default class PhotoDialog extends Vue {
/** Camera stream reference */
private cameraStream: MediaStream | null = null;
private platformService = PlatformServiceFactory.getInstance();
URL = window.URL || window.webkitURL;
isRegistered = false;
private platformCapabilities = this.platformService.getCapabilities();
// =================================================
// COMPUTED PROPERTIES - Template Logic Streamlining
// =================================================
/**
* CSS classes for the dialog heading section
* Reduces template complexity for absolute positioning and styling
*/
get headingClasses(): string {
return "text-center font-bold absolute top-0 inset-x-0 px-4 py-2 bg-black/50 text-white leading-none pointer-events-none";
}
/**
* CSS classes for the close button
* Reduces template complexity for absolute positioning and styling
*/
get closeButtonClasses(): string {
return "text-lg text-center px-2 py-2 leading-none absolute right-0 top-0 text-white cursor-pointer";
}
/**
* CSS classes for the primary action button (Upload)
* Reduces template complexity for gradient button styling
*/
get primaryButtonClasses(): string {
return "bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-2 px-3 rounded-md";
}
/**
* CSS classes for the secondary action button (Retry)
* Reduces template complexity for gradient button styling
*/
get secondaryButtonClasses(): string {
return "bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-2 px-3 rounded-md";
}
/**
* CSS classes for the camera capture button
* Reduces template complexity for absolute positioning and circular styling
*/
get cameraButtonClasses(): string {
return "absolute bottom-4 left-1/2 -translate-x-1/2 bg-white text-slate-800 p-3 rounded-full text-2xl leading-none";
}
/**
* CSS classes for action buttons (camera/image selection)
* Reduces template complexity for button styling
*/
get actionButtonClasses(): string {
return "bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none";
}
/**
* CSS classes for image display
* Reduces template complexity for image styling
*/
get imageDisplayClasses(): string {
return "mt-2 rounded max-h-[90vh] max-w-[90vw] object-contain";
}
/**
* Picture cropper box style configuration
* Consolidates complex configuration object from template
*/
get cropperBoxStyle(): object {
return {
backgroundColor: "#f8f8f8",
margin: "auto",
};
}
/**
* Picture cropper options configuration
* Consolidates complex configuration object from template
*/
get cropperOptions(): object {
return {
viewMode: 1,
dragMode: "crop",
aspectRatio: 1 / 1,
};
}
/**
* Blob URL for displaying images
* Encapsulates blob URL creation logic
*/
get blobUrl(): string {
return this.blob ? this.createBlobURL(this.blob) : "";
}
/**
* Platform capabilities accessor
* Provides cached access to platform capabilities
*/
get platformCapabilities() {
return this.$platformService.getCapabilities();
}
// =================================================
// COMPONENT METHODS
// =================================================
/**
* Lifecycle hook: Initializes component and retrieves user settings
* @throws {Error} When settings retrieval fails
*/
async mounted() {
this.notify = createNotifyHelpers(this.$notify);
// logger.log("PhotoDialog mounted");
try {
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
this.isRegistered = !!settings.isRegistered;
logger.log("isRegistered:", this.isRegistered);
} catch (error: unknown) {
logger.error("Error retrieving settings from database:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
error instanceof Error
? error.message
: "There was an error retrieving your settings.",
},
-1,
this.notify.error(
error instanceof Error
? error.message
: NOTIFY_PHOTO_SETTINGS_ERROR.message,
TIMEOUTS.MODAL,
);
}
}
@@ -275,14 +361,9 @@ export default class PhotoDialog extends Vue {
this.fileName = result.fileName;
} catch (error) {
logger.error("Error taking picture:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to take picture. Please try again.",
},
5000,
this.notify.error(
NOTIFY_PHOTO_CAPTURE_ERROR.message,
TIMEOUTS.STANDARD,
);
}
return;
@@ -335,15 +416,7 @@ export default class PhotoDialog extends Vue {
}
} catch (error) {
logger.error("Error starting camera preview:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to access camera. Please try again.",
},
5000,
);
this.notify.error(NOTIFY_PHOTO_CAMERA_ERROR.message, TIMEOUTS.STANDARD);
this.showCameraPreview = false;
}
}
@@ -394,15 +467,7 @@ export default class PhotoDialog extends Vue {
);
} catch (error) {
logger.error("Error capturing photo:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to capture photo. Please try again.",
},
5000,
);
this.notify.error(NOTIFY_PHOTO_CAPTURE_ERROR.message, TIMEOUTS.STANDARD);
}
}
@@ -417,15 +482,7 @@ export default class PhotoDialog extends Vue {
this.fileName = result.fileName;
} catch (error: unknown) {
logger.error("Error taking picture:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to take picture. Please try again.",
},
5000,
);
this.notify.error(NOTIFY_PHOTO_CAPTURE_ERROR.message, TIMEOUTS.STANDARD);
}
}
@@ -440,14 +497,9 @@ export default class PhotoDialog extends Vue {
this.fileName = result.fileName;
} catch (error: unknown) {
logger.error("Error picking image:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to pick image. Please try again.",
},
5000,
this.notify.error(
NOTIFY_PHOTO_PROCESSING_ERROR.message,
TIMEOUTS.STANDARD,
);
}
}
@@ -489,14 +541,9 @@ export default class PhotoDialog extends Vue {
};
const formData = new FormData();
if (!this.blob) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was an error finding the picture. Please try again.",
},
5000,
this.notify.error(
NOTIFY_PHOTO_PROCESSING_ERROR.message,
TIMEOUTS.STANDARD,
);
this.uploading = false;
return;
@@ -525,7 +572,7 @@ export default class PhotoDialog extends Vue {
// Log the raw error first
logger.error("Raw error object:", JSON.stringify(error, null, 2));
let errorMessage = "There was an error saving the picture.";
let errorMessage = NOTIFY_PHOTO_UPLOAD_ERROR.message;
if (axios.isAxiosError(error)) {
const status = error.response?.status;
@@ -548,10 +595,9 @@ export default class PhotoDialog extends Vue {
if (status === 401) {
errorMessage = "Authentication failed. Please try logging in again.";
} else if (status === 413) {
errorMessage = "Image file is too large. Please try a smaller image.";
errorMessage = NOTIFY_PHOTO_SIZE_ERROR.message;
} else if (status === 415) {
errorMessage =
"Unsupported image format. Please try a different image.";
errorMessage = NOTIFY_PHOTO_UNSUPPORTED_FORMAT.message;
} else if (status && status >= 500) {
errorMessage = "Server error. Please try again later.";
} else if (data?.message) {
@@ -573,15 +619,7 @@ export default class PhotoDialog extends Vue {
});
}
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: errorMessage,
},
5000,
);
this.notify.error(errorMessage, TIMEOUTS.STANDARD);
this.uploading = false;
this.blob = undefined;
}