Browse Source

add page for user profile view and update endpoints; rename any "rowid" to "rowId"

pull/124/head
Trent Larson 3 weeks ago
parent
commit
5763fe4e49
  1. 7
      src/libs/endorserServer.ts
  2. 2
      src/libs/partnerServer.ts
  3. 5
      src/router/index.ts
  4. 186
      src/views/AccountViewView.vue
  5. 75
      src/views/DiscoverView.vue
  6. 6
      src/views/ProjectsView.vue
  7. 151
      src/views/UserProfileView.vue

7
src/libs/endorserServer.ts

@ -191,7 +191,7 @@ export interface PlanVerifiableCredential extends GenericVerifiableCredential {
* Represents data about a project * Represents data about a project
* *
* @deprecated * @deprecated
* We should use PlanSummaryRecord instead, adding rowid to it. * (Maybe we should use PlanSummaryRecord instead, either by adding rowId or by iterating with jwtId.)
**/ **/
export interface PlanData { export interface PlanData {
/** /**
@ -212,9 +212,10 @@ export interface PlanData {
**/ **/
name: string; name: string;
/** /**
* The identifier of the project -- different from jwtId, needs to be fixed * The identifier of the project record -- different from jwtId
* (Maybe we should use the jwtId to iterate through the records instead.)
**/ **/
rowid?: string; rowId?: string;
} }
export interface EndorserRateLimits { export interface EndorserRateLimits {

2
src/libs/partnerServer.ts

@ -3,5 +3,5 @@ export interface UserProfile {
locLat?: number; locLat?: number;
locLon?: number; locLon?: number;
issuerDid: string; issuerDid: string;
rowid?: string; rowId?: string; // set on profile retrieved from server
} }

5
src/router/index.ts

@ -258,6 +258,11 @@ const routes: Array<RouteRecordRaw> = [
name: "test", name: "test",
component: () => import("../views/TestView.vue"), component: () => import("../views/TestView.vue"),
}, },
{
path: "/userProfile/:id?",
name: "userProfile",
component: () => import("../views/UserProfileView.vue"),
},
]; ];
/** @type {*} */ /** @type {*} */

186
src/views/AccountViewView.vue

