Browse Source

feat: Start the ability to star/bookmark a project.

- Currently toggles & stores correctly locally on project. Does not show on other screens.
pull/193/head
Trent Larson 2 months ago
parent
commit
da0621c09a
  1. 12
      package-lock.json
  2. 1
      package.json
  3. 6
      src/db-sql/migration.ts
  4. 15
      src/db/databaseUtil.ts
  5. 5
      src/db/tables/settings.ts
  6. 8
      src/libs/fontawesome.ts
  7. 5
      src/views/AccountViewView.vue
  8. 114
      src/views/ProjectViewView.vue

12
package-lock.json

@ -27,6 +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-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",
"@jlongster/sql.js": "^1.6.7", "@jlongster/sql.js": "^1.6.7",
@ -6786,6 +6787,17 @@
"node": ">=6" "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": { "node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.7.2", "version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",

1
package.json

@ -157,6 +157,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-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",
"@jlongster/sql.js": "^1.6.7", "@jlongster/sql.js": "^1.6.7",

6
src/db-sql/migration.ts

@ -124,6 +124,12 @@ const MIGRATIONS = [
ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE; ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE;
`, `,
}, },
{
name: "003_add_starredProjectIds_to_settings",
sql: `
ALTER TABLE settings ADD COLUMN starredProjectIds TEXT;
`,
},
]; ];
/** /**

15
src/db/databaseUtil.ts

@ -157,10 +157,8 @@ export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
result.columns, result.columns,
result.values, result.values,
)[0] as Settings; )[0] as Settings;
if (settings.searchBoxes) { settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
// @ts-expect-error - the searchBoxes field is a string in the DB settings.starredProjectIds = parseJsonField(settings.starredProjectIds, []);
settings.searchBoxes = JSON.parse(settings.searchBoxes);
}
return settings; return settings;
} }
} }
@ -226,10 +224,11 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
); );
} }
// Handle searchBoxes parsing settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
if (settings.searchBoxes) { settings.starredProjectIds = parseJsonField(
settings.searchBoxes = parseJsonField(settings.searchBoxes, []); settings.starredProjectIds,
} [],
);
return settings; return settings;
} catch (error) { } catch (error) {

5
src/db/tables/settings.ts

@ -60,6 +60,10 @@ export type Settings = {
showContactGivesInline?: boolean; // Display contact inline or not showContactGivesInline?: boolean; // Display contact inline or not
showGeneralAdvanced?: boolean; // Show advanced features which don't have their own flag showGeneralAdvanced?: boolean; // Show advanced features which don't have their own flag
showShortcutBvc?: boolean; // Show shortcut for Bountiful Voluntaryist Community actions showShortcutBvc?: boolean; // Show shortcut for Bountiful Voluntaryist Community actions
// List of starred project IDs, which are recommended to be handleIds
starredProjectIds?: Array<string>;
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
warnIfProdServer?: boolean; // Warn if using a production server warnIfProdServer?: boolean; // Warn if using a production server
warnIfTestServer?: boolean; // Warn if using a testing 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 // type of settings where the searchBoxes are JSON strings instead of objects
export type SettingsWithJsonStrings = Settings & { export type SettingsWithJsonStrings = Settings & {
searchBoxes: string; searchBoxes: string;
starredProjectIds: string;
}; };
export function checkIsAnyFeedFilterOn(settings: Settings): boolean { export function checkIsAnyFeedFilterOn(settings: Settings): boolean {

8
src/libs/fontawesome.ts

@ -86,6 +86,7 @@ import {
faSquareCaretDown, faSquareCaretDown,
faSquareCaretUp, faSquareCaretUp,
faSquarePlus, faSquarePlus,
faStar,
faThumbtack, faThumbtack,
faTrashCan, faTrashCan,
faTriangleExclamation, faTriangleExclamation,
@ -94,6 +95,9 @@ import {
faXmark, faXmark,
} from "@fortawesome/free-solid-svg-icons"; } 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 // Initialize Font Awesome library with all required icons
library.add( library.add(
faArrowDown, faArrowDown,
@ -168,14 +172,16 @@ library.add(
faPlus, faPlus,
faQrcode, faQrcode,
faQuestion, faQuestion,
faRotate,
faRightFromBracket, faRightFromBracket,
faRotate,
faShareNodes, faShareNodes,
faSpinner, faSpinner,
faSquare, faSquare,
faSquareCaretDown, faSquareCaretDown,
faSquareCaretUp, faSquareCaretUp,
faSquarePlus, faSquarePlus,
faStar,
faStarRegular,
faThumbtack, faThumbtack,
faTrashCan, faTrashCan,
faTriangleExclamation, faTriangleExclamation,

5
src/views/AccountViewView.vue

@ -1059,8 +1059,8 @@ export default class AccountViewView extends Vue {
this.hideRegisterPromptOnNewContact = this.hideRegisterPromptOnNewContact =
!!settings.hideRegisterPromptOnNewContact; !!settings.hideRegisterPromptOnNewContact;
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
this.isSearchAreasSet = !!settings.searchBoxes; this.isSearchAreasSet =
this.searchBox = settings.searchBoxes?.[0] || null; !!settings.searchBoxes && settings.searchBoxes.length > 0;
this.notifyingNewActivity = !!settings.notifyingNewActivityTime; this.notifyingNewActivity = !!settings.notifyingNewActivityTime;
this.notifyingNewActivityTime = settings.notifyingNewActivityTime || ""; this.notifyingNewActivityTime = settings.notifyingNewActivityTime || "";
this.notifyingReminder = !!settings.notifyingReminderTime; this.notifyingReminder = !!settings.notifyingReminderTime;
@ -1074,6 +1074,7 @@ export default class AccountViewView extends Vue {
this.passkeyExpirationMinutes = this.passkeyExpirationMinutes =
settings.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES; settings.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES;
this.previousPasskeyExpirationMinutes = this.passkeyExpirationMinutes; this.previousPasskeyExpirationMinutes = this.passkeyExpirationMinutes;
this.searchBox = settings.searchBoxes?.[0] || null;
this.showGeneralAdvanced = !!settings.showGeneralAdvanced; this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
this.showShortcutBvc = !!settings.showShortcutBvc; this.showShortcutBvc = !!settings.showShortcutBvc;
this.warnIfProdServer = !!settings.warnIfProdServer; this.warnIfProdServer = !!settings.warnIfProdServer;

114
src/views/ProjectViewView.vue

@ -33,6 +33,20 @@
class="text-sm text-slate-500 ml-2 mb-1" class="text-sm text-slate-500 ml-2 mb-1"
/> />
</button> </button>
<button
:title="
isStarred
? 'Remove from starred projects'
: 'Add to starred projects'
"
@click="toggleStar()"
>
<font-awesome
:icon="isStarred ? 'star' : ['far', 'star']"
:class="isStarred ? 'text-yellow-500' : 'text-slate-500'"
class="text-sm ml-2 mb-1"
/>
</button>
</h2> </h2>
</div> </div>
</div> </div>
@ -58,13 +72,13 @@
icon="user" icon="user"
class="fa-fw text-slate-400" class="fa-fw text-slate-400"
></font-awesome> ></font-awesome>
<span class="truncate inline-block max-w-[calc(100%-2rem)]"> <span class="truncate max-w-[calc(100%-2rem)] ml-1">
{{ issuerInfoObject?.displayName }} {{ issuerInfoObject?.displayName }}
</span> </span>
<span <span
v-if="!serverUtil.isHiddenDid(issuer)" v-if="!serverUtil.isHiddenDid(issuer)"
class="inline-flex items-center" class="inline-flex items-center ml-1"
> >
<router-link <router-link
:to="{ :to="{
@ -593,6 +607,8 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router"; import { Router } from "vue-router";
import { useClipboard } from "@vueuse/core";
import { import {
GenericVerifiableCredential, GenericVerifiableCredential,
GenericCredWrapper, GenericCredWrapper,
@ -603,25 +619,24 @@ import {
PlanSummaryRecord, PlanSummaryRecord,
} from "../interfaces"; } from "../interfaces";
import GiftedDialog from "../components/GiftedDialog.vue"; import GiftedDialog from "../components/GiftedDialog.vue";
import HiddenDidDialog from "../components/HiddenDidDialog.vue";
import OfferDialog from "../components/OfferDialog.vue"; import OfferDialog from "../components/OfferDialog.vue";
import TopMessage from "../components/TopMessage.vue"; import TopMessage from "../components/TopMessage.vue";
import QuickNav from "../components/QuickNav.vue"; import QuickNav from "../components/QuickNav.vue";
import EntityIcon from "../components/EntityIcon.vue"; import EntityIcon from "../components/EntityIcon.vue";
import ProjectIcon from "../components/ProjectIcon.vue"; import ProjectIcon from "../components/ProjectIcon.vue";
import { NotificationIface } from "../constants/app"; import { APP_SERVER, NotificationIface } from "../constants/app";
// Removed legacy logging import - migrated to PlatformServiceMixin import { UNNAMED_PROJECT } from "../constants/entities";
import { NOTIFY_CONFIRM_CLAIM } from "../constants/notifications";
import * as databaseUtil from "../db/databaseUtil";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
import * as libsUtil from "../libs/util"; import * as libsUtil from "../libs/util";
import * as serverUtil from "../libs/endorserServer"; import * as serverUtil from "../libs/endorserServer";
import { retrieveAccountDids } from "../libs/util"; import { retrieveAccountDids } from "../libs/util";
import HiddenDidDialog from "../components/HiddenDidDialog.vue"; import { logger } from "@/utils/logger";
import { logger } from "../utils/logger";
import { useClipboard } from "@vueuse/core";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { NOTIFY_CONFIRM_CLAIM } from "@/constants/notifications";
import { APP_SERVER } from "@/constants/app";
import { UNNAMED_PROJECT } from "@/constants/entities";
/** /**
* Project View Component * Project View Component
* @author Matthew Raymer * @author Matthew Raymer
@ -718,6 +733,8 @@ export default class ProjectViewView extends Vue {
givesProvidedByHitLimit = false; givesProvidedByHitLimit = false;
givesTotalsByUnit: Array<{ unit: string; amount: number }> = []; givesTotalsByUnit: Array<{ unit: string; amount: number }> = [];
imageUrl = ""; imageUrl = "";
/** Whether this project is starred by the user */
isStarred = false;
/** Project issuer DID */ /** Project issuer DID */
issuer = ""; issuer = "";
/** Cached issuer information */ /** Cached issuer information */
@ -805,6 +822,15 @@ export default class ProjectViewView extends Vue {
} }
this.loadProject(this.projectId, this.activeDid); this.loadProject(this.projectId, this.activeDid);
this.loadTotals(); 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 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,
);
}
}
} }
</script> </script>

Loading…
Cancel
Save