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. 114
      src/views/AccountViewView.vue
  5. 73
      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
*
* @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 {
/**
@ -212,9 +212,10 @@ export interface PlanData {
**/
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 {

2
src/libs/partnerServer.ts

@ -3,5 +3,5 @@ export interface UserProfile {
locLat?: number;
locLon?: number;
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",
component: () => import("../views/TestView.vue"),
},
{
path: "/userProfile/:id?",
name: "userProfile",
component: () => import("../views/UserProfileView.vue"),
},
];
/** @type {*} */

114
src/views/AccountViewView.vue

@ -158,7 +158,7 @@
We'll just pop the message in only if we discover that they need it.
-->
<div
v-if="!loadingLimits && !endorserLimits?.nextWeekBeginDateTime"
v-if="!isRegistered"
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"
>
@ -175,6 +175,7 @@
</div>
<div
v-if="isRegistered"
id="sectionNotifications"
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
>
@ -262,7 +263,10 @@
</div>
<!-- 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">
<fa icon="spinner" class="fa-spin text-slate-400"></fa> Loading
profile...
@ -321,6 +325,8 @@
/>
</l-map>
</div>
<div v-if="!loadingProfile && !savingProfile">
<div class="flex justify-between items-center">
<button
@click="saveProfile"
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"
@ -329,15 +335,30 @@
'opacity-50 cursor-not-allowed': loadingProfile || savingProfile,
}"
>
{{
loadingProfile
? "Loading..."
: savingProfile
? "Saving..."
: "Save Profile"
}}
Save Profile
</button>
<button
@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"
: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
v-if="activeDid"
@ -1014,16 +1035,19 @@ export default class AccountViewView extends Vue {
await this.processIdentity();
// Load the user profile
if (this.isRegistered) {
try {
const headers = await getHeaders(this.activeDid);
const response = await this.axios.get(
this.apiServer + "/api/partner/userProfile/" + this.activeDid,
this.apiServer +
"/api/partner/userProfileForIssuer/" +
this.activeDid,
{ headers },
);
if (response.status === 200) {
this.userProfileDesc = response.data.description || "";
this.userProfileLatitude = response.data.locLat || 0;
this.userProfileLongitude = response.data.locLon || 0;
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;
}
@ -1035,7 +1059,9 @@ export default class AccountViewView extends Vue {
if (error.status === 404) {
// this is ok: the profile is not yet created
} else {
logConsoleAndDb("Error loading profile: " + errorStringForLog(error));
logConsoleAndDb(
"Error loading profile: " + errorStringForLog(error),
);
this.$notify(
{
group: "alert",
@ -1049,6 +1075,7 @@ export default class AccountViewView extends Vue {
} finally {
this.loadingProfile = false;
}
}
} catch (error) {
// this can happen when running automated tests in dev mode because notifications don't work
console.error(
@ -1877,5 +1904,64 @@ export default class AccountViewView extends Vue {
this.zoom = 2;
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>

73
src/views/DiscoverView.vue

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

6
src/views/ProjectsView.vue

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