|
|
|
|
@@ -280,14 +280,41 @@ import OnboardingDialog from "../components/OnboardingDialog.vue";
|
|
|
|
|
import ProjectIcon from "../components/ProjectIcon.vue";
|
|
|
|
|
import TopMessage from "../components/TopMessage.vue";
|
|
|
|
|
import UserNameDialog from "../components/UserNameDialog.vue";
|
|
|
|
|
import * as databaseUtil from "../db/databaseUtil";
|
|
|
|
|
import { Contact } from "../db/tables/contacts";
|
|
|
|
|
import { didInfo, getHeaders, getPlanFromCache } from "../libs/endorserServer";
|
|
|
|
|
import { OfferSummaryRecord, PlanData } from "../interfaces/records";
|
|
|
|
|
import * as libsUtil from "../libs/util";
|
|
|
|
|
import { OnboardPage } from "../libs/util";
|
|
|
|
|
import { logger } from "../utils/logger";
|
|
|
|
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
|
|
|
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
|
|
|
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
|
|
|
|
import {
|
|
|
|
|
NOTIFY_NO_ACCOUNT_ERROR,
|
|
|
|
|
NOTIFY_PROJECT_LOAD_ERROR,
|
|
|
|
|
NOTIFY_PROJECT_INIT_ERROR,
|
|
|
|
|
NOTIFY_OFFERS_LOAD_ERROR,
|
|
|
|
|
NOTIFY_OFFERS_FETCH_ERROR,
|
|
|
|
|
NOTIFY_CAMERA_SHARE_METHOD,
|
|
|
|
|
} from "@/constants/notifications";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Projects View Component
|
|
|
|
|
*
|
|
|
|
|
* Main dashboard for managing user projects and offers within the TimeSafari platform.
|
|
|
|
|
* Provides dual-mode interface for viewing:
|
|
|
|
|
* - Personal projects: Ideas and plans created by the user
|
|
|
|
|
* - Active offers: Commitments made to help with other projects
|
|
|
|
|
*
|
|
|
|
|
* Key Features:
|
|
|
|
|
* - Infinite scrolling for large datasets
|
|
|
|
|
* - Project creation and navigation
|
|
|
|
|
* - Offer tracking with confirmation status
|
|
|
|
|
* - Onboarding integration for new users
|
|
|
|
|
* - Cross-platform compatibility (web, mobile, desktop)
|
|
|
|
|
*
|
|
|
|
|
* Security: All API calls are authenticated using user's DID
|
|
|
|
|
* Privacy: Only user's own projects and offers are displayed
|
|
|
|
|
*/
|
|
|
|
|
@Component({
|
|
|
|
|
components: {
|
|
|
|
|
EntityIcon,
|
|
|
|
|
@@ -298,18 +325,15 @@ import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
|
|
|
|
TopMessage,
|
|
|
|
|
UserNameDialog,
|
|
|
|
|
},
|
|
|
|
|
mixins: [PlatformServiceMixin],
|
|
|
|
|
})
|
|
|
|
|
export default class ProjectsView extends Vue {
|
|
|
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
|
|
|
$router!: Router;
|
|
|
|
|
|
|
|
|
|
errNote(message: string) {
|
|
|
|
|
this.$notify(
|
|
|
|
|
{ group: "alert", type: "danger", title: "Error", text: message },
|
|
|
|
|
5000,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
notify!: ReturnType<typeof createNotifyHelpers>;
|
|
|
|
|
|
|
|
|
|
// User account state
|
|
|
|
|
activeDid = "";
|
|
|
|
|
allContacts: Array<Contact> = [];
|
|
|
|
|
allMyDids: Array<string> = [];
|
|
|
|
|
@@ -317,61 +341,114 @@ export default class ProjectsView extends Vue {
|
|
|
|
|
givenName = "";
|
|
|
|
|
isLoading = false;
|
|
|
|
|
isRegistered = false;
|
|
|
|
|
|
|
|
|
|
// Data collections
|
|
|
|
|
offers: OfferSummaryRecord[] = [];
|
|
|
|
|
projectNameFromHandleId: Record<string, string> = {}; // mapping from handleId to description
|
|
|
|
|
projects: PlanData[] = [];
|
|
|
|
|
|
|
|
|
|
// UI state
|
|
|
|
|
showOffers = false;
|
|
|
|
|
showProjects = true;
|
|
|
|
|
|
|
|
|
|
// Utility imports
|
|
|
|
|
libsUtil = libsUtil;
|
|
|
|
|
didInfo = didInfo;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initializes notification helpers
|
|
|
|
|
*/
|
|
|
|
|
created() {
|
|
|
|
|
this.notify = createNotifyHelpers(this.$notify);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Component initialization
|
|
|
|
|
*
|
|
|
|
|
* Workflow:
|
|
|
|
|
* 1. Load user settings and account information
|
|
|
|
|
* 2. Load contacts for displaying offer recipients
|
|
|
|
|
* 3. Initialize onboarding dialog if needed
|
|
|
|
|
* 4. Load initial project data
|
|
|
|
|
*
|
|
|
|
|
* Error handling: Shows appropriate user messages for different failure scenarios
|
|
|
|
|
*/
|
|
|
|
|
async mounted() {
|
|
|
|
|
try {
|
|
|
|
|
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
|
|
|
|
this.activeDid = settings.activeDid || "";
|
|
|
|
|
this.apiServer = settings.apiServer || "";
|
|
|
|
|
this.isRegistered = !!settings.isRegistered;
|
|
|
|
|
this.givenName = settings.firstName || "";
|
|
|
|
|
|
|
|
|
|
const platformService = PlatformServiceFactory.getInstance();
|
|
|
|
|
const queryResult = await platformService.dbQuery(
|
|
|
|
|
"SELECT * FROM contacts",
|
|
|
|
|
);
|
|
|
|
|
this.allContacts = databaseUtil.mapQueryResultToValues(
|
|
|
|
|
queryResult,
|
|
|
|
|
) as unknown as Contact[];
|
|
|
|
|
|
|
|
|
|
this.allMyDids = await libsUtil.retrieveAccountDids();
|
|
|
|
|
|
|
|
|
|
if (!settings.finishedOnboarding) {
|
|
|
|
|
(this.$refs.onboardingDialog as OnboardingDialog).open(
|
|
|
|
|
OnboardPage.Create,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.allMyDids.length === 0) {
|
|
|
|
|
logger.error("No accounts found.");
|
|
|
|
|
this.errNote("You need an identifier to load your projects.");
|
|
|
|
|
} else {
|
|
|
|
|
await this.loadProjects();
|
|
|
|
|
}
|
|
|
|
|
await this.initializeUserSettings();
|
|
|
|
|
await this.loadContactsData();
|
|
|
|
|
await this.initializeUserIdentities();
|
|
|
|
|
await this.checkOnboardingStatus();
|
|
|
|
|
await this.loadInitialData();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
logger.error("Error initializing:", err);
|
|
|
|
|
this.errNote("Something went wrong loading your projects.");
|
|
|
|
|
logger.error("Error initializing ProjectsView:", err);
|
|
|
|
|
this.notify.error(NOTIFY_PROJECT_INIT_ERROR.message, TIMEOUTS.LONG);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Loads user settings from active account
|
|
|
|
|
*/
|
|
|
|
|
private async initializeUserSettings() {
|
|
|
|
|
const settings = await this.$accountSettings();
|
|
|
|
|
this.activeDid = settings.activeDid || "";
|
|
|
|
|
this.apiServer = settings.apiServer || "";
|
|
|
|
|
this.isRegistered = !!settings.isRegistered;
|
|
|
|
|
this.givenName = settings.firstName || "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Loads contacts data for displaying offer recipients
|
|
|
|
|
*/
|
|
|
|
|
private async loadContactsData() {
|
|
|
|
|
this.allContacts = await this.$getAllContacts();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initializes user identity information
|
|
|
|
|
*/
|
|
|
|
|
private async initializeUserIdentities() {
|
|
|
|
|
this.allMyDids = await libsUtil.retrieveAccountDids();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Checks if onboarding dialog should be shown
|
|
|
|
|
*/
|
|
|
|
|
private async checkOnboardingStatus() {
|
|
|
|
|
const settings = await this.$accountSettings();
|
|
|
|
|
if (!settings.finishedOnboarding) {
|
|
|
|
|
(this.$refs.onboardingDialog as OnboardingDialog).open(
|
|
|
|
|
OnboardPage.Create,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Loads initial project data if user has valid account
|
|
|
|
|
*/
|
|
|
|
|
private async loadInitialData() {
|
|
|
|
|
if (this.allMyDids.length === 0) {
|
|
|
|
|
logger.error("No accounts found for user");
|
|
|
|
|
this.notify.error(NOTIFY_NO_ACCOUNT_ERROR.message, TIMEOUTS.LONG);
|
|
|
|
|
} else {
|
|
|
|
|
await this.loadProjects();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Core project data loader
|
|
|
|
|
* @param url the url used to fetch the data
|
|
|
|
|
* @param token Authorization token
|
|
|
|
|
**/
|
|
|
|
|
*
|
|
|
|
|
* Fetches project data from the endorser server and populates the projects array.
|
|
|
|
|
* Handles authentication, error scenarios, and loading states.
|
|
|
|
|
*
|
|
|
|
|
* @param url - The API endpoint URL for fetching project data
|
|
|
|
|
*/
|
|
|
|
|
async projectDataLoader(url: string) {
|
|
|
|
|
try {
|
|
|
|
|
const headers = await getHeaders(this.activeDid, this.$notify);
|
|
|
|
|
this.isLoading = true;
|
|
|
|
|
const resp = await this.axios.get(url, { headers } as AxiosRequestConfig);
|
|
|
|
|
|
|
|
|
|
if (resp.status === 200 && resp.data.data) {
|
|
|
|
|
const plans: PlanData[] = resp.data.data;
|
|
|
|
|
for (const plan of plans) {
|
|
|
|
|
@@ -391,12 +468,11 @@ export default class ProjectsView extends Vue {
|
|
|
|
|
resp.status,
|
|
|
|
|
resp.data,
|
|
|
|
|
);
|
|
|
|
|
this.errNote("Failed to get projects from the server.");
|
|
|
|
|
this.notify.error(NOTIFY_PROJECT_LOAD_ERROR.message, TIMEOUTS.LONG);
|
|
|
|
|
}
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("Got error loading plans:", error.message || error);
|
|
|
|
|
this.errNote("Got an error loading projects.");
|
|
|
|
|
this.notify.error(NOTIFY_PROJECT_LOAD_ERROR.message, TIMEOUTS.LONG);
|
|
|
|
|
} finally {
|
|
|
|
|
this.isLoading = false;
|
|
|
|
|
}
|
|
|
|
|
@@ -404,8 +480,12 @@ export default class ProjectsView extends Vue {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Data loader used by infinite scroller
|
|
|
|
|
* @param payload is the flag from the InfiniteScroll indicating if it should load
|
|
|
|
|
**/
|
|
|
|
|
*
|
|
|
|
|
* Implements pagination by loading additional projects when user scrolls to bottom.
|
|
|
|
|
* Uses the last project's rowId as a cursor for the next batch.
|
|
|
|
|
*
|
|
|
|
|
* @param payload - Flag from InfiniteScroll component indicating if more data should be loaded
|
|
|
|
|
*/
|
|
|
|
|
async loadMoreProjectData(payload: boolean) {
|
|
|
|
|
if (this.projects.length > 0 && payload) {
|
|
|
|
|
const latestProject = this.projects[this.projects.length - 1];
|
|
|
|
|
@@ -414,19 +494,24 @@ export default class ProjectsView extends Vue {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Load projects initially
|
|
|
|
|
* @param issuerDid of the user
|
|
|
|
|
* @param urlExtra additional url parameters in a string
|
|
|
|
|
**/
|
|
|
|
|
* Load projects initially or with pagination
|
|
|
|
|
*
|
|
|
|
|
* Constructs the API URL for fetching user's projects and delegates to projectDataLoader.
|
|
|
|
|
*
|
|
|
|
|
* @param urlExtra - Additional URL parameters for pagination (e.g., "beforeId=123")
|
|
|
|
|
*/
|
|
|
|
|
async loadProjects(urlExtra: string = "") {
|
|
|
|
|
const url = `${this.apiServer}/api/v2/report/plansByIssuer?${urlExtra}`;
|
|
|
|
|
await this.projectDataLoader(url);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle clicking on a project entry found in the list
|
|
|
|
|
* @param id of the project
|
|
|
|
|
**/
|
|
|
|
|
* Handle clicking on a project entry
|
|
|
|
|
*
|
|
|
|
|
* Navigates to the detailed project view for the selected project.
|
|
|
|
|
*
|
|
|
|
|
* @param id - The unique identifier of the project to view
|
|
|
|
|
*/
|
|
|
|
|
onClickLoadProject(id: string) {
|
|
|
|
|
const route = {
|
|
|
|
|
path: "/project/" + encodeURIComponent(id),
|
|
|
|
|
@@ -435,8 +520,10 @@ export default class ProjectsView extends Vue {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handling clicking on the new project button
|
|
|
|
|
**/
|
|
|
|
|
* Handle clicking on the new project button
|
|
|
|
|
*
|
|
|
|
|
* Navigates to the project creation/editing interface.
|
|
|
|
|
*/
|
|
|
|
|
onClickNewProject(): void {
|
|
|
|
|
const route = {
|
|
|
|
|
name: "new-edit-project",
|
|
|
|
|
@@ -444,6 +531,13 @@ export default class ProjectsView extends Vue {
|
|
|
|
|
this.$router.push(route);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle clicking on a claim/offer link
|
|
|
|
|
*
|
|
|
|
|
* Navigates to the detailed claim view for the selected offer.
|
|
|
|
|
*
|
|
|
|
|
* @param jwtId - The JWT identifier of the claim to view
|
|
|
|
|
*/
|
|
|
|
|
onClickLoadClaim(jwtId: string) {
|
|
|
|
|
const route = {
|
|
|
|
|
path: "/claim/" + encodeURIComponent(jwtId),
|
|
|
|
|
@@ -453,17 +547,21 @@ export default class ProjectsView extends Vue {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Core offer data loader
|
|
|
|
|
* @param url the url used to fetch the data
|
|
|
|
|
* @param token Authorization token
|
|
|
|
|
**/
|
|
|
|
|
*
|
|
|
|
|
* Fetches offer data from the endorser server and populates the offers array.
|
|
|
|
|
* Also retrieves associated project names for display purposes.
|
|
|
|
|
*
|
|
|
|
|
* @param url - The API endpoint URL for fetching offer data
|
|
|
|
|
*/
|
|
|
|
|
async offerDataLoader(url: string) {
|
|
|
|
|
const headers = await getHeaders(this.activeDid);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
this.isLoading = true;
|
|
|
|
|
const resp = await this.axios.get(url, { headers } as AxiosRequestConfig);
|
|
|
|
|
|
|
|
|
|
if (resp.status === 200 && resp.data.data) {
|
|
|
|
|
// add one-by-one as they retrieve project names, potentially from the server
|
|
|
|
|
// Process offers one-by-one to retrieve project names from server cache
|
|
|
|
|
for (const offer of resp.data.data) {
|
|
|
|
|
if (offer.fulfillsPlanHandleId) {
|
|
|
|
|
const project = await getPlanFromCache(
|
|
|
|
|
@@ -484,37 +582,24 @@ export default class ProjectsView extends Vue {
|
|
|
|
|
resp.status,
|
|
|
|
|
resp.data,
|
|
|
|
|
);
|
|
|
|
|
this.$notify(
|
|
|
|
|
{
|
|
|
|
|
group: "alert",
|
|
|
|
|
type: "danger",
|
|
|
|
|
title: "Error",
|
|
|
|
|
text: "Failed to get offers from the server.",
|
|
|
|
|
},
|
|
|
|
|
5000,
|
|
|
|
|
);
|
|
|
|
|
this.notify.error(NOTIFY_OFFERS_LOAD_ERROR.message, TIMEOUTS.LONG);
|
|
|
|
|
}
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error("Got error loading offers:", error.message || error);
|
|
|
|
|
this.$notify(
|
|
|
|
|
{
|
|
|
|
|
group: "alert",
|
|
|
|
|
type: "danger",
|
|
|
|
|
title: "Error",
|
|
|
|
|
text: "Got an error loading offers.",
|
|
|
|
|
},
|
|
|
|
|
5000,
|
|
|
|
|
);
|
|
|
|
|
this.notify.error(NOTIFY_OFFERS_FETCH_ERROR.message, TIMEOUTS.LONG);
|
|
|
|
|
} finally {
|
|
|
|
|
this.isLoading = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Data loader used by infinite scroller
|
|
|
|
|
* @param payload is the flag from the InfiniteScroll indicating if it should load
|
|
|
|
|
**/
|
|
|
|
|
* Data loader used by infinite scroller for offers
|
|
|
|
|
*
|
|
|
|
|
* Implements pagination by loading additional offers when user scrolls to bottom.
|
|
|
|
|
* Uses the last offer's jwtId as a cursor for the next batch.
|
|
|
|
|
*
|
|
|
|
|
* @param payload - Flag from InfiniteScroll component indicating if more data should be loaded
|
|
|
|
|
*/
|
|
|
|
|
async loadMoreOfferData(payload: boolean) {
|
|
|
|
|
if (this.offers.length > 0 && payload) {
|
|
|
|
|
const latestOffer = this.offers[this.offers.length - 1];
|
|
|
|
|
@@ -523,15 +608,23 @@ export default class ProjectsView extends Vue {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Load offers initially
|
|
|
|
|
* @param issuerDid of the user
|
|
|
|
|
* @param urlExtra additional url parameters in a string
|
|
|
|
|
**/
|
|
|
|
|
* Load offers initially or with pagination
|
|
|
|
|
*
|
|
|
|
|
* Constructs the API URL for fetching user's offers and delegates to offerDataLoader.
|
|
|
|
|
*
|
|
|
|
|
* @param urlExtra - Additional URL parameters for pagination (e.g., "&beforeId=123")
|
|
|
|
|
*/
|
|
|
|
|
async loadOffers(urlExtra: string = "") {
|
|
|
|
|
const url = `${this.apiServer}/api/v2/report/offers?offeredByDid=${this.activeDid}${urlExtra}`;
|
|
|
|
|
await this.offerDataLoader(url);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Shows name dialog if needed, then prompts for share method
|
|
|
|
|
*
|
|
|
|
|
* Ensures user has provided their name before proceeding with contact sharing.
|
|
|
|
|
* Uses UserNameDialog component if name is not set.
|
|
|
|
|
*/
|
|
|
|
|
showNameThenIdDialog() {
|
|
|
|
|
if (!this.givenName) {
|
|
|
|
|
(this.$refs.userNameDialog as UserNameDialog).open(() => {
|
|
|
|
|
@@ -542,13 +635,23 @@ export default class ProjectsView extends Vue {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Prompts user to choose contact sharing method
|
|
|
|
|
*
|
|
|
|
|
* Presents modal dialog asking if users are nearby with cameras.
|
|
|
|
|
* Routes to appropriate sharing method based on user's choice:
|
|
|
|
|
* - QR code sharing for nearby users with cameras
|
|
|
|
|
* - Alternative sharing methods for remote users
|
|
|
|
|
*
|
|
|
|
|
* Note: Uses raw $notify for complex modal with custom buttons and onNo callback
|
|
|
|
|
*/
|
|
|
|
|
promptForShareMethod() {
|
|
|
|
|
this.$notify(
|
|
|
|
|
{
|
|
|
|
|
group: "modal",
|
|
|
|
|
type: "confirm",
|
|
|
|
|
title: "Are you nearby with cameras?",
|
|
|
|
|
text: "If so, we'll use those with QR codes to share.",
|
|
|
|
|
title: NOTIFY_CAMERA_SHARE_METHOD.title,
|
|
|
|
|
text: NOTIFY_CAMERA_SHARE_METHOD.text,
|
|
|
|
|
onCancel: async () => {},
|
|
|
|
|
onNo: async () => {
|
|
|
|
|
this.$router.push({ name: "share-my-contact-info" });
|
|
|
|
|
@@ -556,49 +659,68 @@ export default class ProjectsView extends Vue {
|
|
|
|
|
onYes: async () => {
|
|
|
|
|
this.handleQRCodeClick();
|
|
|
|
|
},
|
|
|
|
|
noText: "we will share another way",
|
|
|
|
|
yesText: "we are nearby with cameras",
|
|
|
|
|
noText: NOTIFY_CAMERA_SHARE_METHOD.noText,
|
|
|
|
|
yesText: NOTIFY_CAMERA_SHARE_METHOD.yesText,
|
|
|
|
|
},
|
|
|
|
|
-1,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public computedOfferTabClassNames() {
|
|
|
|
|
/**
|
|
|
|
|
* Computed properties for template logic streamlining
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* CSS class names for offer tab styling
|
|
|
|
|
* @returns Object with CSS classes based on current tab selection
|
|
|
|
|
*/
|
|
|
|
|
get offerTabClasses() {
|
|
|
|
|
return {
|
|
|
|
|
"inline-block": true,
|
|
|
|
|
"py-3": true,
|
|
|
|
|
"rounded-t-lg": true,
|
|
|
|
|
"border-b-2": true,
|
|
|
|
|
|
|
|
|
|
active: this.showOffers,
|
|
|
|
|
"text-black": this.showOffers,
|
|
|
|
|
"border-black": this.showOffers,
|
|
|
|
|
"font-semibold": this.showOffers,
|
|
|
|
|
|
|
|
|
|
"text-blue-600": !this.showOffers,
|
|
|
|
|
"border-transparent": !this.showOffers,
|
|
|
|
|
"hover:border-slate-400": !this.showOffers,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public computedProjectTabClassNames() {
|
|
|
|
|
/**
|
|
|
|
|
* CSS class names for project tab styling
|
|
|
|
|
* @returns Object with CSS classes based on current tab selection
|
|
|
|
|
*/
|
|
|
|
|
get projectTabClasses() {
|
|
|
|
|
return {
|
|
|
|
|
"inline-block": true,
|
|
|
|
|
"py-3": true,
|
|
|
|
|
"rounded-t-lg": true,
|
|
|
|
|
"border-b-2": true,
|
|
|
|
|
|
|
|
|
|
active: this.showProjects,
|
|
|
|
|
"text-black": this.showProjects,
|
|
|
|
|
"border-black": this.showProjects,
|
|
|
|
|
"font-semibold": this.showProjects,
|
|
|
|
|
|
|
|
|
|
"text-blue-600": !this.showProjects,
|
|
|
|
|
"border-transparent": !this.showProjects,
|
|
|
|
|
"hover:border-slate-400": !this.showProjects,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Utility methods
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handles QR code sharing functionality with platform detection
|
|
|
|
|
*
|
|
|
|
|
* Routes to appropriate QR code interface based on current platform:
|
|
|
|
|
* - Full QR scanner for native mobile platforms
|
|
|
|
|
* - Web-based QR interface for browser environments
|
|
|
|
|
*/
|
|
|
|
|
private handleQRCodeClick() {
|
|
|
|
|
if (Capacitor.isNativePlatform()) {
|
|
|
|
|
this.$router.push({ name: "contact-qr-scan-full" });
|
|
|
|
|
@@ -606,5 +728,21 @@ export default class ProjectsView extends Vue {
|
|
|
|
|
this.$router.push({ name: "contact-qr" });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Legacy method compatibility
|
|
|
|
|
* @deprecated Use computedOfferTabClassNames for backward compatibility
|
|
|
|
|
*/
|
|
|
|
|
public computedOfferTabClassNames() {
|
|
|
|
|
return this.offerTabClasses;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Legacy method compatibility
|
|
|
|
|
* @deprecated Use computedProjectTabClassNames for backward compatibility
|
|
|
|
|
*/
|
|
|
|
|
public computedProjectTabClassNames() {
|
|
|
|
|
return this.projectTabClasses;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|