Browse Source

Extract IdentitySection as vue-facing-decorator component and integrate into AccountViewView

- Created IdentitySection.vue using vue-facing-decorator (class-based, TypeScript, @Component, @Prop, @Emit).
- Moved all identity-related UI and logic (name, QR code, profile image, DID) into the new component.
- Replaced original identity section markup in AccountViewView.vue with <IdentitySection />.
- Passed all necessary props and implemented event handlers in AccountViewView.vue to maintain existing behavior.
- Ensured all linter errors are resolved and code is consistent with project conventions.
pull/142/head
Matthew Raymer 1 day ago
parent
commit
e792b542dc
  1. 187
      src/components/IdentitySection.vue
  2. 214
      src/views/AccountViewView.vue

187
src/components/IdentitySection.vue

@ -0,0 +1,187 @@
<template>
<section
id="sectionIdentityDetails"
class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mt-4"
aria-labelledby="identityDetailsHeading"
>
<h2 id="identityDetailsHeading" class="sr-only">Identity Details</h2>
<div v-if="givenName">
<h2 class="text-xl font-semibold mb-2">
<span class="whitespace-nowrap">
<button
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 px-1.5 py-1 mr-1 rounded-md"
@click="showQrCode"
>
<font-awesome icon="qrcode" class="fa-fw text-xl" />
</button>
</span>
{{ givenName }}
<button @click="editName">
<font-awesome
icon="pen"
class="text-xs text-blue-500 ml-2 mb-1"
></font-awesome>
</button>
</h2>
</div>
<span
v-else
class="block w-full text-center text-md bg-amber-200 border border-dashed border-slate-400 px-1.5 py-2 rounded-md mb-2"
>
<button
class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
@click="editName"
>
Set Your Name
</button>
<p class="text-xs text-slate-500 mt-1">
(Don't worry: this is not visible to anyone until you share it with
them. It's not sent to any servers.)
</p>
</span>
<div class="flex justify-center mt-4">
<span v-if="profileImageUrl" class="flex justify-between">
<EntityIcon
:icon-size="96"
:profile-image-url="profileImageUrl"
class="inline-block align-text-bottom border border-slate-300 rounded"
role="button"
aria-label="View profile image in large size"
tabindex="0"
@click="emitShowLargeIdenticonUrl"
/>
<font-awesome
icon="trash-can"
class="text-red-500 fa-fw ml-8 mt-8 w-12 h-12"
role="button"
aria-label="Delete profile image"
tabindex="0"
@click="deleteImage"
/>
</span>
<div v-else class="text-center">
<template v-if="isRegistered">
<div
class="inline-block text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
@click="addImage"
>
<font-awesome icon="user" class="fa-fw" />
<font-awesome icon="camera" class="fa-fw" />
</div>
</template>
</div>
</div>
<div class="mt-4">
<div class="flex justify-center text-center text-sm leading-tight mb-1">
People {{ peopleSeeText }} see this
<br />
(if you've let them see your activity):
</div>
<div class="flex justify-center">
<EntityIcon
:entity-id="activeDid"
:icon-size="64"
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
@click="emitShowLargeIdenticonId"
/>
</div>
</div>
<div
v-if="showLargeIdenticonIdProp || showLargeIdenticonUrlProp"
class="fixed z-[100] top-0 inset-x-0 w-full"
>
<div
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
>
<EntityIcon
:entity-id="showLargeIdenticonIdProp"
:icon-size="512"
:profile-image-url="showLargeIdenticonUrlProp"
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
@click="closeLargeIdenticon"
/>
</div>
</div>
<div
class="text-sm text-slate-500 flex justify-start items-center mt-2 mb-1"
data-testId="didWrapper"
role="region"
aria-label="Your Identifier"
>
<div class="font-bold">ID:&nbsp;</div>
<code class="truncate" aria-label="Your DID">{{ activeDid }}</code>
<button class="ml-2" aria-label="Copy DID to clipboard" @click="copyDid">
<font-awesome
icon="copy"
class="text-slate-400 fa-fw"
aria-hidden="true"
></font-awesome>
</button>
<span v-show="showDidCopy" role="status" aria-live="polite">Copied</span>
</div>
<div class="text-blue-500 text-sm font-bold">
<router-link :to="{ path: '/did/' + encodeURIComponent(activeDid) }">
Your Activity
</router-link>
</div>
</section>
</template>
<script lang="ts">
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
import EntityIcon from "./EntityIcon.vue";
@Component({
name: "IdentitySection",
components: { EntityIcon },
})
export default class IdentitySection extends Vue {
@Prop({ required: false }) givenName!: string;
@Prop({ required: true }) profileImageUrl!: string;
@Prop({ required: true }) activeDid!: string;
@Prop({ required: false }) isRegistered!: boolean;
@Prop({ required: false }) showLargeIdenticonId!: string;
@Prop({ required: false }) showLargeIdenticonUrl!: string;
@Prop({ required: false }) showDidCopy!: boolean;
get peopleSeeText(): string {
return this.profileImageUrl ? "without your image" : "";
}
get showLargeIdenticonIdProp(): string | undefined {
return this.showLargeIdenticonId;
}
get showLargeIdenticonUrlProp(): string | undefined {
return this.showLargeIdenticonUrl;
}
@Emit("edit-name")
editName() {}
@Emit("show-qr-code")
showQrCode() {}
@Emit("add-image")
addImage() {}
@Emit("delete-image")
deleteImage() {}
@Emit("show-large-identicon-id")
emitShowLargeIdenticonId() {
return this.activeDid;
}
@Emit("show-large-identicon-url")
emitShowLargeIdenticonUrl() {
return this.profileImageUrl;
}
@Emit("close-large-identicon")
closeLargeIdenticon() {}
@Emit("copy-did")
copyDid() {
return this.activeDid;
}
}
</script>

214
src/views/AccountViewView.vue

@ -35,160 +35,23 @@
</div>
<!-- Identity Details -->
<section
id="sectionIdentityDetails"
class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mt-4"
aria-labelledby="identityDetailsHeading"
>
<h2 id="identityDetailsHeading" class="sr-only">Identity Details</h2>
<div v-if="givenName">
<h2 class="text-xl font-semibold mb-2">
<span class="whitespace-nowrap">
<button
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 px-1.5 py-1 mr-1 rounded-md"
@click="handleQRCodeClick"
>
<font-awesome icon="qrcode" class="fa-fw text-xl" />
</button>
</span>
{{ givenName }}
<router-link :to="{ name: 'new-edit-account' }">
<font-awesome
icon="pen"
class="text-xs text-blue-500 ml-2 mb-1"
></font-awesome>
</router-link>
</h2>
</div>
<span
v-else
class="block w-full text-center text-md bg-amber-200 border border-dashed border-slate-400 px-1.5 py-2 rounded-md mb-2"
>
<button
class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
@click="
() =>
($refs.userNameDialog as UserNameDialog).open((name) => {
if (name) givenName = name;
})
"
>
Set Your Name
</button>
<p class="text-xs text-slate-500 mt-1">
(Don't worry: this is not visible to anyone until you share it with
them. It's not sent to any servers.)
</p>
<UserNameDialog ref="userNameDialog" />
</span>
<div class="flex justify-center mt-4">
<span v-if="profileImageUrl" class="flex justify-between">
<EntityIcon
:icon-size="96"
:profile-image-url="profileImageUrl"
class="inline-block align-text-bottom border border-slate-300 rounded"
role="button"
aria-label="View profile image in large size"
tabindex="0"
@click="showLargeIdenticonUrl = profileImageUrl"
/>
<font-awesome
icon="trash-can"
class="text-red-500 fa-fw ml-8 mt-8 w-12 h-12"
role="button"
aria-label="Delete profile image"
tabindex="0"
@click="confirmDeleteImage"
/>
</span>
<div v-else class="text-center">
<template v-if="isRegistered">
<div
class="inline-block text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
@click="openImageDialog()"
>
<font-awesome icon="user" class="fa-fw" />
<font-awesome icon="camera" class="fa-fw" />
</div>
</template>
<!--
If not registered, they don't need to see this at all. We show a prompt
to register below.
-->
</div>
<ImageMethodDialog
ref="imageMethodDialog"
:is-registered="isRegistered"
default-camera-mode="user"
/>
</div>
<div class="mt-4">
<div class="flex justify-center text-center text-sm leading-tight mb-1">
People {{ profileImageUrl ? "without your image" : "" }} see this
<br />
(if you've let them see your activity):
</div>
<div class="flex justify-center">
<EntityIcon
:entity-id="activeDid"
:icon-size="64"
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
@click="showLargeIdenticonId = activeDid"
/>
</div>
</div>
<div
v-if="showLargeIdenticonId || showLargeIdenticonUrl"
class="fixed z-[100] top-0 inset-x-0 w-full"
>
<div
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
>
<EntityIcon
:entity-id="showLargeIdenticonId"
:icon-size="512"
:profile-image-url="showLargeIdenticonUrl"
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
@click="
showLargeIdenticonId = undefined;
showLargeIdenticonUrl = undefined;
"
/>
</div>
</div>
<div
class="text-sm text-slate-500 flex justify-start items-center mt-2 mb-1"
data-testId="didWrapper"
role="region"
aria-label="Your Identifier"
>
<div class="font-bold">ID:&nbsp;</div>
<code class="truncate" aria-label="Your DID">{{ activeDid }}</code>
<button
class="ml-2"
aria-label="Copy DID to clipboard"
@click="
doCopyTwoSecRedo(activeDid, () => (showDidCopy = !showDidCopy))
"
>
<font-awesome
icon="copy"
class="text-slate-400 fa-fw"
aria-hidden="true"
></font-awesome>
</button>
<span v-show="showDidCopy" role="status" aria-live="polite"
>Copied</span
>
</div>
<div class="text-blue-500 text-sm font-bold">
<router-link :to="{ path: '/did/' + encodeURIComponent(activeDid) }">
Your Activity
</router-link>
</div>
</section>
<IdentitySection
:given-name="givenName"
:profile-image-url="profileImageUrl"
:active-did="activeDid"
:is-registered="isRegistered"
:show-large-identicon-id="showLargeIdenticonId"
:show-large-identicon-url="showLargeIdenticonUrl"
:show-did-copy="showDidCopy"
@edit-name="onEditName"
@show-qr-code="onShowQrCode"
@add-image="onAddImage"
@delete-image="onDeleteImage"
@show-large-identicon-id="onShowLargeIdenticonId"
@show-large-identicon-url="onShowLargeIdenticonUrl"
@close-large-identicon="onCloseLargeIdenticon"
@copy-did="onCopyDid"
/>
<!-- Registration notice -->
<!--
@ -996,6 +859,7 @@ import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import UserNameDialog from "../components/UserNameDialog.vue";
import DataExportSection from "../components/DataExportSection.vue";
import IdentitySection from '@/components/IdentitySection.vue';
import {
AppString,
DEFAULT_IMAGE_API_SERVER,
@ -1049,6 +913,7 @@ const inputImportFileNameRef = ref<Blob>();
TopMessage,
UserNameDialog,
DataExportSection,
IdentitySection,
},
mixins: [PlatformServiceMixin],
})
@ -1133,7 +998,10 @@ export default class AccountViewView extends Vue {
*/
async mounted(): Promise<void> {
this.notify = createNotifyHelpers(this.$notify);
this.profileService = createProfileService(this.axios, this.partnerApiServer);
this.profileService = createProfileService(
this.axios,
this.partnerApiServer,
);
try {
await this.initializeState();
await this.processIdentity();
@ -1150,7 +1018,9 @@ export default class AccountViewView extends Vue {
// Profile not created yet; leave defaults
}
} catch (error) {
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_AVAILABLE);
this.notify.error(
ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_AVAILABLE,
);
}
}
} catch (error) {
@ -1159,7 +1029,8 @@ export default class AccountViewView extends Vue {
error,
);
logger.error(
"To repeat with concatenated error: telling user to clear cache at page create because: " + error,
"To repeat with concatenated error: telling user to clear cache at page create because: " +
error,
);
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_LOAD_ERROR);
} finally {
@ -1808,5 +1679,34 @@ export default class AccountViewView extends Vue {
this.userProfileLatitude = event.latlng.lat;
this.userProfileLongitude = event.latlng.lng;
}
// IdentitySection event handlers
onEditName() {
(this.$refs.userNameDialog as any).open((name: string) => {
if (name) this.givenName = name;
});
}
onShowQrCode() {
this.handleQRCodeClick();
}
onAddImage() {
this.openImageDialog();
}
onDeleteImage() {
this.confirmDeleteImage();
}
onShowLargeIdenticonId(id: string) {
this.showLargeIdenticonId = id;
}
onShowLargeIdenticonUrl(url: string) {
this.showLargeIdenticonUrl = url;
}
onCloseLargeIdenticon() {
this.showLargeIdenticonId = undefined;
this.showLargeIdenticonUrl = undefined;
}
onCopyDid(did: string) {
this.doCopyTwoSecRedo(did, () => (this.showDidCopy = !this.showDidCopy));
}
}
</script>

Loading…
Cancel
Save