Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
4c326d2
Add demo mode with API mocks and enablement logic
talyguryn Jan 28, 2026
11ad11f
remove demo user
talyguryn Feb 7, 2026
e2bb138
Update .nvmrc
talyguryn Feb 7, 2026
a4bea5b
Update main.ts
talyguryn Feb 7, 2026
bd626b1
Update EventsList.vue
talyguryn Feb 7, 2026
34e7193
Update index.ts
talyguryn Feb 7, 2026
c10df9e
Update .nvmrc
talyguryn Feb 7, 2026
2d10b07
Update EventsList.vue
talyguryn Feb 7, 2026
445b011
Update main.ts
talyguryn Feb 7, 2026
2be3421
Add demo mode, mock DB and API mocks
talyguryn Feb 11, 2026
fb999e8
Centralize demo mocks and enhance withMockDemo
talyguryn Feb 11, 2026
1c2ebfb
Add time utils and tighten mock types
talyguryn Feb 11, 2026
0935337
Code style cleanup and small refactors
talyguryn Feb 11, 2026
3a12786
Use absolute mock paths and improve resolver
talyguryn Feb 11, 2026
3757741
Add demo composable and refactor demo mock loading
talyguryn Feb 11, 2026
4fde36e
Use @hawk.so/types and seconds-based timestamps
talyguryn Feb 11, 2026
2c98ecd
Add demo-banner and refactor demo mode handling
talyguryn Feb 18, 2026
7eee912
Fix i18n locale assignment and tidy events module
talyguryn Feb 18, 2026
ca5ac7b
Update index.js
talyguryn Feb 18, 2026
f28db0c
Use void to ignore returned promises
talyguryn Feb 18, 2026
a5c5e1d
Make demo composable shared and update usages
talyguryn Feb 21, 2026
99640f5
Refactor demo mode handling and i18n setup
talyguryn Feb 21, 2026
e44ad5e
Handle demo-mode and notification errors
talyguryn Feb 21, 2026
06838cb
Update index.js
talyguryn Feb 21, 2026
ab3cf38
chore(app-shell): improve demo-banner component
neSpecc Feb 21, 2026
63f9cd3
Use i18n for demo-mode unavailable message
talyguryn Feb 21, 2026
92586ce
Add notification mocks and wrap API with mocks
talyguryn Feb 21, 2026
80c8c24
Support filtering/sorting/search in mock events
talyguryn Feb 21, 2026
bd154b6
Add demo mocks and demo-mode API wrappers
talyguryn Feb 21, 2026
c9d1e64
Rename withMockDemo to withDemoMock
talyguryn Feb 21, 2026
e519d2f
Mock: generate chart data from DEMO_EVENTS
talyguryn Feb 21, 2026
26cb90b
Support multiple demo projects in mocks
talyguryn Feb 21, 2026
c467ffb
Update utils.ts
talyguryn Mar 6, 2026
b53a89c
Update router.ts
talyguryn Mar 6, 2026
2f89ab6
Update ru.json
talyguryn Mar 6, 2026
6cfcaca
Update getBusinessOperations.mock.ts
talyguryn Mar 6, 2026
dd0c5a3
Expose computed isDemoActive from useDemo
talyguryn Mar 6, 2026
e15bc90
Update useDemo.ts
talyguryn Mar 6, 2026
6e25595
Update index.ts
talyguryn Mar 6, 2026
02c1d40
Export demo tokens and centralize usage
talyguryn Mar 6, 2026
a5ce864
Update index.ts
talyguryn Mar 6, 2026
e251671
Export getBalance and remove demo mock
talyguryn Mar 6, 2026
a1e2bf6
Update getAllWorkspacesWithProjects.mock.ts
talyguryn Mar 6, 2026
a5cbce6
Export real getPlans and remove demo mock
talyguryn Mar 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"@amplitude/analytics-types": "^2.10.0",
"@codexteam/ui": "0.2.0-rc.4",
"@hawk.so/javascript": "3.2.13",
"@hawk.so/types": "^0.5.2",
"@hawk.so/types": "^0.5.9",
"axios": "^1.12.2",
"codex-notifier": "^1.1.2",
"cssnano": "^7.1.1",
Expand Down
8 changes: 7 additions & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,19 @@
import * as api from './api/';
import { setLanguage } from './i18n';
import { defineComponent } from 'vue';
import { useDemo } from './composables/useDemo';
import FeedbackButton from './components/utils/FeedbackButton.vue';

