forked from trent_larson/crowd-funder-for-time-pwa
feat: add starred projects at the top of the list to choose
This commit is contained in:
18
package-lock.json
generated
18
package-lock.json
generated
@@ -27,7 +27,7 @@
|
|||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
"@ethersproject/wallet": "^5.8.0",
|
"@ethersproject/wallet": "^5.8.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||||
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
"@fortawesome/free-brands-svg-icons": "^6.5.1",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.6",
|
"@fortawesome/vue-fontawesome": "^3.0.6",
|
||||||
@@ -6791,24 +6791,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fortawesome/free-brands-svg-icons": {
|
"node_modules/@fortawesome/free-brands-svg-icons": {
|
||||||
"version": "7.1.0",
|
"version": "6.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.7.2.tgz",
|
||||||
"integrity": "sha512-9byUd9bgNfthsZAjBl6GxOu1VPHgBuRUP9juI7ZoM98h8xNPTCTagfwUFyYscdZq4Hr7gD1azMfM9s5tIWKZZA==",
|
"integrity": "sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-common-types": "7.1.0"
|
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fortawesome/free-brands-svg-icons/node_modules/@fortawesome/fontawesome-common-types": {
|
|
||||||
"version": "7.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz",
|
|
||||||
"integrity": "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@fortawesome/free-regular-svg-icons": {
|
"node_modules/@fortawesome/free-regular-svg-icons": {
|
||||||
"version": "6.7.2",
|
"version": "6.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz",
|
||||||
|
|||||||
@@ -139,17 +139,62 @@ projects, and special entities with selection. * * @author Matthew Raymer */
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="entityType === 'projects'">
|
<template v-else-if="entityType === 'projects'">
|
||||||
<ProjectCard
|
<!-- When showing projects without search: split into recently bookmarked and rest -->
|
||||||
v-for="project in displayedEntities as PlanData[]"
|
<template v-if="!searchTerm.trim()">
|
||||||
:key="project.handleId"
|
<!-- Recently Bookmarked Section -->
|
||||||
:project="project"
|
<template v-if="recentBookmarkedProjects.length > 0">
|
||||||
:active-did="activeDid"
|
<li
|
||||||
:all-my-dids="allMyDids"
|
class="text-xs font-semibold text-slate-500 uppercase pt-5 pb-1.5 px-2 border-b border-slate-300"
|
||||||
:all-contacts="allContacts"
|
>
|
||||||
:notify="notify"
|
Recently Bookmarked
|
||||||
:conflict-context="conflictContext"
|
</li>
|
||||||
@project-selected="handleProjectSelected"
|
<ProjectCard
|
||||||
/>
|
v-for="project in recentBookmarkedProjects"
|
||||||
|
:key="project.handleId"
|
||||||
|
:project="project"
|
||||||
|
:active-did="activeDid"
|
||||||
|
:all-my-dids="allMyDids"
|
||||||
|
:all-contacts="allContacts"
|
||||||
|
:notify="notify"
|
||||||
|
:conflict-context="conflictContext"
|
||||||
|
@project-selected="handleProjectSelected"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Rest of Projects Section -->
|
||||||
|
<li
|
||||||
|
v-if="recentBookmarkedProjects.length > 0"
|
||||||
|
class="text-xs font-semibold text-slate-500 uppercase pt-5 pb-1.5 px-2 border-b border-slate-300"
|
||||||
|
>
|
||||||
|
All Projects
|
||||||
|
</li>
|
||||||
|
<ProjectCard
|
||||||
|
v-for="project in remainingProjects"
|
||||||
|
:key="project.handleId"
|
||||||
|
:project="project"
|
||||||
|
:active-did="activeDid"
|
||||||
|
:all-my-dids="allMyDids"
|
||||||
|
:all-contacts="allContacts"
|
||||||
|
:notify="notify"
|
||||||
|
:conflict-context="conflictContext"
|
||||||
|
@project-selected="handleProjectSelected"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- When searching: show filtered results normally -->
|
||||||
|
<template v-else>
|
||||||
|
<ProjectCard
|
||||||
|
v-for="project in displayedEntities as PlanData[]"
|
||||||
|
:key="project.handleId"
|
||||||
|
:project="project"
|
||||||
|
:active-did="activeDid"
|
||||||
|
:all-my-dids="allMyDids"
|
||||||
|
:all-contacts="allContacts"
|
||||||
|
:notify="notify"
|
||||||
|
:conflict-context="conflictContext"
|
||||||
|
@project-selected="handleProjectSelected"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
@@ -175,6 +220,7 @@ import { TIMEOUTS } from "@/utils/notify";
|
|||||||
const INITIAL_BATCH_SIZE = 20;
|
const INITIAL_BATCH_SIZE = 20;
|
||||||
const INCREMENT_SIZE = 20;
|
const INCREMENT_SIZE = 20;
|
||||||
const RECENT_CONTACTS_COUNT = 3;
|
const RECENT_CONTACTS_COUNT = 3;
|
||||||
|
const RECENT_BOOKMARKED_PROJECTS_COUNT = 3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EntityGrid - Unified grid layout for displaying people or projects
|
* EntityGrid - Unified grid layout for displaying people or projects
|
||||||
@@ -223,6 +269,9 @@ export default class EntityGrid extends Vue {
|
|||||||
infiniteScrollReset?: () => void;
|
infiniteScrollReset?: () => void;
|
||||||
scrollContainer?: HTMLElement;
|
scrollContainer?: HTMLElement;
|
||||||
|
|
||||||
|
// Starred projects state (for showing recently bookmarked projects)
|
||||||
|
starredPlanHandleIds: string[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Array of entities to display
|
* Array of entities to display
|
||||||
*
|
*
|
||||||
@@ -378,7 +427,8 @@ export default class EntityGrid extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the remaining contacts sorted alphabetically (when showing contacts and not searching)
|
* Get all contacts sorted alphabetically (when showing contacts and not searching)
|
||||||
|
* Includes contacts shown in "Recently Added" section as well
|
||||||
* Uses infinite scroll to control how many are displayed
|
* Uses infinite scroll to control how many are displayed
|
||||||
*/
|
*/
|
||||||
get alphabeticalContacts(): Contact[] {
|
get alphabeticalContacts(): Contact[] {
|
||||||
@@ -389,18 +439,68 @@ export default class EntityGrid extends Vue {
|
|||||||
) {
|
) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
// Skip the first few (recent contacts) and sort the rest alphabetically
|
// Sort all contacts alphabetically (including recent ones)
|
||||||
// Create a copy to avoid mutating the original array
|
// Create a copy to avoid mutating the original array
|
||||||
const remaining = this.entities as Contact[];
|
const sorted = [...(this.entities as Contact[])].sort(
|
||||||
const sorted = [...remaining].sort((a: Contact, b: Contact) => {
|
(a: Contact, b: Contact) => {
|
||||||
// Sort alphabetically by name, falling back to DID if name is missing
|
// Sort alphabetically by name, falling back to DID if name is missing
|
||||||
const nameA = (a.name || a.did).toLowerCase();
|
const nameA = (a.name || a.did).toLowerCase();
|
||||||
const nameB = (b.name || b.did).toLowerCase();
|
const nameB = (b.name || b.did).toLowerCase();
|
||||||
return nameA.localeCompare(nameB);
|
return nameA.localeCompare(nameB);
|
||||||
});
|
},
|
||||||
// Apply infinite scroll: show based on displayedCount (minus the recent contacts)
|
);
|
||||||
const toShow = Math.max(0, this.displayedCount - RECENT_CONTACTS_COUNT);
|
// Apply infinite scroll: show based on displayedCount
|
||||||
return sorted.slice(0, toShow);
|
return sorted.slice(0, this.displayedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the 3 most recently bookmarked projects (when showing projects and not searching)
|
||||||
|
* The starredPlanHandleIds array order represents bookmark order (newest at the end)
|
||||||
|
*/
|
||||||
|
get recentBookmarkedProjects(): PlanData[] {
|
||||||
|
if (
|
||||||
|
this.entityType !== "projects" ||
|
||||||
|
this.searchTerm.trim() ||
|
||||||
|
this.starredPlanHandleIds.length === 0
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const projects = this.entitiesToUse as PlanData[];
|
||||||
|
if (projects.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the last 3 starred IDs (most recently bookmarked)
|
||||||
|
const recentStarredIds = this.starredPlanHandleIds.slice(
|
||||||
|
-RECENT_BOOKMARKED_PROJECTS_COUNT,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find projects matching those IDs, preserving the order
|
||||||
|
const recentProjects = recentStarredIds
|
||||||
|
.map((id) => projects.find((p) => p.handleId === id))
|
||||||
|
.filter((p): p is PlanData => p !== undefined);
|
||||||
|
|
||||||
|
return recentProjects;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all projects (when showing projects and not searching)
|
||||||
|
* Includes projects shown in "Recently Bookmarked" section as well
|
||||||
|
* Uses infinite scroll to control how many are displayed
|
||||||
|
*/
|
||||||
|
get remainingProjects(): PlanData[] {
|
||||||
|
if (this.entityType !== "projects" || this.searchTerm.trim()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const projects = this.entitiesToUse as PlanData[];
|
||||||
|
if (projects.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply infinite scroll: show based on displayedCount
|
||||||
|
return projects.slice(0, this.displayedCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -793,12 +893,11 @@ export default class EntityGrid extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// People: check if more alphabetical contacts available
|
// People: check if more alphabetical contacts available
|
||||||
// Total available = recent + all alphabetical
|
// All contacts are shown alphabetically (recent ones appear in both sections)
|
||||||
if (!this.entities) {
|
if (!this.entities) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const totalAvailable = RECENT_CONTACTS_COUNT + this.entities.length;
|
return this.displayedCount < this.entities.length;
|
||||||
return this.displayedCount < totalAvailable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -810,6 +909,9 @@ export default class EntityGrid extends Vue {
|
|||||||
const settings = await this.$accountSettings();
|
const settings = await this.$accountSettings();
|
||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || "";
|
||||||
|
|
||||||
|
// Load starred project IDs for showing recently bookmarked projects
|
||||||
|
this.starredPlanHandleIds = settings.starredPlanHandleIds || [];
|
||||||
|
|
||||||
// Load projects on mount if entities prop not provided
|
// Load projects on mount if entities prop not provided
|
||||||
if (!this.entities && this.apiServer) {
|
if (!this.entities && this.apiServer) {
|
||||||
this.isLoadingProjects = true;
|
this.isLoadingProjects = true;
|
||||||
@@ -959,7 +1061,7 @@ export default class EntityGrid extends Vue {
|
|||||||
this.displayedCount = INITIAL_BATCH_SIZE;
|
this.displayedCount = INITIAL_BATCH_SIZE;
|
||||||
this.infiniteScrollReset?.();
|
this.infiniteScrollReset?.();
|
||||||
|
|
||||||
// For projects: if entities prop is provided, clear internal state
|
// For projects: clear internal state if entities prop is provided
|
||||||
if (this.entityType === "projects" && this.entities) {
|
if (this.entityType === "projects" && this.entities) {
|
||||||
this.allProjects = [];
|
this.allProjects = [];
|
||||||
this.loadBeforeId = undefined;
|
this.loadBeforeId = undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user