forked from trent_larson/crowd-funder-for-time-pwa
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.
This commit is contained in:
187
src/components/IdentitySection.vue
Normal file
187
src/components/IdentitySection.vue
Normal file
@@ -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: </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>
|
||||
@@ -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: </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>
|
||||
|
||||
Reference in New Issue
Block a user