@ -158,7 +158,7 @@
We'll just pop the message in only if we discover that they need it. We'll just pop the message in only if we discover that they need it.
--> -->
<div <div
v-if="!loadingLimits && !endorserLimits?.nextWeekBeginDateTime" v-if="!isRegistered"
id="noticeBeforeAnnounce" id="noticeBeforeAnnounce"
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mt-4" class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mt-4"
> >
@ -175,6 +175,7 @@
</div> </div>
<div <div
v-if="isRegistered"
id="sectionNotifications" id="sectionNotifications"
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8" class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
> >
@ -262,7 +263,10 @@
</div> </div>
<!-- User Profile --> <!-- User Profile -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"> <div
v-if="isRegistered"
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
>
<div v-if="loadingProfile" class="text-center mb-2"> <div v-if="loadingProfile" class="text-center mb-2">
<fa icon="spinner" class="fa-spin text-slate-400"></fa> Loading <fa icon="spinner" class="fa-spin text-slate-400"></fa> Loading
profile... profile...
@ -321,22 +325,39 @@
/> />
</l-map> </l-map>
</div> </div>
<button <div v-if="!loadingProfile && !savingProfile">
@click="saveProfile" <div class="flex justify-between items-center">
class="mt-2 px-4 py-2 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md" <button
:disabled="loadingProfile || savingProfile" @click="saveProfile"
:class="{ class="mt-2 px-4 py-2 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
'opacity-50 cursor-not-allowed': loadingProfile || savingProfile, :disabled="loadingProfile || savingProfile"
}" :class="{
> 'opacity-50 cursor-not-allowed': loadingProfile || savingProfile,
{{ }"
loadingProfile >
? "Loading..." Save Profile
: savingProfile </button>
? "Saving..." <button
: "Save Profile" @click="confirmDeleteProfile"
}} class="mt-2 px-4 py-2 bg-gradient-to-b from-red-400 to-red-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
</button> :disabled="loadingProfile || savingProfile"
:class="{
'opacity-50 cursor-not-allowed':
loadingProfile ||
savingProfile ||
(!userProfileDesc && !includeUserProfileLocation),
}"
>
Delete Profile
</button>
</div>
</div>
<div v-else-if="loadingProfile">
Loading...
</div>
<div v-else="savingProfile">
Saving...
</div>
</div> </div>
<div <div
@ -1014,40 +1035,46 @@ export default class AccountViewView extends Vue {
await this.processIdentity(); await this.processIdentity();
// Load the user profile // Load the user profile
try { if (this.isRegistered) {
const headers = await getHeaders(this.activeDid); try {
const response = await this.axios.get( const headers = await getHeaders(this.activeDid);
this.apiServer + "/api/partner/userProfile/" + this.activeDid, const response = await this.axios.get(
{ headers }, this.apiServer +
); "/api/partner/userProfileForIssuer/" +
if (response.status === 200) { this.activeDid,
this.userProfileDesc = response.data.description || ""; { headers },
this.userProfileLatitude = response.data.locLat || 0;
this.userProfileLongitude = response.data.locLon || 0;
if (this.userProfileLatitude && this.userProfileLongitude) {
this.includeUserProfileLocation = true;
}
} else {
// won't get here because axios throws an error instead
throw Error("Unable to load profile.");
}
} catch (error) {
if (error.status === 404) {
// this is ok: the profile is not yet created
} else {
logConsoleAndDb("Error loading profile: " + errorStringForLog(error));
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Loading Profile",
text: "Your server profile is not available.",
},
5000,
); );
if (response.status === 200) {
this.userProfileDesc = response.data.data.description || "";
this.userProfileLatitude = response.data.data.locLat || 0;
this.userProfileLongitude = response.data.data.locLon || 0;
if (this.userProfileLatitude && this.userProfileLongitude) {
this.includeUserProfileLocation = true;
}
} else {
// won't get here because axios throws an error instead
throw Error("Unable to load profile.");
}
} catch (error) {
if (error.status === 404) {
// this is ok: the profile is not yet created
} else {
logConsoleAndDb(
"Error loading profile: " + errorStringForLog(error),
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Loading Profile",
text: "Your server profile is not available.",
},
5000,
);
}
} finally {
this.loadingProfile = false;
} }
} finally {
this.loadingProfile = false;
} }
} catch (error) { } catch (error) {
// this can happen when running automated tests in dev mode because notifications don't work // this can happen when running automated tests in dev mode because notifications don't work
@ -1877,5 +1904,64 @@ export default class AccountViewView extends Vue {
this.zoom = 2; this.zoom = 2;
this.includeUserProfileLocation = false; this.includeUserProfileLocation = false;
} }
async confirmDeleteProfile() {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Delete Profile",
text: "Are you sure you want to delete your public profile? This will remove your description and location from the server, and it cannot be undone.",
onYes: this.deleteProfile,
},
-1,
);
}
async deleteProfile() {
this.savingProfile = true;
try {
const headers = await getHeaders(this.activeDid);
const response = await this.axios.delete(
this.apiServer + "/api/partner/userProfile",
{ headers },
);
if (response.status === 204) {
this.userProfileDesc = "";
this.userProfileLatitude = 0;
this.userProfileLongitude = 0;
this.includeUserProfileLocation = false;
this.$notify(
{
group: "alert",
type: "success",
title: "Profile Deleted",
text: "Your profile has been deleted successfully.",
},
3000,
);
} else {
throw Error("Profile not deleted");
}
} catch (error) {
logConsoleAndDb("Error deleting profile: " + errorStringForLog(error));
const errorMessage: string =
error.response?.data?.error?.message ||
error.response?.data?.error ||
error.message ||
"There was an error deleting your profile.";
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Deleting Profile",
text: errorMessage,
},
3000,
);
} finally {
this.savingProfile = false;
}
}
} }
</script> </script>

75
src/views/DiscoverView.vue