export default defineComponent({
name: 'App',
components: {
FeedbackButton,
},
setup() {
const { isDemoActive } = useDemo();

return { isDemoActive };
},
computed: {
/**
* Returns classname according to the theme name
Expand All @@ -39,7 +45,7 @@ export default defineComponent({
this.$store.watch(
state => state.user.accessToken,
(accessToken) => {
if (!accessToken) {
if (!accessToken && !this.isDemoActive) {
this.$router.push('/login');
}
api.setAuthToken(accessToken);
Expand Down
8 changes: 7 additions & 1 deletion src/api/billing/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { MUTATION_PAY_WITH_CARD, QUERY_BUSINESS_OPERATIONS, QUERY_COMPOSE_PAYMENT } from './queries';
import * as api from '../';
import { withDemoMock } from '@/utils/withDemoMock';
import type { BusinessOperation } from '../../types/business-operation';
import { BeforePaymentPayload } from '@/types/before-payment-payload';

/**
* Request business operations list for passed workspaces
* @param ids - ids of workspaces
*/
export async function getBusinessOperations(ids: string[]): Promise<BusinessOperation[]> {
async function getBusinessOperationsRequest(ids: string[]): Promise<BusinessOperation[]> {
return (await api.callOld(QUERY_BUSINESS_OPERATIONS, { ids })).businessOperations;
}

export const getBusinessOperations = withDemoMock(
getBusinessOperationsRequest,
'/src/api/billing/mocks/getBusinessOperations.mock.ts'
);

/**
* Data for processing payment with saved card
*/
Expand Down
15 changes: 15 additions & 0 deletions src/api/billing/mocks/getBusinessOperations.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { BusinessOperation } from '@/types/business-operation';
import { DEMO_WORKSPACE_ID, DEMO_BUSINESS_OPERATIONS } from '@/api/mock-db';

/**
* Mock: getBusinessOperations
*
* Returns demo business operations (payment history) for specified workspace IDs
*/
export default function mockGetBusinessOperations(ids: string[]): BusinessOperation[] {
// Return demo operations if requesting for demo workspace
if (ids.includes(DEMO_WORKSPACE_ID)) {
return DEMO_BUSINESS_OPERATIONS;
}
return [];
Comment on lines +9 to +14
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mock hard-codes the demo workspace id string. Since DEMO_WORKSPACE_ID is already defined in @/api/mock-db, using the shared constant here would prevent drift if the demo ids ever change.

Copilot uses AI. Check for mistakes.
}
172 changes: 94 additions & 78 deletions src/api/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import type { User } from '@/types/user';
import type { EventChartItem, ChartLine } from '@/types/chart';
import type { APIResponse } from '../../types/api';
import { withDemoMock } from '@/utils/withDemoMock';

/**
* Get specific event
Expand All @@ -30,19 +31,21 @@ import type { APIResponse } from '../../types/api';
* @param originalEventId - id of the original event
* @returns
*/
export async function getEvent(projectId: string, eventId: string, originalEventId: string): Promise<HawkEvent | null> {
const project = await (await api.callOld(QUERY_EVENT, {
projectId,
eventId,
originalEventId,
})).project;
export const getEvent = withDemoMock(
async function getEvent(projectId: string, eventId: string, originalEventId: string): Promise<HawkEvent | null> {
const project = await (await api.callOld(QUERY_EVENT, {
projectId,
eventId,
originalEventId,
})).project;

if (!project) {
return null;
}

return project.event;
}
if (!project) {
return null;
}
return project.event;
},
'/src/api/events/mocks/getEvent.mock.ts'
);

/**
* Returns portion (list) of daily events with pointer to the first daily event of the next portion
Expand All @@ -53,38 +56,40 @@ export async function getEvent(projectId: string, eventId: string, originalEvent
* @param search - search string for daily events
* @param release - release identifier to filter events
*/
export async function fetchDailyEventsPortion(
projectId: string,
nextCursor: DailyEventsCursor | null = null,
export const fetchDailyEventsPortion = withDemoMock(
async function fetchDailyEventsPortion(
projectId: string,
nextCursor: DailyEventsCursor | null = null,
sort = EventsSortOrder.ByDate,
filters: EventsFilters = {},
search = '',
release?: string
): Promise<DailyEventsPortion> {
const response = await api.call(QUERY_PROJECT_DAILY_EVENTS, {
projectId,
cursor: nextCursor,
sort,
filters,
search,
release,
}, undefined, {
): Promise<DailyEventsPortion> {
const response = await api.call(QUERY_PROJECT_DAILY_EVENTS, {
projectId,
cursor: nextCursor,
sort,
filters,
search,
release,
}, undefined, {
/**
* This request calls on the app start, so we don't want to break app if something goes wrong
* With this flag, errors from the API won't be thrown, but returned in the response for further handling
*/
allowErrors: true,
});
allowErrors: true,
});

const project = response.data.project;
const project = response.data.project;

if (response.errors?.length) {
response.errors.forEach(e => console.error(e));
}

return project?.dailyEventsPortion ?? { cursor: null,
dailyEventsPortion: [] };
}
if (response.errors?.length) {
response.errors.forEach(e => console.error(e));
}
return project?.dailyEventsPortion ?? { nextCursor: null,
dailyEvents: [] };
},
'/src/api/events/mocks/fetchDailyEventsPortion.mock.ts'
);

/**
* Fetches event's repetitions portion from project
Expand All @@ -94,56 +99,64 @@ export async function fetchDailyEventsPortion(
* @param cursor - the cursor to fetch the next page of repetitions
* @returns
*/
export async function getRepetitionsPortion(
projectId: string, originalEventId: string, limit: number, cursor?: string
): Promise<APIResponse<{ project: { event: { repetitionsPortion: { repetitions: HawkEvent[];
nextCursor?: string; }; }; }; }>> {
const response = await api.call(QUERY_EVENT_REPETITIONS_PORTION, {
limit,
projectId,
originalEventId,
cursor,
}, undefined, {
export const getRepetitionsPortion = withDemoMock(
async function getRepetitionsPortion(
projectId: string, originalEventId: string, limit: number, cursor?: string
): Promise<APIResponse<{ project: { event: { repetitionsPortion: { repetitions: HawkEvent[];
nextCursor?: string; }; }; }; }>> {
const response = await api.call(QUERY_EVENT_REPETITIONS_PORTION, {
limit,
projectId,
originalEventId,
cursor,
}, undefined, {
/**
* This request calls on the app start, so we don't want to break app if something goes wrong
* With this flag, errors from the API won't be thrown, but returned in the response for further handling
*/
allowErrors: true,
});
allowErrors: true,
});

if (response.errors?.length) {
response.errors.forEach(e => console.error(e));
}

return response;
}
if (response.errors?.length) {
response.errors.forEach(e => console.error(e));
}
return response;
},
'/src/api/events/mocks/getRepetitionsPortion.mock.ts'
);

/**
* Mark event as visited for current user
* @param projectId - project event related to
* @param originalEventId — original event id of the visited one
* @returns
*/
export async function visitEvent(projectId: string, originalEventId: string): Promise<boolean> {
return (await api.callOld(MUTATION_VISIT_EVENT, {
projectId,
originalEventId,
})).visitEvent;
}
export const visitEvent = withDemoMock(
async function visitEvent(projectId: string, originalEventId: string): Promise<boolean> {
return (await api.callOld(MUTATION_VISIT_EVENT, {
projectId,
originalEventId,
})).visitEvent;
},
'/src/api/events/mocks/visitEvent.mock.ts'
);

/**
* Set or unset mark to event
* @param projectId - project event is related to
* @param eventId — event Id
* @param mark — mark to set
*/
export async function toggleEventMark(projectId: string, eventId: string, mark: EventMark): Promise<boolean> {
return (await api.callOld(MUTATION_TOGGLE_EVENT_MARK, {
projectId,
eventId,
mark,
})).toggleEventMark;
}
export const toggleEventMark = withDemoMock(
async function toggleEventMark(projectId: string, eventId: string, mark: EventMark): Promise<boolean> {
return (await api.callOld(MUTATION_TOGGLE_EVENT_MARK, {
projectId,
eventId,
mark,
})).toggleEventMark;
},
'/src/api/events/mocks/toggleEventMark.mock.ts'
);

/**
* Update assignee
Expand Down Expand Up @@ -183,16 +196,19 @@ export async function removeAssignee(projectId: string, eventId: string): Promis
* @param days - how many days we need to fetch for displaying in chart
* @param timezoneOffset - user's local timezone
*/
export async function fetchChartData(
projectId: string,
originalEventId: string,
days: number,
timezoneOffset: number
): Promise<ChartLine[]> {
return (await api.callOld(QUERY_CHART_DATA, {
projectId,
originalEventId,
days,
timezoneOffset,
})).project.event.chartData;
}
export const fetchChartData = withDemoMock(
async function fetchChartData(
projectId: string,
originalEventId: string,
days: number,
timezoneOffset: number
): Promise<ChartLine[]> {
return (await api.callOld(QUERY_CHART_DATA, {
projectId,
originalEventId,
days,
timezoneOffset,
})).project.event.chartData;
},
'/src/api/events/mocks/fetchChartData.mock.ts'
);
68 changes: 68 additions & 0 deletions src/api/events/mocks/fetchChartData.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { getDemoEventsByProjectId } from '@/api/mock-db';
import type { ChartLine } from '@hawk.so/types';

const SECONDS_IN_MINUTE = 60;
const SECONDS_IN_DAY = 24 * 60 * 60;

function alignToDay(timestamp: number, timezoneOffset = 0): number {
const shiftedTimestamp = timestamp - timezoneOffset * SECONDS_IN_MINUTE;

return Math.floor(shiftedTimestamp / SECONDS_IN_DAY) * SECONDS_IN_DAY + timezoneOffset * SECONDS_IN_MINUTE;
}

/**
* Mock: fetchChartData (events)
*
* Returns chart data from mock-db
*/
export default function mockFetchChartData(
projectId: string,
originalEventId: string,
days = 14,
timezoneOffset = 0
): ChartLine[] {
const nowSeconds = Math.floor(Date.now() / 1000);
const fromTimestamp = nowSeconds - Math.max(1, days) * SECONDS_IN_DAY;
const buckets = new Map<number, number>();

for (let day = 0; day <= days; day++) {
const dayTimestamp = alignToDay(fromTimestamp + day * SECONDS_IN_DAY, timezoneOffset);

buckets.set(dayTimestamp, 0);
}

const projectEvents = getDemoEventsByProjectId(projectId);
const anchorEvent = projectEvents.find(event => event.originalEventId === originalEventId);

const relatedEvents = projectEvents.filter((event) => {
if (event.timestamp < fromTimestamp || event.timestamp > nowSeconds) {
return false;
}

if (event.originalEventId === originalEventId) {
return true;
}

if (anchorEvent && event.payload.title === anchorEvent.payload.title) {
return true;
}

return false;
});

relatedEvents.forEach((event) => {
const dayBucket = alignToDay(event.timestamp, timezoneOffset);

buckets.set(dayBucket, (buckets.get(dayBucket) || 0) + event.totalCount);
});

return [
{
label: 'accepted',
data: Array.from(buckets.entries()).map(([timestamp, count]) => ({
timestamp,
count,
})),
},
];
}
Loading