From da0621c09a3a473e07372530d21f21fb286203b5 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 9 Jul 2025 08:59:42 -0600 Subject: [PATCH] feat: Start the ability to star/bookmark a project. - Currently toggles & stores correctly locally on project. Does not show on other screens. --- package-lock.json | 12 ++++ package.json | 1 + src/db-sql/migration.ts | 6 ++ src/db/databaseUtil.ts | 15 +++-- src/db/tables/settings.ts | 5 ++ src/libs/fontawesome.ts | 8 ++- src/views/AccountViewView.vue | 5 +- src/views/ProjectViewView.vue | 114 +++++++++++++++++++++++++++++++--- 8 files changed, 145 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 693dac6f..b46584fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "@ethersproject/hdnode": "^5.7.0", "@ethersproject/wallet": "^5.8.0", "@fortawesome/fontawesome-svg-core": "^6.5.1", + "@fortawesome/free-regular-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/vue-fontawesome": "^3.0.6", "@jlongster/sql.js": "^1.6.7", @@ -6786,6 +6787,17 @@ "node": ">=6" } }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz", + "integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@fortawesome/free-solid-svg-icons": { "version": "6.7.2", "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz", diff --git a/package.json b/package.json index 9ca11deb..03290fe0 100644 --- a/package.json +++ b/package.json @@ -157,6 +157,7 @@ "@ethersproject/hdnode": "^5.7.0", "@ethersproject/wallet": "^5.8.0", "@fortawesome/fontawesome-svg-core": "^6.5.1", + "@fortawesome/free-regular-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/vue-fontawesome": "^3.0.6", "@jlongster/sql.js": "^1.6.7", diff --git a/src/db-sql/migration.ts b/src/db-sql/migration.ts index 67944b75..66227b61 100644 --- a/src/db-sql/migration.ts +++ b/src/db-sql/migration.ts @@ -124,6 +124,12 @@ const MIGRATIONS = [ ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE; `, }, + { + name: "003_add_starredProjectIds_to_settings", + sql: ` + ALTER TABLE settings ADD COLUMN starredProjectIds TEXT; + `, + }, ]; /** diff --git a/src/db/databaseUtil.ts b/src/db/databaseUtil.ts index 9b96475d..458d4e88 100644 --- a/src/db/databaseUtil.ts +++ b/src/db/databaseUtil.ts @@ -157,10 +157,8 @@ export async function retrieveSettingsForDefaultAccount(): Promise { result.columns, result.values, )[0] as Settings; - if (settings.searchBoxes) { - // @ts-expect-error - the searchBoxes field is a string in the DB - settings.searchBoxes = JSON.parse(settings.searchBoxes); - } + settings.searchBoxes = parseJsonField(settings.searchBoxes, []); + settings.starredProjectIds = parseJsonField(settings.starredProjectIds, []); return settings; } } @@ -226,10 +224,11 @@ export async function retrieveSettingsForActiveAccount(): Promise { ); } - // Handle searchBoxes parsing - if (settings.searchBoxes) { - settings.searchBoxes = parseJsonField(settings.searchBoxes, []); - } + settings.searchBoxes = parseJsonField(settings.searchBoxes, []); + settings.starredProjectIds = parseJsonField( + settings.starredProjectIds, + [], + ); return settings; } catch (error) { diff --git a/src/db/tables/settings.ts b/src/db/tables/settings.ts index 0b86e355..784612aa 100644 --- a/src/db/tables/settings.ts +++ b/src/db/tables/settings.ts @@ -60,6 +60,10 @@ export type Settings = { showContactGivesInline?: boolean; // Display contact inline or not showGeneralAdvanced?: boolean; // Show advanced features which don't have their own flag showShortcutBvc?: boolean; // Show shortcut for Bountiful Voluntaryist Community actions + + // List of starred project IDs, which are recommended to be handleIds + starredProjectIds?: Array; + vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push warnIfProdServer?: boolean; // Warn if using a production server warnIfTestServer?: boolean; // Warn if using a testing server @@ -69,6 +73,7 @@ export type Settings = { // type of settings where the searchBoxes are JSON strings instead of objects export type SettingsWithJsonStrings = Settings & { searchBoxes: string; + starredProjectIds: string; }; export function checkIsAnyFeedFilterOn(settings: Settings): boolean { diff --git a/src/libs/fontawesome.ts b/src/libs/fontawesome.ts index 30c745c7..efd8ff03 100644 --- a/src/libs/fontawesome.ts +++ b/src/libs/fontawesome.ts @@ -86,6 +86,7 @@ import { faSquareCaretDown, faSquareCaretUp, faSquarePlus, + faStar, faThumbtack, faTrashCan, faTriangleExclamation, @@ -94,6 +95,9 @@ import { faXmark, } from "@fortawesome/free-solid-svg-icons"; +// these are referenced differently, eg. ":icon='['far', 'star']'" as in ProjectViewView.vue +import { faStar as faStarRegular } from "@fortawesome/free-regular-svg-icons"; + // Initialize Font Awesome library with all required icons library.add( faArrowDown, @@ -168,14 +172,16 @@ library.add( faPlus, faQrcode, faQuestion, - faRotate, faRightFromBracket, + faRotate, faShareNodes, faSpinner, faSquare, faSquareCaretDown, faSquareCaretUp, faSquarePlus, + faStar, + faStarRegular, faThumbtack, faTrashCan, faTriangleExclamation, diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index e68efca2..bfcd3000 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -1059,8 +1059,8 @@ export default class AccountViewView extends Vue { this.hideRegisterPromptOnNewContact = !!settings.hideRegisterPromptOnNewContact; this.isRegistered = !!settings?.isRegistered; - this.isSearchAreasSet = !!settings.searchBoxes; - this.searchBox = settings.searchBoxes?.[0] || null; + this.isSearchAreasSet = + !!settings.searchBoxes && settings.searchBoxes.length > 0; this.notifyingNewActivity = !!settings.notifyingNewActivityTime; this.notifyingNewActivityTime = settings.notifyingNewActivityTime || ""; this.notifyingReminder = !!settings.notifyingReminderTime; @@ -1074,6 +1074,7 @@ export default class AccountViewView extends Vue { this.passkeyExpirationMinutes = settings.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES; this.previousPasskeyExpirationMinutes = this.passkeyExpirationMinutes; + this.searchBox = settings.searchBoxes?.[0] || null; this.showGeneralAdvanced = !!settings.showGeneralAdvanced; this.showShortcutBvc = !!settings.showShortcutBvc; this.warnIfProdServer = !!settings.warnIfProdServer; diff --git a/src/views/ProjectViewView.vue b/src/views/ProjectViewView.vue index 361c822f..99bb62ab 100644 --- a/src/views/ProjectViewView.vue +++ b/src/views/ProjectViewView.vue @@ -33,6 +33,20 @@ class="text-sm text-slate-500 ml-2 mb-1" /> + @@ -58,13 +72,13 @@ icon="user" class="fa-fw text-slate-400" > - + {{ issuerInfoObject?.displayName }} = []; imageUrl = ""; + /** Whether this project is starred by the user */ + isStarred = false; /** Project issuer DID */ issuer = ""; /** Cached issuer information */ @@ -805,6 +822,15 @@ export default class ProjectViewView extends Vue { } this.loadProject(this.projectId, this.activeDid); this.loadTotals(); + + // Check if this project is starred when settings are loaded + if (this.projectId && settings.starredProjectIds) { + const starredIds: string[] = databaseUtil.parseJsonField( + settings.starredProjectIds, + [], + ); + this.isStarred = starredIds.includes(this.projectId); + } } /** @@ -1470,5 +1496,73 @@ export default class ProjectViewView extends Vue { this.givesTotalsByUnit.find((total) => total.unit === "HUR")?.amount || 0 ); } + + /** + * Toggle the starred status of the current project + */ + async toggleStar() { + if (!this.projectId) return; + + try { + if (!this.isStarred) { + // Add to starred projects + const settings = await databaseUtil.retrieveSettingsForActiveAccount(); + const starredIds: string[] = databaseUtil.parseJsonField( + settings.starredProjectIds, + [], + ); + + if (!starredIds.includes(this.projectId)) { + const newStarredIds = [...starredIds, this.projectId]; + const newIdsParam = JSON.stringify(newStarredIds); + const result = await databaseUtil.updateDidSpecificSettings( + this.activeDid, + // @ts-expect-error until we use SettingsWithJsonString properly + { starredProjectIds: newIdsParam }, + ); + if (!result) { + // eslint-disable-next-line no-console + console.log( + "Still getting a bad result from SQL update to star a project.", + ); + } + } + this.isStarred = true; + } else { + // Remove from starred projects + const settings = await databaseUtil.retrieveSettingsForActiveAccount(); + const starredIds: string[] = databaseUtil.parseJsonField( + settings.starredProjectIds, + [], + ); + + const updatedIds = starredIds.filter((id) => id !== this.projectId); + const newIdsParam = JSON.stringify(updatedIds); + const result = await databaseUtil.updateDidSpecificSettings( + this.activeDid, + // @ts-expect-error until we use SettingsWithJsonString properly + { starredProjectIds: newIdsParam }, + ); + if (!result) { + // eslint-disable-next-line no-console + console.log( + "Still getting a bad result from SQL update to unstar a project.", + ); + } + this.isStarred = false; + } + } catch (error) { + logger.error("Error toggling star status:", error); + this.$notify( + { + group: "alert", + type: "danger", + title: "Error", + text: "Failed to update starred status. Please try again.", + }, + 3000, + ); + } + } }