@ -184,7 +184,10 @@
> >
<fa icon="spinner" class="fa-spin-pulse"></fa> <fa icon="spinner" class="fa-spin-pulse"></fa>
</div> </div>
<div v-else-if="projects.length === 0 && userProfiles.length === 0" class="text-center mt-8"> <div
v-else-if="projects.length === 0 && userProfiles.length === 0"
class="text-center mt-8"
>
<p class="text-lg text-slate-500"> <p class="text-lg text-slate-500">
<span v-if="isLocalActive"> <span v-if="isLocalActive">
<span v-if="searchBox"> None found in the selected area. </span> <span v-if="searchBox"> None found in the selected area. </span>
@ -224,7 +227,12 @@
<div class="text-sm"> <div class="text-sm">
<fa icon="user" class="fa-fw text-slate-400"></fa> <fa icon="user" class="fa-fw text-slate-400"></fa>
{{ {{
didInfo(project.issuerDid, activeDid, allMyDids, allContacts) didInfo(
project.issuerDid,
activeDid,
allMyDids,
allContacts,
)
}} }}
</div> </div>
</div> </div>
@ -240,22 +248,38 @@
:key="profile.issuerDid" :key="profile.issuerDid"
> >
<a <a
@click="onClickLoadItem(profile.issuerDid)" @click="onClickLoadItem(profile?.rowId || '')"
class="block py-4 flex gap-4 cursor-pointer" class="block py-4 flex gap-4 cursor-pointer"
> >
<div class="grow"> <div class="grow">
<div class="text-sm"> <div class="text-sm">
<fa icon="user" class="fa-fw text-slate-400"></fa> <fa icon="user" class="fa-fw text-slate-400"></fa>
{{ {{
didInfo(profile.issuerDid, activeDid, allMyDids, allContacts) didInfo(
profile.issuerDid,
activeDid,
allMyDids,
allContacts,
)
}} }}
</div> </div>
<p v-if="profile.description" class="mt-1 text-sm text-slate-600"> <p
v-if="profile.description"
class="mt-1 text-sm text-slate-600"
>
{{ profile.description }} {{ profile.description }}
</p> </p>
<div v-if="profile.locLat && profile.locLon" class="mt-1 text-xs text-slate-500"> <div
v-if="isAnywhereActive && profile.locLat && profile.locLon"
class="mt-1 text-xs text-slate-500"
>
<fa icon="location-dot" class="fa-fw"></fa> <fa icon="location-dot" class="fa-fw"></fa>
{{ profile.locLat.toFixed(2) }}, {{ profile.locLon.toFixed(2) }} {{
(profile.locLat > 0 ? "North" : "South") +
" in " +
(profile.locLon > 0 ? "Eastern" : "Western") +
" Hemisphere"
}}
</div> </div>
</div> </div>
</a> </a>
@ -432,17 +456,19 @@ export default class DiscoverView extends Vue {
const results = await response.json(); const results = await response.json();
if (this.isProjectsActive) { if (this.isProjectsActive) {
this.userProfiles = [];
const plans: PlanData[] = results.data; const plans: PlanData[] = results.data;
if (plans) { if (plans) {
for (const plan of plans) { for (const plan of plans) {
const { name, description, handleId, image, issuerDid, rowid } = plan; const { name, description, handleId, image, issuerDid, rowId } =
plan;
this.projects.push({ this.projects.push({
name, name,
description, description,
handleId, handleId,
image, image,
issuerDid, issuerDid,
rowid, rowId,
}); });
} }
this.remoteCount = this.projects.length; this.remoteCount = this.projects.length;
@ -450,6 +476,7 @@ export default class DiscoverView extends Vue {
throw JSON.stringify(results); throw JSON.stringify(results);
} }
} else { } else {
this.projects = [];
const profiles: UserProfile[] = results.data; const profiles: UserProfile[] = results.data;
if (profiles) { if (profiles) {
this.userProfiles.push(...profiles); this.userProfiles.push(...profiles);
@ -468,7 +495,11 @@ export default class DiscoverView extends Vue {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error Searching", title: "Error Searching",
text: e.userMessage || "There was a problem retrieving " + (this.isProjectsActive ? "projects" : "profiles") + ".", text:
e.userMessage ||
"There was a problem retrieving " +
(this.isProjectsActive ? "projects" : "profiles") +
".",
}, },
5000, 5000,
); );
@ -530,17 +561,18 @@ export default class DiscoverView extends Vue {
const results = await response.json(); const results = await response.json();
if (this.isProjectsActive) { if (this.isProjectsActive) {
this.userProfiles = [];
if (results.data) { if (results.data) {
if (beforeId) { if (beforeId) {
const plans: PlanData[] = results.data; const plans: PlanData[] = results.data;
for (const plan of plans) { for (const plan of plans) {
const { name, description, handleId, issuerDid, rowid } = plan; const { name, description, handleId, issuerDid, rowId } = plan;
this.projects.push({ this.projects.push({
name, name,
description, description,
handleId, handleId,
issuerDid, issuerDid,
rowid, rowId,
}); });
} }
} else { } else {
@ -551,6 +583,7 @@ export default class DiscoverView extends Vue {
throw JSON.stringify(results); throw JSON.stringify(results);
} }
} else { } else {
this.projects = [];
const profiles: UserProfile[] = results.data; const profiles: UserProfile[] = results.data;
if (profiles) { if (profiles) {
this.userProfiles.push(...profiles); this.userProfiles.push(...profiles);
@ -567,7 +600,11 @@ export default class DiscoverView extends Vue {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: e.userMessage || "There was a problem retrieving " + (this.isProjectsActive ? "projects" : "profiles") + ".", text:
e.userMessage ||
"There was a problem retrieving " +
(this.isProjectsActive ? "projects" : "profiles") +
".",
}, },
5000, 5000,
); );
@ -585,16 +622,16 @@ export default class DiscoverView extends Vue {
if (this.isProjectsActive && this.projects.length > 0) { if (this.isProjectsActive && this.projects.length > 0) {
const latestProject = this.projects[this.projects.length - 1]; const latestProject = this.projects[this.projects.length - 1];
if (this.isLocalActive || this.isMappedActive) { if (this.isLocalActive || this.isMappedActive) {
this.searchLocal(latestProject.rowid); this.searchLocal(latestProject.rowId);
} else if (this.isAnywhereActive) { } else if (this.isAnywhereActive) {
this.searchAll(latestProject.rowid); this.searchAll(latestProject.rowId);
} }
} else if (!this.isProjectsActive && this.userProfiles.length > 0) { } else if (!this.isProjectsActive && this.userProfiles.length > 0) {
const latestProfile = this.userProfiles[this.userProfiles.length - 1]; const latestProfile = this.userProfiles[this.userProfiles.length - 1];
if (this.isLocalActive || this.isMappedActive) { if (this.isLocalActive || this.isMappedActive) {
this.searchLocal(latestProfile.rowid || ""); this.searchLocal(latestProfile.rowId || "");
} else if (this.isAnywhereActive) { } else if (this.isAnywhereActive) {
this.searchAll(latestProfile.rowid || ""); this.searchAll(latestProfile.rowId || "");
} }
} }
} }
@ -653,7 +690,7 @@ export default class DiscoverView extends Vue {
this.markers = {}; this.markers = {};
const results = await response.json(); const results = await response.json();
if (results.data?.tiles?.length > 0) { if (results.data?.tiles?.length > 0) {
for (const tile of results.data.tiles) { for (const tile: Tile of results.data.tiles) {
const pinLat = (tile.minFoundLat + tile.maxFoundLat) / 2; const pinLat = (tile.minFoundLat + tile.maxFoundLat) / 2;
const pinLon = (tile.minFoundLon + tile.maxFoundLon) / 2; const pinLon = (tile.minFoundLon + tile.maxFoundLon) / 2;
const numberIcon = L.divIcon({ const numberIcon = L.divIcon({
@ -711,7 +748,7 @@ export default class DiscoverView extends Vue {
**/ **/
onClickLoadItem(id: string) { onClickLoadItem(id: string) {
const route = { const route = {
path: this.isProjectsActive path: this.isProjectsActive
? "/project/" + encodeURIComponent(id) ? "/project/" + encodeURIComponent(id)
: "/userProfile/" + encodeURIComponent(id), : "/userProfile/" + encodeURIComponent(id),
}; };

6
src/views/ProjectsView.vue

@ -361,14 +361,14 @@ export default class ProjectsView extends Vue {
if (resp.status === 200 && resp.data.data) { if (resp.status === 200 && resp.data.data) {
const plans: PlanData[] = resp.data.data; const plans: PlanData[] = resp.data.data;
for (const plan of plans) { for (const plan of plans) {
const { name, description, handleId, image, issuerDid, rowid } = plan; const { name, description, handleId, image, issuerDid, rowId } = plan;
this.projects.push({ this.projects.push({
name, name,
description, description,
image, image,
handleId, handleId,
issuerDid, issuerDid,
rowid, rowId,
}); });
} }
} else { } else {
@ -395,7 +395,7 @@ export default class ProjectsView extends Vue {
async loadMoreProjectData(payload: boolean) { async loadMoreProjectData(payload: boolean) {
if (this.projects.length > 0 && payload) { if (this.projects.length > 0 && payload) {
const latestProject = this.projects[this.projects.length - 1]; const latestProject = this.projects[this.projects.length - 1];
await this.loadProjects(`beforeId=${latestProject.rowid}`); await this.loadProjects(`beforeId=${latestProject.rowId}`);
} }
} }

151
src/views/UserProfileView.vue

@ -0,0 +1,151 @@
<template>
<QuickNav selected="Discover" />
<TopMessage />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<h1 id="ViewHeading" class="text-4xl text-center font-light">
Individual Profile
</h1>
<!-- Loading Animation -->
<div
class="fixed left-6 mt-16 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
v-if="isLoading"
>
<fa icon="spinner" class="fa-spin-pulse"></fa>
</div>
<div v-else-if="profile">
<!-- Profile Info -->
<div class="mt-8">
<div class="text-sm">
<fa icon="user" class="fa-fw text-slate-400"></fa>
{{ didInfo(profile.issuerDid, activeDid, allMyDids, allContacts) }}
</div>
<p v-if="profile.description" class="mt-4 text-slate-600">
{{ profile.description }}
</p>
</div>
<!-- Map -->
<div v-if="profile?.locLat && profile?.locLon" class="mt-4">
<h2 class="text-lg font-semibold">Location</h2>
<div class="h-96 mt-2 w-full">
<l-map
ref="profileMap"
:center="[profile.locLat, profile.locLon]"
:zoom="12"
>
<l-tile-layer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
layer-type="base"
name="OpenStreetMap"
/>
<l-marker :lat-lng="[profile.locLat, profile.locLon]">
<l-popup>{{
didInfo(profile.issuerDid, activeDid, allMyDids, allContacts)
}}</l-popup>
</l-marker>
</l-map>
</div>
</div>
</div>
<div v-else class="text-center mt-8">
<p class="text-lg text-slate-500">Profile not found.</p>
</div>
</section>
</template>
<script lang="ts">
import "leaflet/dist/leaflet.css";
import { Component, Vue } from "vue-facing-decorator";
import { LMap, LTileLayer, LMarker, LPopup } from "@vue-leaflet/vue-leaflet";
import { Router, RouteLocationNormalizedLoaded } from "vue-router";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { DEFAULT_PARTNER_API_SERVER, NotificationIface } from "@/constants/app";
import { db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { didInfo, getHeaders } from "@/libs/endorserServer";
import { UserProfile } from "@/libs/partnerServer";
import { retrieveAccountDids } from "@/libs/util";
@Component({
components: {
LMap,
LMarker,
LPopup,
LTileLayer,
QuickNav,
TopMessage,
},
})
export default class UserProfileView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
$route!: RouteLocationNormalizedLoaded;
activeDid = "";
allContacts: Array<Contact> = [];
allMyDids: Array<string> = [];
isLoading = true;
partnerApiServer = DEFAULT_PARTNER_API_SERVER;
profile: UserProfile | null = null;
// make this function available to the Vue template
didInfo = didInfo;
async mounted() {
const settings = await db.settings.toArray();
this.activeDid = settings[0]?.activeDid || "";
this.partnerApiServer =
settings[0]?.partnerApiServer || this.partnerApiServer;
this.allContacts = await db.contacts.toArray();
this.allMyDids = await retrieveAccountDids();
await this.loadProfile();
}
async loadProfile() {
const profileId: string = this.$route.params.id as string;
if (!profileId) {
this.isLoading = false;
return;
}
try {
const response = await fetch(
`${this.partnerApiServer}/api/partner/userProfile/${encodeURIComponent(profileId)}`,
{
method: "GET",
headers: await getHeaders(this.activeDid),
},
);
if (response.status === 200) {
const result = await response.json();
this.profile = result.data;
} else {
throw new Error("Failed to load profile");
}
} catch (error) {
console.error("Error loading profile:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem loading the profile.",
},
5000,
);
} finally {
this.isLoading = false;
}
}
}
</script>
Loading…
Cancel
Save