Compare commits

...

16 Commits

Author SHA1 Message Date
Jose Olarte III
5dbd66e51b Nav tweaks 2025-03-12 23:12:04 +08:00
Jose Olarte III
312b4aaaa3 Padding adjustments 2025-03-12 17:54:18 +08:00
Jose Olarte III
3a6a24d923 Contact list tweaks 2025-03-12 16:50:13 +08:00
Jose Olarte III
d7afb80a07 Pointer cursor 2025-03-12 15:51:39 +08:00
Jose Olarte III
751df09fe5 Button style tweaks + consistency 2025-03-12 15:51:15 +08:00
Jose Olarte III
8858495f73 Larger contact image
ClickUp task 86b3dgv2f
2025-03-10 19:55:57 +08:00
Jose Olarte III
ecb088bee2 Recolored confirm button to gray
ClickUp task 86b3y8f95
2025-03-10 19:08:49 +08:00
e96617ca0f tweak tests for clarity 2025-02-28 12:17:22 -07:00
b91f2a5df7 fix one more styling error 2025-02-27 18:02:05 -07:00
f6871e139d fix linting 2025-02-27 17:51:57 -07:00
61afba3bca show totals of gives to a project 2025-02-22 20:34:23 -07:00
eabe2b9448 fix problem when going directly to people-map where the search results disappear 2025-02-17 20:35:05 -07:00
5eaaf32043 bump version and add "-beta" (was 0.4.3 now 0.4.4-beta) 2025-02-17 19:22:03 -07:00
1e9c3f3101 add Discover query param searchPeople to go straight to people map 2025-02-17 19:19:38 -07:00
2e60e2bba9 bump version and add "-beta" 2025-02-17 08:55:07 -07:00
78d7f38aa3 Merge pull request 'new build process to allow building native apps' (#126) from split_build_process into master
Reviewed-on: #126
2025-02-17 10:50:47 -05:00
15 changed files with 341 additions and 190 deletions

View File

@@ -259,13 +259,23 @@ See `.env.*` files for configuration.
## Deployment ## Deployment
### Version Management
1. Update CHANGELOG.md with new changes
2. Update version in package.json
3. Commit changes and tag release:
```bash
git tag <VERSION_TAG>
git push origin <VERSION_TAG>
```
4. After deployment, update package.json with next version + "-beta"
### Test Server ### Test Server
```bash ```bash
# Build using staging environment # Build using staging environment
npm run build -- --mode staging npm run build -- --mode staging
# Deploy to test server # Deploy to test server
rsync -azvu -e "ssh -i ~/.ssh/your_key" dist ubuntutest@test.timesafari.app:time-safari/ rsync -azvu -e "ssh -i ~/.ssh/<YOUR_KEY>" dist ubuntutest@test.timesafari.app:time-safari/
``` ```
### Production Server ### Production Server
@@ -274,26 +284,15 @@ rsync -azvu -e "ssh -i ~/.ssh/your_key" dist ubuntutest@test.timesafari.app:time
pkgx +npm sh pkgx +npm sh
cd crowd-funder-for-time-pwa cd crowd-funder-for-time-pwa
git checkout master && git pull git checkout master && git pull
git checkout <version_tag> git checkout <VERSION_TAG>
npm install npm install
npm run build npm run build
cd - cd -
# Backup and deploy # Backup and deploy
mv time-safari/dist time-safari-dist-prev.0 mv time-safari/dist time-safari-dist-prev.0 && mv crowd-funder-for-time-pwa/dist time-safari/
mv crowd-funder-for-time-pwa/dist time-safari/
``` ```
### Version Management
1. Update CHANGELOG.md with new changes
2. Update version in package.json
3. Commit changes and tag release:
```bash
git tag <version>
git push origin <version>
```
4. After deployment, update package.json with next version + "-beta"
## Troubleshooting ## Troubleshooting
### Common Build Issues ### Common Build Issues

View File

@@ -6,12 +6,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
== [0.4.2] - 2025.02.17
### Merged ## [0.4.5] - 2025.02.23
- Master to split_process_build ### Added
- fixed path issues - Total amounts of gives on project page
- all tests passing ### Changed in DB or environment
- capacitor build to Android working - Requires Endorser.ch version 4.2.6+
## [0.4.4] - 2025.02.17
### Fixed
- On production (due to data?) the search results would disappear after scrolling down. Now we don't show any results when going to the people map with a shortcut.
## [0.4.3] - 2025.02.17
### Added
- Discover query parameter searchPeople to go directly to the people map
## [0.4.2] - 2025.02.17
### Added
- Capacitor build to Android
### Fixed
- Path issues
## [0.4.1] - 2025.02.16 ## [0.4.1] - 2025.02.16
### Fixed ### Fixed
@@ -23,7 +41,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Images in the home feed now take up the full width of the card. - Images in the home feed now take up the full width of the card.
- Clicking the image previously, would open the image in a new tab. Now, clicking the image opens the image in a lightbox view. - Clicking the image previously, would open the image in a new tab. Now, clicking the image opens the image in a lightbox view.
### Added ### Added
- Clicking an image also now displays an in-app lightbox view of the image. - Clicking an image also now displays an in-app lightbox view of the image.
- The lightbox view includes a download button for the image in mobile view. - The lightbox view includes a download button for the image in mobile view.

8
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "TimeSafari", "name": "timesafari",
"version": "0.4.1", "version": "0.4.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "TimeSafari", "name": "timesafari",
"version": "0.4.1", "version": "0.4.4",
"dependencies": { "dependencies": {
"@capacitor/android": "^6.2.0", "@capacitor/android": "^6.2.0",
"@capacitor/cli": "^6.2.0", "@capacitor/cli": "^6.2.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "0.4.2", "version": "0.4.4",
"description": "TimeSafari Desktop Application", "description": "TimeSafari Desktop Application",
"author": { "author": {
"name": "TimeSafari Team" "name": "TimeSafari Team"

View File

@@ -1,7 +1,7 @@
<template> <template>
<!-- QUICK NAV --> <!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50"> <nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2 max-w-3xl mx-auto"> <ul class="flex text-2xl px-6 py-2 gap-1 max-w-3xl mx-auto">
<!-- Home Feed --> <!-- Home Feed -->
<li <li
:class="{ :class="{

View File

@@ -256,7 +256,7 @@
<span class="mb-2 font-bold">Location for Searches</span> <span class="mb-2 font-bold">Location for Searches</span>
<router-link <router-link
:to="{ name: 'search-area' }" :to="{ name: 'search-area' }"
class="text-m 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-2 rounded-md mb-2" class="text-m 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 mb-2"
> >
{{ isSearchAreasSet ? "Change" : "Set" }} Search Area {{ isSearchAreasSet ? "Change" : "Set" }} Search Area
</router-link> </router-link>

View File

@@ -204,7 +204,7 @@
<div v-if="libsUtil.isGiveAction(veriClaim)"> <div v-if="libsUtil.isGiveAction(veriClaim)">
<div class="flex columns-3"> <div class="flex columns-3">
<button <button
class="col-span-1 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-4 py-2 rounded-md" class="col-span-1 bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-4 py-2 rounded-md"
v-if=" v-if="
libsUtil.isGiveRecordTheUserCanConfirm( libsUtil.isGiveRecordTheUserCanConfirm(
isRegistered, isRegistered,

View File

@@ -106,7 +106,7 @@
/> />
<button <button
href="" href=""
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-2 px-1 py-1 rounded-md" class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-3 px-3 py-1.5 rounded-md"
:style=" :style="
contactsSelected.length > 0 contactsSelected.length > 0
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);' ? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
@@ -127,7 +127,7 @@
<div class="w-full text-right"> <div class="w-full text-right">
<button <button
href="" href=""
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-1 py-1 rounded-md" 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()" @click="toggleShowContactAmounts()"
> >
{{ {{
@@ -177,14 +177,7 @@
data-testId="contactListItem" data-testId="contactListItem"
> >
<div class="grow overflow-hidden"> <div class="grow overflow-hidden">
<div class="flex items-center"> <div class="flex items-center gap-3">
<EntityIcon
:contact="contact"
:iconSize="24"
class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer"
@click="showLargeIdenticon = contact"
/>
<input <input
type="checkbox" type="checkbox"
v-if="!showGiveNumbers" v-if="!showGiveNumbers"
@@ -201,94 +194,97 @@
data-testId="contactCheckOne" data-testId="contactCheckOne"
/> />
<h2 <EntityIcon
class="text-base font-semibold ml-2 w-1/3 truncate flex-shrink-0" :contact="contact"
> :iconSize="48"
class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden"
@click="showLargeIdenticon = contact"
/>
<h2 class="text-base font-semibold w-1/3 truncate flex-shrink-0">
{{ contactNameNonBreakingSpace(contact.name) }} {{ contactNameNonBreakingSpace(contact.name) }}
</h2> </h2>
<span> <span>
<div class="flex items-center"> <div class="flex gap-2 items-center">
<router-link <router-link
:to="{ :to="{
path: '/did/' + encodeURIComponent(contact.did), path: '/did/' + encodeURIComponent(contact.did),
}" }"
title="See more about this person" title="See more about this person"
> >
<fa icon="circle-info" class="text-xl text-blue-500 ml-4" /> <fa icon="circle-info" class="text-xl text-blue-500" />
</router-link> </router-link>
<span class="ml-4 text-sm overflow-hidden">{{ <span class="text-sm overflow-hidden">{{
libsUtil.shortDid(contact.did) libsUtil.shortDid(contact.did)
}}</span> }}</span>
</div> </div>
<div class="ml-4 text-sm"> <div class="text-sm">
{{ contact.notes }} {{ contact.notes }}
</div> </div>
</span> </span>
</div> </div>
<div id="ContactActions" class="flex gap-1.5 mt-2"> <div
<div v-if="showGiveNumbers && contact.did != activeDid"
v-if="showGiveNumbers && contact.did != activeDid" class="ml-auto flex gap-1.5 mt-2"
class="ml-auto flex gap-1.5" >
<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-l-md"
@click="confirmShowGiftedDialog(contact.did, activeDid)"
:title="givenToMeDescriptions[contact.did] || ''"
> >
<button From:
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-l-md" <br />
@click="confirmShowGiftedDialog(contact.did, activeDid)" {{
:title="givenToMeDescriptions[contact.did] || ''" /* eslint-disable prettier/prettier */
> this.showGiveTotals
From: ? ((givenToMeConfirmed[contact.did] || 0)
<br /> + (givenToMeUnconfirmed[contact.did] || 0))
{{ : this.showGiveConfirmed
/* eslint-disable prettier/prettier */ ? (givenToMeConfirmed[contact.did] || 0)
this.showGiveTotals : (givenToMeUnconfirmed[contact.did] || 0)
? ((givenToMeConfirmed[contact.did] || 0) /* eslint-enable prettier/prettier */
+ (givenToMeUnconfirmed[contact.did] || 0)) }}
: this.showGiveConfirmed </button>
? (givenToMeConfirmed[contact.did] || 0)
: (givenToMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
</button>
<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 -ml-1.5 px-2 py-1.5 rounded-r-md border-l" 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 -ml-1.5 px-2 py-1.5 rounded-r-md border-l"
@click="confirmShowGiftedDialog(activeDid, contact.did)" @click="confirmShowGiftedDialog(activeDid, contact.did)"
:title="givenByMeDescriptions[contact.did] || ''" :title="givenByMeDescriptions[contact.did] || ''"
> >
To: To:
<br /> <br />
{{ {{
/* eslint-disable prettier/prettier */ /* eslint-disable prettier/prettier */
this.showGiveTotals this.showGiveTotals
? ((givenByMeConfirmed[contact.did] || 0) ? ((givenByMeConfirmed[contact.did] || 0)
+ (givenByMeUnconfirmed[contact.did] || 0)) + (givenByMeUnconfirmed[contact.did] || 0))
: this.showGiveConfirmed : this.showGiveConfirmed
? (givenByMeConfirmed[contact.did] || 0) ? (givenByMeConfirmed[contact.did] || 0)
: (givenByMeUnconfirmed[contact.did] || 0) : (givenByMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */ /* eslint-enable prettier/prettier */
}} }}
</button> </button>
<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 py-1.5 rounded-md border border-blue-400" 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 border border-blue-400"
@click="openOfferDialog(contact.did, contact.name)" @click="openOfferDialog(contact.did, contact.name)"
data-testId="offerButton" data-testId="offerButton"
> >
Offer Offer
</button> </button>
<router-link <router-link
:to="{ :to="{
name: 'contact-amounts', name: 'contact-amounts',
query: { contactDid: contact.did }, 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 border border-slate-400" 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 border border-slate-400"
title="See more given activity" title="See more given activity"
> >
<fa icon="file-lines" class="fa-fw" /> <fa icon="file-lines" class="fa-fw" />
</router-link> </router-link>
</div>
</div> </div>
</div> </div>
</li> </li>
@@ -310,7 +306,7 @@
/> />
<button <button
href="" href=""
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 ml-2 px-1 py-1 rounded-md" 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 ml-3 px-3 py-1.5 rounded-md"
:style=" :style="
contactsSelected.length > 0 contactsSelected.length > 0
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);' ? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'

View File

@@ -352,9 +352,9 @@ export default class DiscoverView extends Vue {
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
apiServer = ""; apiServer = "";
isLoading = false; isLoading = false;
isLocalActive = true; isLocalActive = false;
isMappedActive = false; isMappedActive = false;
isAnywhereActive = false; isAnywhereActive = true;
isProjectsActive = true; isProjectsActive = true;
isPeopleActive = false; isPeopleActive = false;
isSearchVisible = true; isSearchVisible = true;
@@ -375,6 +375,11 @@ export default class DiscoverView extends Vue {
didInfo = didInfo; didInfo = didInfo;
async mounted() { async mounted() {
this.searchTerms = this.$route.query["searchText"]?.toString() || "";
const searchPeople = !!this.$route.query["searchPeople"];
const settings = await retrieveSettingsForActiveAccount(); const settings = await retrieveSettingsForActiveAccount();
this.activeDid = (settings.activeDid as string) || ""; this.activeDid = (settings.activeDid as string) || "";
this.apiServer = (settings.apiServer as string) || ""; this.apiServer = (settings.apiServer as string) || "";
@@ -386,25 +391,35 @@ export default class DiscoverView extends Vue {
this.allMyDids = await retrieveAccountDids(); this.allMyDids = await retrieveAccountDids();
this.searchTerms = this.$route.query["searchText"]?.toString() || "";
if (!settings.finishedOnboarding) { if (!settings.finishedOnboarding) {
(this.$refs.onboardingDialog as OnboardingDialog).open( (this.$refs.onboardingDialog as OnboardingDialog).open(
OnboardPage.Discover, OnboardPage.Discover,
); );
} }
if (this.searchBox) { // Someday we'll have enough people that we can default to their local area.
await this.searchLocal(); // if (this.searchBox) {
// this.isLocalActive = true;
// this.isAnywhereActive = false;
// await this.searchLocal();
//
// const bbox = this.searchBox.bbox;
// this.localCenterLat = (bbox.maxLat + bbox.minLat) / 2;
// this.localCenterLong = (bbox.eastLong + bbox.westLong) / 2;
// } else {
const bbox = this.searchBox.bbox; if (searchPeople) {
this.localCenterLat = (bbox.maxLat + bbox.minLat) / 2; this.isPeopleActive = true;
this.localCenterLong = (bbox.eastLong + bbox.westLong) / 2; this.isProjectsActive = false;
this.isMappedActive = true;
this.isAnywhereActive = false;
}
if (this.isMappedActive) {
// The map will be loaded when it's ready
// and if we try to do it here before the map is ready then we get errors.
} else { } else {
this.isLocalActive = false; await this.searchSelected();
this.isMappedActive = false;
this.isAnywhereActive = true;
await this.searchAll();
} }
} }
@@ -466,7 +481,7 @@ export default class DiscoverView extends Vue {
} else { } else {
throw JSON.stringify(results); throw JSON.stringify(results);
} }
} else { } else { // people search must be active
this.projects = []; this.projects = [];
const profiles: UserProfile[] = results.data; const profiles: UserProfile[] = results.data;
if (profiles) { if (profiles) {

View File

@@ -3,7 +3,7 @@
<TopMessage /> <TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-2 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<h1 id="ViewHeading" class="text-4xl text-center font-light mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light mb-8">
{{ AppString.APP_NAME }} {{ AppString.APP_NAME }}
</h1> </h1>

View File

@@ -237,7 +237,7 @@
<button <button
data-testId="offerButton" data-testId="offerButton"
@click="openOfferDialog()" @click="openOfferDialog()"
class="block w-full 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 rounded-md" class="block w-full 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-3 py-1.5 text-sm leading-tight rounded-md"
> >
Offer to this (maybe with conditions)... Offer to this (maybe with conditions)...
</button> </button>
@@ -318,81 +318,146 @@
<div class="text-center"> <div class="text-center">
<button <button
@click="openGiftDialogToProject()" @click="openGiftDialogToProject()"
class="block w-full 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-1rounded-md" class="block w-full 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-3 py-1.5 text-sm leading-tight rounded-md"
> >
Given To This... Given To This...
</button> </button>
</div> </div>
</div> </div>
<h3 class="text-lg font-bold mb-3 mt-4">Given To This Idea</h3> <h3 class="text-lg font-bold mt-4">Given To This Idea</h3>
<div v-if="givesToThis.length === 0"> <div v-if="givesToThis.length === 0" class="text-sm">
(None yet. If you've seen something, say something by clicking a (None yet. If you've seen something, say something by clicking a
contact above.) contact above.)
</div> </div>
<ul v-else class="text-sm border-t border-slate-300"> <div v-else class="mt-1 text-sm">
<li <!-- Totals section -->
v-for="give in givesToThis" <div class="mt-1 flex items-center min-h-[1.5rem]">
:key="give.id" <div v-if="loadingTotals" class="flex-1">
class="py-1.5 border-b border-slate-300" <fa icon="spinner" class="fa-spin-pulse text-blue-500" />
> </div>
<div class="flex justify-between gap-4"> <div v-else-if="givesTotalsByUnit.length > 0" class="flex-1">
<span> <span class="font-semibold mr-2 shrink-0">Totals</span>
<fa icon="user" class="fa-fw text-slate-400" /> <span class="whitespace-nowrap overflow-hidden text-ellipsis">
{{ <a
serverUtil.didInfo( @click="totalsExpanded = !totalsExpanded"
give.agentDid, class="cursor-pointer text-blue-500"
activeDid, >
allMyDids, <!-- just show the hours, or alternatively whatever is first -->
allContacts, <span v-if="givenTotalHours() > 0">
) {{ givenTotalHours() }} {{ libsUtil.UNIT_SHORT["HUR"] }}
</span>
<span v-else>
{{ givesTotalsByUnit[0].amount }}
{{ libsUtil.UNIT_SHORT[givesTotalsByUnit[0].unit] }}
</span>
<span v-if="givesTotalsByUnit.length > 1">...</span>
<span>
<fa
:icon="totalsExpanded ? 'chevron-up' : 'chevron-right'"
class="fa-fw text-xs ml-1"
/>
</span>
</a>
<!-- show the full list when expanded -->
<div v-if="totalsExpanded">
<div
v-for="total in givesTotalsByUnit"
:key="total.unit"
class="ml-2"
>
<fa
:icon="libsUtil.iconForUnitCode(total.unit)"
class="fa-fw text-slate-400 mr-1"
/>
{{ total.amount }} {{ libsUtil.UNIT_LONG[total.unit] }}
</div>
</div>
</span>
</div>
<div v-else>
<span class="font-semibold mr-2 shrink-0">
{{ givesToThis.length }}{{ givesHitLimit ? "+" : "" }} record{{
givesToThis.length === 1 ? "" : "s"
}} }}
</span> </span>
<span v-if="give.amount" class="whitespace-nowrap">
<fa
:icon="libsUtil.iconForUnitCode(give.unit)"
class="fa-fw text-slate-400"
/>{{ give.amount }}
</span>
</div> </div>
<div class="text-slate-500"> </div>
<fa icon="calendar" class="fa-fw text-slate-400" />
{{ give.issuedAt?.substring(0, 10) }}
</div>
<div v-if="give.description" class="text-slate-500">
<fa icon="comment" class="fa-fw text-slate-400" />
{{ give.description }}
</div>
<div class="flex justify-between">
<a @click="onClickLoadClaim(give.jwtId)">
<fa icon="file-lines" class="text-blue-500 cursor-pointer" />
</a>
<a <!-- List of gives -->
v-if=" <ul class="mt-2 text-sm border-t border-slate-300">
checkIsConfirmable(give) && <li
!recentlyCheckedAndUnconfirmableJwts.includes(give.jwtId) v-for="give in givesToThis"
" :key="give.id"
@click="deepCheckConfirmable(give)" class="py-1.5 border-b border-slate-300"
> >
<fa icon="circle-check" class="text-blue-500 cursor-pointer" /> <div class="flex justify-between gap-4">
</a> <span>
<a v-else-if="checkingConfirmationForJwtId === give.jwtId"> <fa icon="user" class="fa-fw text-slate-400" />
<fa icon="spinner" class="fa-spin-pulse" /> {{
</a> serverUtil.didInfo(
<a v-else @click="shallowNotifyWhyCannotConfirm(give)"> give.agentDid,
<fa icon="circle-check" class="text-slate-500 cursor-pointer" /> activeDid,
</a> allMyDids,
</div> allContacts,
<div v-if="give.fullClaim.image" class="flex justify-center"> )
<a :href="give.fullClaim.image" target="_blank"> }}
<img :src="give.fullClaim.image" class="h-24 mt-2 rounded-xl" /> </span>
</a> <span v-if="give.amount" class="whitespace-nowrap">
</div> <fa
</li> :icon="libsUtil.iconForUnitCode(give.unit)"
</ul> class="fa-fw text-slate-400"
/>{{ give.amount }}
</span>
</div>
<div class="text-slate-500">
<fa icon="calendar" class="fa-fw text-slate-400" />
{{ give.issuedAt?.substring(0, 10) }}
</div>
<div v-if="give.description" class="text-slate-500">
<fa icon="comment" class="fa-fw text-slate-400" />
{{ give.description }}
</div>
<div class="flex justify-between">
<a @click="onClickLoadClaim(give.jwtId)">
<fa icon="file-lines" class="text-blue-500 cursor-pointer" />
</a>
<a
v-if="
checkIsConfirmable(give) &&
!recentlyCheckedAndUnconfirmableJwts.includes(give.jwtId)
"
@click="deepCheckConfirmable(give)"
>
<fa
icon="circle-check"
class="text-blue-500 cursor-pointer"
/>
</a>
<a v-else-if="checkingConfirmationForJwtId === give.jwtId">
<fa icon="spinner" class="fa-spin-pulse" />
</a>
<a v-else @click="shallowNotifyWhyCannotConfirm(give)">
<fa
icon="circle-check"
class="text-slate-500 cursor-pointer"
/>
</a>
</div>
<div v-if="give.fullClaim.image" class="flex justify-center">
<a :href="give.fullClaim.image" target="_blank">
<img
:src="give.fullClaim.image"
class="h-24 mt-2 rounded-xl"
/>
</a>
</div>
</li>
</ul>
</div>
<div v-if="givesHitLimit" class="text-center text-blue-500"> <div v-if="givesHitLimit" class="text-center text-blue-500">
<button @click="loadGives()">Load More</button> <button @click="loadGives()">Load More</button>
</div> </div>
@@ -405,7 +470,7 @@
<div class="text-center"> <div class="text-center">
<button <button
@click="openGiftDialogFromProject()" @click="openGiftDialogFromProject()"
class="block w-full 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 rounded-md" class="block w-full 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-3 py-1.5 text-sm leading-tight rounded-md"
> >
Given By This... Given By This...
</button> </button>
@@ -554,6 +619,7 @@ export default class ProjectViewView extends Vue {
givesHitLimit = false; givesHitLimit = false;
givesProvidedByThis: Array<GiveSummaryRecord> = []; givesProvidedByThis: Array<GiveSummaryRecord> = [];
givesProvidedByHitLimit = false; givesProvidedByHitLimit = false;
givesTotalsByUnit: Array<{ unit: string; amount: number }> = [];
imageUrl = ""; imageUrl = "";
isRegistered = false; isRegistered = false;
issuer = ""; issuer = "";
@@ -564,6 +630,7 @@ export default class ProjectViewView extends Vue {
} | null = null; } | null = null;
issuerVisibleToDids: Array<string> = []; issuerVisibleToDids: Array<string> = [];
latitude = 0; latitude = 0;
loadingTotals = false;
longitude = 0; longitude = 0;
name = ""; name = "";
offersToThis: Array<OfferSummaryRecord> = []; offersToThis: Array<OfferSummaryRecord> = [];
@@ -571,6 +638,7 @@ export default class ProjectViewView extends Vue {
projectId = ""; // handle ID projectId = ""; // handle ID
recentlyCheckedAndUnconfirmableJwts: string[] = []; recentlyCheckedAndUnconfirmableJwts: string[] = [];
startTime = ""; startTime = "";
totalsExpanded = false;
truncatedDesc = ""; truncatedDesc = "";
truncateLength = 40; truncateLength = 40;
url = ""; url = "";
@@ -609,6 +677,7 @@ export default class ProjectViewView extends Vue {
this.projectId = decodeURIComponent(pathParam); this.projectId = decodeURIComponent(pathParam);
} }
this.loadProject(this.projectId, this.activeDid); this.loadProject(this.projectId, this.activeDid);
this.loadTotals();
} }
onEditClick() { onEditClick() {
@@ -1207,5 +1276,56 @@ export default class ProjectViewView extends Vue {
this.allMyDids, this.allMyDids,
); );
} }
async loadTotals() {
this.loadingTotals = true;
const url =
this.apiServer +
"/api/v2/report/givesToPlans?planIds=" +
encodeURIComponent(JSON.stringify([this.projectId]));
const headers = await serverUtil.getHeaders(this.activeDid);
try {
const resp = await this.axios.get(url, { headers });
if (resp.status === 200 && resp.data.data) {
// Calculate totals by unit
const totals: { [key: string]: number } = {};
resp.data.data.forEach((give: GiveSummaryRecord) => {
const amount = give.fullClaim.object?.amountOfThisGood;
const unit = give.fullClaim.object?.unitCode;
if (amount && unit) {
totals[unit] = (totals[unit] || 0) + amount;
}
});
// Convert totals object to array format
this.givesTotalsByUnit = Object.entries(totals).map(
([unit, amount]) => ({
unit,
amount,
}),
);
}
} catch (error) {
console.error("Error loading totals:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to load totals for this project.",
},
5000,
);
} finally {
this.loadingTotals = false;
}
}
givenTotalHours(): number {
return (
this.givesTotalsByUnit.find((total) => total.unit === "HUR")?.amount || 0
);
}
} }
</script> </script>

View File

@@ -233,7 +233,7 @@
> >
<a <a
@click="onClickLoadProject(project.handleId)" @click="onClickLoadProject(project.handleId)"
class="block py-4 flex gap-4" class="block py-4 flex gap-4 cursor-pointer"
> >
<div class="flex-none"> <div class="flex-none">
<ProjectIcon <ProjectIcon

View File

@@ -48,7 +48,7 @@
*/ */
import path from 'path'; import path from 'path';
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import { importUser } from './testUtils'; import { importUserAndCloseOnboarding } from './testUtils';
/** /**
* Note: by default, this test uses the test image API server. * Note: by default, this test uses the test image API server.
@@ -65,7 +65,7 @@ test('Record item given from image-share', async ({ page }) => {
// Combine title prefix with the random string // Combine title prefix with the random string
const finalTitle = `Gift ${randomString} from image-share`; const finalTitle = `Gift ${randomString} from image-share`;
await importUser(page, '00'); await importUserAndCloseOnboarding(page, '00');
// Record something given // Record something given
await page.goto('./test'); await page.goto('./test');
@@ -84,10 +84,8 @@ test('Record item given from image-share', async ({ page }) => {
await page.getByRole('spinbutton').fill('2'); await page.getByRole('spinbutton').fill('2');
await page.getByRole('button', { name: 'Sign & Send' }).click(); await page.getByRole('button', { name: 'Sign & Send' }).click();
// we end up on a page with the onboarding info // const recorded = await page.getByText('That gift was recorded.');
await page.getByTestId('closeOnboardingAndFinish').click(); await expect(await page.getByText('That gift was recorded.')).toBeVisible();
await expect(page.getByText('That gift was recorded.')).toBeVisible();
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
// Refresh home view and check gift // Refresh home view and check gift

View File

@@ -207,7 +207,6 @@ test('Add contact, copy details, delete, and import from paste & from file', asy
// Add another new contact // Add another new contact
await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x222BB77E6Ff3774d34c751f3c1260866357B677b, User #222, asdf1234'); await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x222BB77E6Ff3774d34c751f3c1260866357B677b, User #222, asdf1234');
await page.locator('button > svg.fa-plus').click(); await page.locator('button > svg.fa-plus').click();
await expect(page.locator('div[role="alert"]')).toBeVisible();
await expect(page.locator('div[role="alert"] span:has-text("No")')).toBeVisible(); await expect(page.locator('div[role="alert"] span:has-text("No")')).toBeVisible();
await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register
await expect(page.locator('div[role="alert"] span:has-text("Contact Added")')).toBeVisible(); await expect(page.locator('div[role="alert"] span:has-text("Contact Added")')).toBeVisible();

View File

@@ -33,6 +33,13 @@ export async function importUser(page: Page, id?: string): Promise<string> {
return did; return did;
} }
export async function importUserAndCloseOnboarding(page: Page, id?: string): Promise<string> {
const did = await importUser(page, id);
await page.goto('./');
await page.getByTestId('closeOnboardingAndFinish').click();
return did;
}
// This is to switch to someone already in the identity table. It doesn't include registration. // This is to switch to someone already in the identity table. It doesn't include registration.
export async function switchToUser(page: Page, did: string): Promise<void> { export async function switchToUser(page: Page, did: string): Promise<void> {
// This is the direct approach but users have to tap on things so we'll do that instead. // This is the direct approach but users have to tap on things so we'll do that instead.