diff --git a/dashboard/components/CVerticalNavigation.vue b/dashboard/components/CVerticalNavigation.vue index d9233bb..407ebc7 100644 --- a/dashboard/components/CVerticalNavigation.vue +++ b/dashboard/components/CVerticalNavigation.vue @@ -134,11 +134,11 @@ const pricingDrawer = usePricingDrawer(); }">
-
+
diff --git a/dashboard/components/dashboard/ActionableChart.vue b/dashboard/components/dashboard/ActionableChart.vue index d416a04..3759ef2 100644 --- a/dashboard/components/dashboard/ActionableChart.vue +++ b/dashboard/components/dashboard/ActionableChart.vue @@ -106,11 +106,12 @@ const externalTooltipElement = ref(null); function externalTooltipHandler(context: { chart: any, tooltip: TooltipModel<'line' | 'bar'> }) { const { chart, tooltip } = context; const tooltipEl = externalTooltipElement.value; - - currentTooltipData.value = [0,0,0,'']; - currentTooltipData.value.push(...tooltip.dataPoints.map(e => e.raw) as number[]); - currentTooltipData.value[2] = ((tooltip.dataPoints[2]?.raw as any)?.r2 || 0) as number; - currentTooltipData.value[3] = new Date(allDatesFull.value[tooltip.dataPoints[0].dataIndex]).toLocaleDateString(); + + currentTooltipData.value.visits = (tooltip.dataPoints.find(e=> e.datasetIndex == 0)?.raw) as number; + currentTooltipData.value.sessions = (tooltip.dataPoints.find(e=> e.datasetIndex == 1)?.raw) as number; + currentTooltipData.value.events = ((tooltip.dataPoints.find(e=> e.datasetIndex == 2)?.raw) as any)?.r2 as number; + + currentTooltipData.value.date = new Date(allDatesFull.value[tooltip.dataPoints[0].dataIndex]).toLocaleDateString(); if (!tooltipEl) return; if (tooltip.opacity === 0) { @@ -147,7 +148,6 @@ function transformResponse(input: { _id: string, count: number }[]) { } const body = computed(() => { - console.log('INDEX IS', selectedLabelIndex.value, 'VALUE IS', selectLabels[selectedLabelIndex.value].value) return { from: safeSnapshotDates.value.from, to: safeSnapshotDates.value.to, @@ -214,7 +214,8 @@ function onDataReady() { chartData.value.datasets[0].data = visitsData.data.value.data; chartData.value.datasets[1].data = sessionsData.data.value.data; chartData.value.datasets[2].data = eventsData.data.value.data.map(e => { - return { x: 0, y: maxChartY + 70, r: 25 / maxEventSize * e, r2: e } + const rValue = 25 / maxEventSize * e; + return { x: 0, y: maxChartY + 70, r: isNaN(rValue) ? 0 : rValue, r2: e } }); chartData.value.datasets[0].backgroundColor = [createGradient('#5655d7')]; @@ -226,7 +227,14 @@ function onDataReady() { } -const currentTooltipData = ref<[number, number, number, string]>([0, 0, 0, '']); +const currentTooltipData = ref<{ visits: number, events: number, sessions: number, date: string }>({ + visits: 0, + events: 0, + sessions: 0, + date: '' +}); + +const tooltipNameIndex = ['visits', 'sessions', 'events']; function onLegendChange(dataset: any, index: number, checked: any) { dataset.hidden = !checked; @@ -273,14 +281,14 @@ onMounted(async () => {
Date:
-
{{ currentTooltipData[3] }}
+
{{ currentTooltipData.date }}
{{ dataset.label }}
- {{ currentTooltipData[index] }} + {{ (currentTooltipData as any)[tooltipNameIndex[index]] }}
diff --git a/dashboard/nuxt.config.ts b/dashboard/nuxt.config.ts index 8d63bdf..64edaa5 100644 --- a/dashboard/nuxt.config.ts +++ b/dashboard/nuxt.config.ts @@ -43,6 +43,8 @@ export default defineNuxtConfig({ AUTH_JWT_SECRET: process.env.AUTH_JWT_SECRET, GOOGLE_AUTH_CLIENT_ID: process.env.GOOGLE_AUTH_CLIENT_ID, GOOGLE_AUTH_CLIENT_SECRET: process.env.GOOGLE_AUTH_CLIENT_SECRET, + GITHUB_AUTH_CLIENT_ID: process.env.GITHUB_AUTH_CLIENT_ID, + GITHUB_AUTH_CLIENT_SECRET: process.env.GITHUB_AUTH_CLIENT_SECRET, STRIPE_SECRET: process.env.STRIPE_SECRET, STRIPE_WH_SECRET: process.env.STRIPE_WH_SECRET, STRIPE_SECRET_TEST: process.env.STRIPE_SECRET_TEST, @@ -50,7 +52,8 @@ export default defineNuxtConfig({ NOAUTH_USER_EMAIL: process.env.NOAUTH_USER_EMAIL, NOAUTH_USER_NAME: process.env.NOAUTH_USER_NAME, public: { - AUTH_MODE: process.env.AUTH_MODE + AUTH_MODE: process.env.AUTH_MODE, + GITHUB_CLIENT_ID: process.env.GITHUB_AUTH_CLIENT_ID || 'NONE' } }, diff --git a/dashboard/pages/index.vue b/dashboard/pages/index.vue index 68fb3da..0aeb4b8 100644 --- a/dashboard/pages/index.vue +++ b/dashboard/pages/index.vue @@ -78,6 +78,10 @@ const { snapshot } = useSnapshot(); const refreshKey = computed(() => `${snapshot.value._id.toString() + activeProject.value?._id.toString()}`); +const isPremium = computed(() => { + return activeProject.value?.premium; +}) + const pricingDrawer = usePricingDrawer(); function goToUpgrade() { @@ -93,9 +97,7 @@ function goToUpgrade() {
-
- - +
@@ -110,23 +112,38 @@ function goToUpgrade() {
Upgrade
-
- +
+
+
+ Launch offer: 25% off +
+
+ We're offering an exclusive 25% discount forever on all plans starting from the Acceleration + Plan for our first 100 users who believe in our project. +
+ Redeem Code: LIT25 at checkout to + claim your discount. +
+
+
+ Upgrade +
+
- + +
- + + - +
+ + +
+ -
+
--> - +
diff --git a/dashboard/pages/login.vue b/dashboard/pages/login.vue index fa88266..63e25be 100644 --- a/dashboard/pages/login.vue +++ b/dashboard/pages/login.vue @@ -81,6 +81,33 @@ function handleOnError(errorResponse: any) { alert('Error' + errorResponse); }; +function getRandomHex(size: number) { + const bytes = new Uint8Array(size); + window.crypto.getRandomValues(bytes); + return Array.from(bytes) + .map((byte) => byte.toString(16).padStart(2, '0')) + .join(''); +} + +function githubLogin() { + const client_id = config.public.GITHUB_CLIENT_ID; + const redirect_uri = window.location.origin + '/api'; + console.log({ redirect_uri }) + const state = getRandomHex(16); + localStorage.setItem("latestCSRFToken", state); + const link = `https://github.com/login/oauth/authorize?client_id=${client_id}&response_type=code&scope=repo&redirect_uri=${redirect_uri}/integrations/github/oauth2/callback&state=${state}`; + window.location.assign(link); +} + +const route = useRoute(); + +onMounted(() => { + if (route.query.github_access_token) { + //TODO: Something + } +}) + + @@ -103,23 +130,34 @@ function handleOnError(errorResponse: any) {
- Real-time analytics for 15+ JS/TS frameworks + Track web analytics and custom events
- with one-line code setup. + with extreme simplicity in under 30 sec.
-
+
-
-
- + +
+
+
+ +
+ Continue with Google +
+ +
+
+ +
+ Continue with GitHub
- Continue with Google
- By continuing you are indicating that you accept + By continuing you are accepting
our Terms of Service and diff --git a/dashboard/server/api/integrations/github/oauth2/callback.ts b/dashboard/server/api/integrations/github/oauth2/callback.ts new file mode 100644 index 0000000..fdb5eea --- /dev/null +++ b/dashboard/server/api/integrations/github/oauth2/callback.ts @@ -0,0 +1,72 @@ + +import { createUserJwt } from '~/server/AuthManager'; +import { UserModel } from '@schema/UserSchema'; +import EmailService from '@services/EmailService'; + +const config = useRuntimeConfig(); + +export default defineEventHandler(async event => { + + const { code } = getQuery(event); + console.log('CODE', code); + + const redirect_uri = 'http://127.0.0.1:3000' + + const res = await fetch(`https://github.com/login/oauth/access_token?client_id=${config.GITHUB_AUTH_CLIENT_ID}&client_secret=${config.GITHUB_AUTH_CLIENT_SECRET}&code=${code}&redirect_url=${redirect_uri}`, { + headers: { + "Accept": "application/json", + "Accept-Encoding": "application/json", + }, + }); + + const data = await res.json(); + + const access_token = data.access_token; + + console.log(data); + + return sendRedirect(event,`http://127.0.0.1:3000/login?github_access_token=${access_token}`) + + + // const origin = event.headers.get('origin'); + + // const tokenResponse = await client.getToken({ + // code: body.code, + // redirect_uri: origin || '' + // }); + + // const tokens = tokenResponse.tokens; + + // const ticket = await client.verifyIdToken({ + // idToken: tokens.id_token || '', + // audience: GOOGLE_AUTH_CLIENT_ID, + // }); + + // const payload = ticket.getPayload(); + // if (!payload) return { error: true, access_token: '' }; + + + // const user = await UserModel.findOne({ email: payload.email }); + + // if (user) return { error: false, access_token: createUserJwt({ email: user.email, name: user.name }) } + + + // const newUser = new UserModel({ + // email: payload.email, + // given_name: payload.given_name, + // name: payload.name, + // locale: payload.locale, + // picture: payload.picture, + // created_at: Date.now() + // }); + + // const savedUser = await newUser.save(); + + // setImmediate(() => { + // console.log('SENDING WELCOME EMAIL TO', payload.email); + // if (payload.email) EmailService.sendWelcomeEmail(payload.email); + // }); + + // return { error: false, access_token: createUserJwt({ email: savedUser.email, name: savedUser.name }) } + +}); \ No newline at end of file diff --git a/dashboard/server/api/metrics/[project_id]/timeline/events.post.ts b/dashboard/server/api/metrics/[project_id]/timeline/events.post.ts index 36ed24d..9e02eb8 100644 --- a/dashboard/server/api/metrics/[project_id]/timeline/events.post.ts +++ b/dashboard/server/api/metrics/[project_id]/timeline/events.post.ts @@ -2,7 +2,7 @@ import { EventModel } from "@schema/metrics/EventSchema"; import { getTimeline } from "./generic"; import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService"; import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA"; -import { executeTimelineAggregation, fillAndMergeTimelineAggregation } from "~/server/services/TimelineService"; +import { executeTimelineAggregation, fillAndMergeTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService"; export default defineEventHandler(async event => { const project_id = getRequestProjectId(event); @@ -27,7 +27,7 @@ export default defineEventHandler(async event => { model: EventModel, from, to, slice }); - const timelineFilledMerged = fillAndMergeTimelineAggregation(timelineData, slice); + const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from, to); return timelineFilledMerged; }); diff --git a/dashboard/server/api/metrics/[project_id]/timeline/referrers.post.ts b/dashboard/server/api/metrics/[project_id]/timeline/referrers.post.ts index 813ef6e..b53581c 100644 --- a/dashboard/server/api/metrics/[project_id]/timeline/referrers.post.ts +++ b/dashboard/server/api/metrics/[project_id]/timeline/referrers.post.ts @@ -1,9 +1,7 @@ -import { getTimeline } from "./generic"; import { VisitModel } from "@schema/metrics/VisitSchema"; -import DateService from "@services/DateService"; import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService"; import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA"; -import { executeAdvancedTimelineAggregation, fillAndMergeTimelineAggregation } from "~/server/services/TimelineService"; +import { executeAdvancedTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService"; export default defineEventHandler(async event => { const project_id = getRequestProjectId(event); @@ -31,7 +29,7 @@ export default defineEventHandler(async event => { referrer } }); - const timelineFilledMerged = fillAndMergeTimelineAggregation(timelineData, slice); + const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from, to); return timelineFilledMerged; }); diff --git a/dashboard/server/api/metrics/[project_id]/timeline/sessions.post.ts b/dashboard/server/api/metrics/[project_id]/timeline/sessions.post.ts index 3a776a7..9cd9cc7 100644 --- a/dashboard/server/api/metrics/[project_id]/timeline/sessions.post.ts +++ b/dashboard/server/api/metrics/[project_id]/timeline/sessions.post.ts @@ -2,7 +2,7 @@ import { getTimeline } from "./generic"; import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService"; import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA"; import { SessionModel } from "@schema/metrics/SessionSchema"; -import { executeTimelineAggregation, fillAndMergeTimelineAggregation } from "~/server/services/TimelineService"; +import { executeTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService"; export default defineEventHandler(async event => { const project_id = getRequestProjectId(event); @@ -28,7 +28,7 @@ export default defineEventHandler(async event => { model: SessionModel, from, to, slice }); - const timelineFilledMerged = fillAndMergeTimelineAggregation(timelineData, slice); + const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from, to); return timelineFilledMerged; }); diff --git a/dashboard/server/api/metrics/[project_id]/timeline/sessions_duration.post.ts b/dashboard/server/api/metrics/[project_id]/timeline/sessions_duration.post.ts index 46b44fe..4848a20 100644 --- a/dashboard/server/api/metrics/[project_id]/timeline/sessions_duration.post.ts +++ b/dashboard/server/api/metrics/[project_id]/timeline/sessions_duration.post.ts @@ -2,7 +2,7 @@ import { getTimeline } from "./generic"; import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService"; import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA"; import { SessionModel } from "@schema/metrics/SessionSchema"; -import { executeAdvancedTimelineAggregation, executeTimelineAggregation, fillAndMergeTimelineAggregation } from "~/server/services/TimelineService"; +import { executeAdvancedTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService"; export default defineEventHandler(async event => { const project_id = getRequestProjectId(event); @@ -45,7 +45,7 @@ export default defineEventHandler(async event => { count: { $divide: ["$duration", "$count"] } }, }); - const timelineFilledMerged = fillAndMergeTimelineAggregation(timelineData, slice); + const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from ,to); return timelineFilledMerged; }); diff --git a/dashboard/server/api/metrics/[project_id]/timeline/visits.post.ts b/dashboard/server/api/metrics/[project_id]/timeline/visits.post.ts index 145cec1..1a4696c 100644 --- a/dashboard/server/api/metrics/[project_id]/timeline/visits.post.ts +++ b/dashboard/server/api/metrics/[project_id]/timeline/visits.post.ts @@ -2,7 +2,7 @@ import { VisitModel } from "@schema/metrics/VisitSchema"; import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService"; import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA"; import DateService from "@services/DateService"; -import { executeTimelineAggregation, fillAndMergeTimelineAggregation } from "~/server/services/TimelineService"; +import { executeTimelineAggregation, fillAndMergeTimelineAggregationV2 } from "~/server/services/TimelineService"; export default defineEventHandler(async event => { const project_id = getRequestProjectId(event); @@ -28,11 +28,9 @@ export default defineEventHandler(async event => { model: VisitModel, from, to, slice, }); - const timelineFilledMerged = fillAndMergeTimelineAggregation(timelineData, slice); + const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from, to); return timelineFilledMerged; }); - - }); \ No newline at end of file diff --git a/dashboard/server/services/TimelineService.ts b/dashboard/server/services/TimelineService.ts index 88c6e1c..16da83d 100644 --- a/dashboard/server/services/TimelineService.ts +++ b/dashboard/server/services/TimelineService.ts @@ -61,4 +61,10 @@ export function fillAndMergeTimelineAggregation(timeline: { _id: string, count: const filledDates = DateService.fillDates(timeline.map(e => e._id), slice); const merged = DateService.mergeFilledDates(filledDates, timeline, '_id', slice, { count: 0 }); return merged; +} + +export function fillAndMergeTimelineAggregationV2(timeline: { _id: string, count: number }[], slice: Slice, from: string, to: string) { + const filledDates = DateService.createBetweenDates(from, to, slice); + const merged = DateService.mergeFilledDates(filledDates.dates, timeline, '_id', slice, { count: 0 }); + return merged; } \ No newline at end of file diff --git a/shared/services/DateService.ts b/shared/services/DateService.ts index 44f3537..ee86cdd 100644 --- a/shared/services/DateService.ts +++ b/shared/services/DateService.ts @@ -102,6 +102,18 @@ class DateService { } } + createBetweenDates(from: string, to: string, slice: Slice) { + let start = dayjs(from); + const end = dayjs(to); + const filledDates: dayjs.Dayjs[] = []; + while (start.isBefore(end) || start.isSame(end)) { + filledDates.push(start); + start = start.add(1, slice); + } + return { dates: filledDates, from, to }; + } + + fillDates(dates: string[], slice: Slice) { const allDates: dayjs.Dayjs[] = []; const firstDate = dayjs(dates.at(0)); @@ -109,7 +121,7 @@ class DateService { let currentDate = firstDate.clone(); allDates.push(currentDate); - + while (currentDate.isBefore(lastDate, slice)) { currentDate = currentDate.add(1, slice); allDates.push(currentDate); @@ -121,7 +133,7 @@ class DateService { mergeFilledDates, K extends keyof T>(dates: dayjs.Dayjs[], items: T[], dateField: K, slice: Slice, fillData: Omit) { const result = new Array(); for (const date of dates) { - const item = items.find(e => dayjs(e[dateField]).isSame(date), slice); + const item = items.find(e => dayjs(e[dateField]).isSame(date, slice)); result.push(item ?? { ...fillData, [dateField]: date.format() } as T); } return result;