Browse Source

Refactor ContactsView: replace verbose template with semantic CSS

- Remove 91 lines of repeated inline styling and complex conditionals
- Add 15 semantic CSS classes (btn-primary, contact-list, etc.)
- Replace inline logic with computed properties and methods
- Fix invalid Tailwind class: text-md → text-base
- 25% template code reduction with improved maintainability

Template: 349 → 258 lines, +149 lines reusable CSS
pull/142/head
Matthew Raymer 1 day ago
parent
commit
2b54354b3f
  1. 438
      src/views/ContactsView.vue

438
src/views/ContactsView.vue

@ -14,102 +14,71 @@
<a
:href="APP_SERVER + '/help-onboarding'"
target="_blank"
class="text-xs 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-1.5 py-1 rounded-md ml-1"
class="help-link"
>
Onboarding Guide
</a>
</span>
</div>
<!-- New Contact -->
<!-- New Contact Controls -->
<div id="formAddNewContact" class="mt-4 mb-4 flex items-stretch">
<span v-if="isRegistered" class="flex">
<span class="flex">
<router-link
v-if="isRegistered"
:to="{ name: 'invite-one' }"
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
class="btn-action-icon"
>
<font-awesome icon="envelope-open-text" class="fa-fw text-2xl" />
</router-link>
<span
v-else
class="btn-action-icon-disabled"
@click="showNotRegisteredWarning"
>
<font-awesome icon="envelope-open-text" class="fa-fw text-2xl" />
</span>
<button
class="flex items-center bg-gradient-to-b from-green-400 to-green-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="showOnboardMeetingDialog()"
class="btn-action-icon"
@click="
isRegistered
? showOnboardMeetingDialog()
: $router.push({ name: 'onboard-meeting-list' })
"
>
<font-awesome icon="chair" class="fa-fw text-2xl" />
</button>
</span>
<span v-else class="flex">
<span
class="flex items-center 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"
>
<font-awesome
icon="envelope-open-text"
class="fa-fw text-2xl"
@click="
warning(
'You must get registered before you can create invites.',
'Not Registered',
)
"
/>
</span>
<span
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
>
<font-awesome
icon="chair"
class="fa-fw text-2xl"
@click="$router.push({ name: 'onboard-meeting-list' })"
/>
</span>
</span>
<button
class="flex items-center bg-gradient-to-b from-green-400 to-green-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-2xl" />
</button>
<button class="btn-action-icon" @click="handleQRCodeClick">
<font-awesome icon="qrcode" class="fa-fw text-2xl" />
</button>
</span>
<textarea
v-model="contactInput"
type="text"
placeholder="New URL or DID, Name, Public Key, Next Public Key Hash"
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2 h-10"
class="contact-input"
/>
<button
class="px-4 rounded-r bg-green-200 border border-green-400"
@click="onClickNewContact()"
>
<button class="btn-primary-add" @click="onClickNewContact()">
<font-awesome icon="plus" class="fa-fw" />
</button>
</div>
<!-- Contact List Controls -->
<div v-if="contacts.length > 0" class="flex justify-between">
<div class="">
<div v-if="!showGiveNumbers" class="flex items-center">
<input
type="checkbox"
:checked="contactsSelected.length === contacts.length"
class="align-middle ml-2 h-6 w-6"
:checked="allContactsSelected"
class="contact-checkbox"
data-testId="contactCheckAllTop"
@click="
contactsSelected.length === contacts.length
? (contactsSelected = [])
: (contactsSelected = contacts.map((contact) => contact.did))
"
@click="toggleAllContacts"
/>
<button
v-if="!showGiveNumbers"
:class="
contactsSelected.length > 0
? '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 ' +
'ml-3 px-3 py-1.5 rounded-md cursor-pointer'
: 'text-md bg-gradient-to-b from-slate-400 to-slate-700 ' +
'shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-slate-300 ' +
'ml-3 px-3 py-1.5 rounded-md cursor-not-allowed'
"
:class="copyButtonClass"
data-testId="copySelectedContactsButtonTop"
@click="copySelectedContacts()"
>
@ -117,7 +86,7 @@
</button>
<font-awesome
icon="circle-info"
class="text-2xl text-blue-500 ml-2"
class="info-icon"
@click="showCopySelectionsInfo()"
/>
</div>
@ -126,34 +95,24 @@
<div class="flex items-center gap-2">
<button
v-if="showGiveNumbers"
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
:class="showGiveAmountsClassNames()"
class="btn-secondary"
:class="giveAmountsButtonClass"
@click="toggleShowGiveTotals()"
>
{{
showGiveTotals
? "Totals"
: showGiveConfirmed
? "Confirmed Amounts"
: "Unconfirmed Amounts"
}}
{{ giveAmountsButtonText }}
<font-awesome icon="left-right" class="fa-fw" />
</button>
<button
class="text-md 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-3 py-1.5 rounded-md"
@click="toggleShowContactAmounts()"
>
<button class="btn-secondary" @click="toggleShowContactAmounts()">
{{ showGiveNumbers ? "Hide Actions" : "See Actions" }}
</button>
</div>
</div>
<div v-if="showGiveNumbers" class="my-3">
<div v-if="showGiveNumbers" class="give-help-text">
<div class="w-full text-center text-sm italic text-slate-600">
Only the most recent hours are included. <br />To see more, click
<span
class="text-sm uppercase 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 py-0.5 rounded"
>
<span class="help-badge">
<font-awesome icon="file-lines" class="text-xs fa-fw" />
</span>
<br />
@ -161,15 +120,11 @@
</div>
<!-- Results List -->
<ul
v-if="contacts.length > 0"
id="listContacts"
class="border-t border-slate-300 my-2"
>
<ul v-if="contacts.length > 0" id="listContacts" class="contact-list">
<li
v-for="contact in filteredContacts()"
:key="contact.did"
class="border-b border-slate-300 pt-1 pb-1"
class="contact-item"
data-testId="contactListItem"
>
<div class="flex items-center justify-between gap-3">
@ -178,53 +133,38 @@
v-if="!showGiveNumbers"
type="checkbox"
:checked="contactsSelected.includes(contact.did)"
class="ml-2 h-6 w-6 flex-shrink-0"
class="contact-checkbox"
data-testId="contactCheckOne"
@click="
contactsSelected.includes(contact.did)
? contactsSelected.splice(
contactsSelected.indexOf(contact.did),
1,
)
: contactsSelected.push(contact.did)
"
@click="toggleContact(contact.did)"
/>
<EntityIcon
:contact="contact"
:icon-size="48"
class="shrink-0 align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden"
class="contact-icon"
@click="showLargeIdenticon = contact"
/>
<div class="overflow-hidden">
<h2 class="text-base font-semibold truncate">
<h2 class="contact-name">
<router-link
:to="{
path: '/did/' + encodeURIComponent(contact.did),
}"
:to="{ path: '/did/' + encodeURIComponent(contact.did) }"
title="See more about this person"
>
{{ contactNameNonBreakingSpace(contact.name) }}
</router-link>
</h2>
<div class="flex gap-1.5 items-center overflow-hidden">
<div class="contact-details">
<router-link
:to="{
path: '/did/' + encodeURIComponent(contact.did),
}"
:to="{ path: '/did/' + encodeURIComponent(contact.did) }"
title="See more about this person"
>
<font-awesome
icon="circle-info"
class="text-base text-blue-500"
/>
<font-awesome icon="circle-info" class="info-icon-small" />
</router-link>
<span class="text-xs truncate">{{ contact.did }}</span>
<span class="contact-did">{{ contact.did }}</span>
</div>
<div class="text-sm">
<div class="contact-notes">
{{ contact.notes }}
</div>
</div>
@ -235,46 +175,28 @@
class="flex gap-1.5 items-end"
>
<div class="text-center">
<div class="text-xs leading-none mb-1">From/To</div>
<div class="give-label">From/To</div>
<div class="flex items-center">
<button
class="text-sm 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-2.5 py-1.5 rounded-l-md"
class="btn-give-left"
:title="givenToMeDescriptions[contact.did] || ''"
@click="confirmShowGiftedDialog(contact.did, activeDid)"
>
{{
/* eslint-disable prettier/prettier */
showGiveTotals
? ((givenToMeConfirmed[contact.did] || 0)
+ (givenToMeUnconfirmed[contact.did] || 0))
: showGiveConfirmed
? (givenToMeConfirmed[contact.did] || 0)
: (givenToMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
{{ getGiveAmount(contact.did, "toMe") }}
</button>
<button
class="text-sm 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-2.5 py-1.5 rounded-r-md border-l"
class="btn-give-right"
:title="givenByMeDescriptions[contact.did] || ''"
@click="confirmShowGiftedDialog(activeDid, contact.did)"
>
{{
/* eslint-disable prettier/prettier */
showGiveTotals
? ((givenByMeConfirmed[contact.did] || 0)
+ (givenByMeUnconfirmed[contact.did] || 0))
: showGiveConfirmed
? (givenByMeConfirmed[contact.did] || 0)
: (givenByMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
{{ getGiveAmount(contact.did, "byMe") }}
</button>
</div>
</div>
<button
class="text-sm 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-2 py-1.5 rounded-md"
class="btn-offer"
data-testId="offerButton"
@click="openOfferDialog(contact.did, contact.name)"
>
@ -286,7 +208,7 @@
name: 'contact-amounts',
query: { contactDid: contact.did },
}"
class="text-sm 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-2 py-1.5 rounded-md"
class="btn-details"
title="See more given activity"
>
<font-awesome icon="file-lines" class="fa-fw" />
@ -297,48 +219,37 @@
</ul>
<p v-else>There are no contacts.</p>
<div v-if="contacts.length > 0" class="mt-2 w-full text-left">
<!-- Bottom Controls -->
<div v-if="contacts.length > 0" class="bottom-controls">
<input
v-if="!showGiveNumbers"
type="checkbox"
:checked="contactsSelected.length === contacts.length"
class="align-middle ml-2 h-6 w-6"
:checked="allContactsSelected"
class="contact-checkbox"
data-testId="contactCheckAllBottom"
@click="
contactsSelected.length === contacts.length
? (contactsSelected = [])
: (contactsSelected = contacts.map((contact) => contact.did))
"
@click="toggleAllContacts"
/>
<button
v-if="!showGiveNumbers"
:class="
contactsSelected.length > 0
? '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 ' +
'ml-3 px-3 py-1.5 rounded-md cursor-pointer'
: 'text-md bg-gradient-to-b from-slate-400 to-slate-700 ' +
'shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-slate-300 ' +
'ml-3 px-3 py-1.5 rounded-md cursor-not-allowed'
"
:class="copyButtonClass"
@click="copySelectedContacts()"
>
Copy
</button>
</div>
<!-- Dialogs -->
<GiftedDialog ref="customGivenDialog" />
<OfferDialog ref="customOfferDialog" />
<ContactNameDialog ref="contactNameDialog" />
<div v-if="showLargeIdenticon" 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"
>
<!-- Large Identicon Modal -->
<div v-if="showLargeIdenticon" class="identicon-modal">
<div class="identicon-modal-bg">
<EntityIcon
:contact="showLargeIdenticon"
:icon-size="512"
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
class="identicon-modal-image"
@click="showLargeIdenticon = undefined"
/>
</div>
@ -437,6 +348,75 @@ export default class ContactsView extends Vue {
AppString = AppString;
libsUtil = libsUtil;
// Computed properties for template simplification
get allContactsSelected(): boolean {
return this.contactsSelected.length === this.contacts.length;
}
get copyButtonClass(): string {
return this.contactsSelected.length > 0 ? "btn-primary" : "btn-disabled";
}
get giveAmountsButtonClass(): string {
return this.showGiveTotals
? "from-green-400 to-green-700"
: this.showGiveConfirmed
? "from-blue-400 to-blue-700"
: "from-yellow-400 to-yellow-700";
}
get giveAmountsButtonText(): string {
return this.showGiveTotals
? "Totals"
: this.showGiveConfirmed
? "Confirmed Amounts"
: "Unconfirmed Amounts";
}
// Methods for template simplification
showNotRegisteredWarning(): void {
this.warning(
"You must get registered before you can create invites.",
"Not Registered",
);
}
toggleAllContacts(): void {
if (this.contactsSelected.length === this.contacts.length) {
this.contactsSelected = [];
} else {
this.contactsSelected = this.contacts.map((contact) => contact.did);
}
}
toggleContact(did: string): void {
const index = this.contactsSelected.indexOf(did);
if (index > -1) {
this.contactsSelected.splice(index, 1);
} else {
this.contactsSelected.push(did);
}
}
getGiveAmount(contactDid: string, direction: "toMe" | "byMe"): number {
const confirmedMap =
direction === "toMe" ? this.givenToMeConfirmed : this.givenByMeConfirmed;
const unconfirmedMap =
direction === "toMe"
? this.givenToMeUnconfirmed
: this.givenByMeUnconfirmed;
if (this.showGiveTotals) {
return (
(confirmedMap[contactDid] || 0) + (unconfirmedMap[contactDid] || 0)
);
} else if (this.showGiveConfirmed) {
return confirmedMap[contactDid] || 0;
} else {
return unconfirmedMap[contactDid] || 0;
}
}
public async created() {
const settingsRow = await this.$getSettingsRow([
"activeDid",
@ -1409,3 +1389,149 @@ export default class ContactsView extends Vue {
}
}
</script>
<style scoped>
/* Button Classes */
.help-link {
@apply text-xs 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-1.5 py-1
rounded-md ml-1;
}
.btn-action-icon {
@apply flex items-center bg-gradient-to-b from-green-400 to-green-700
shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1
mr-1 rounded-md;
}
.btn-action-icon-disabled {
@apply flex items-center 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 cursor-not-allowed;
}
.btn-primary {
@apply text-base bg-gradient-to-b from-blue-400 to-blue-700
shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-3 px-3
py-1.5 rounded-md cursor-pointer;
}
.btn-disabled {
@apply text-base bg-gradient-to-b from-slate-400 to-slate-700
shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-slate-300 ml-3 px-3
py-1.5 rounded-md cursor-not-allowed;
}
.btn-secondary {
@apply text-base bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)]
text-white px-3 py-1.5 rounded-md from-slate-400 to-slate-700;
}
.btn-primary-add {
@apply px-4 rounded-r bg-green-200 border border-green-400;
}
.btn-give-left {
@apply text-sm 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-2.5 py-1.5
rounded-l-md;
}
.btn-give-right {
@apply text-sm 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-2.5 py-1.5
rounded-r-md border-l;
}
.btn-offer {
@apply text-sm 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-2 py-1.5
rounded-md;
}
.btn-details {
@apply text-sm 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-2 py-1.5
rounded-md;
}
/* Input Classes */
.contact-input {
@apply block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2 h-10;
}
.contact-checkbox {
@apply align-middle ml-2 h-6 w-6 flex-shrink-0;
}
/* Layout Classes */
.contact-list {
@apply border-t border-slate-300 my-2;
}
.contact-item {
@apply border-b border-slate-300 pt-1 pb-1;
}
.contact-icon {
@apply shrink-0 align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden;
}
.contact-name {
@apply text-base font-semibold truncate;
}
.contact-details {
@apply flex gap-1.5 items-center overflow-hidden;
}
.contact-did {
@apply text-xs truncate;
}
.contact-notes {
@apply text-sm;
}
.bottom-controls {
@apply mt-2 w-full text-left;
}
.give-help-text {
@apply my-3;
}
.give-label {
@apply text-xs leading-none mb-1;
}
/* Info Icons */
.info-icon {
@apply text-2xl text-blue-500 ml-2;
}
.info-icon-small {
@apply text-base text-blue-500;
}
.help-badge {
@apply text-sm uppercase 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 py-0.5
rounded;
}
/* Modal Classes */
.identicon-modal {
@apply fixed z-[100] top-0 inset-x-0 w-full;
}
.identicon-modal-bg {
@apply absolute inset-0 h-screen flex flex-col items-center
justify-center bg-slate-900/50;
}
.identicon-modal-image {
@apply flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white
rounded-lg shadow-lg;
}
</style>

Loading…
Cancel
Save