mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +02:00
feat(analytics): add Analytics Maturity metric to ManaScore and custom event tracking to 4 apps
Add new "Analytics Maturity" extended metric to ManaScore schema with 5 checks: pageViewTracking, customEvents, authTracking, landingTracking, publicDashboard. Populate analytics data across all 20 audit files. Document the metric in about.md. Add app-specific Umami custom event helpers and integrate tracking into: - ManaCore: 13 events (nav, onboarding, dashboard widgets, credits, settings) - Presi: 12 events (deck/slide CRUD, presentation start/exit, sharing) - Zitare: 9 events (quotes, favorites, categories, search, lists, language) - Mukke: 12 events (upload, library, playlists, projects, editor export) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f0233b8d31
commit
d2264f5360
46 changed files with 314 additions and 4 deletions
|
|
@ -253,6 +253,16 @@ const manascoreCollection = defineCollection({
|
|||
sharedLlm: z.boolean().optional(), // Uses @manacore/shared-llm (if applicable)
|
||||
})
|
||||
.optional(),
|
||||
// Analytics maturity (Umami tracking depth)
|
||||
analytics: z
|
||||
.object({
|
||||
pageViewTracking: z.boolean(), // Umami script injected (automatic page views)
|
||||
customEvents: z.boolean(), // App-specific custom events implemented
|
||||
authTracking: z.boolean(), // Login/signup/logout tracked via shared-auth
|
||||
landingTracking: z.boolean(), // Landing page CTA/pricing events
|
||||
publicDashboard: z.boolean(), // Public Umami dashboard configured
|
||||
})
|
||||
.optional(),
|
||||
// Score history for trend visualization
|
||||
history: z
|
||||
.array(
|
||||
|
|
|
|||
|
|
@ -49,6 +49,12 @@ lighthouse:
|
|||
accessibility: 95
|
||||
bestPractices: 96
|
||||
seo: 100
|
||||
analytics:
|
||||
pageViewTracking: true
|
||||
customEvents: true
|
||||
authTracking: true
|
||||
landingTracking: true
|
||||
publicDashboard: true
|
||||
status: 'production'
|
||||
version: '1.1.0'
|
||||
stats:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ scores:
|
|||
documentation: 85
|
||||
security: 82
|
||||
ux: 80
|
||||
analytics:
|
||||
pageViewTracking: true
|
||||
customEvents: true
|
||||
authTracking: true
|
||||
landingTracking: true
|
||||
publicDashboard: true
|
||||
status: 'production'
|
||||
version: '0.3.0'
|
||||
stats:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ scores:
|
|||
documentation: 10
|
||||
security: 60
|
||||
ux: 55
|
||||
analytics:
|
||||
pageViewTracking: true
|
||||
customEvents: true
|
||||
authTracking: true
|
||||
landingTracking: true
|
||||
publicDashboard: true
|
||||
status: 'beta'
|
||||
version: '0.2.0'
|
||||
stats:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ scores:
|
|||
documentation: 92
|
||||
security: 85
|
||||
ux: 85
|
||||
analytics:
|
||||
pageViewTracking: true
|
||||
customEvents: true
|
||||
authTracking: true
|
||||
landingTracking: false
|
||||
publicDashboard: true
|
||||
status: 'production'
|
||||
version: '1.0.0'
|
||||
stats:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ scores:
|
|||
documentation: 85
|
||||
security: 68
|
||||
ux: 65
|
||||
analytics:
|
||||
pageViewTracking: true
|
||||
customEvents: false
|
||||
authTracking: true
|
||||
landingTracking: false
|
||||
publicDashboard: true
|
||||
status: 'beta'
|
||||
version: '0.1.0'
|
||||
stats:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ scores:
|
|||
documentation: 88
|
||||
security: 80
|
||||
ux: 92
|
||||
analytics:
|
||||
pageViewTracking: true
|
||||
customEvents: true
|
||||
authTracking: true
|
||||
landingTracking: true
|
||||
publicDashboard: true
|
||||
status: 'production'
|
||||
version: '0.3.0'
|
||||
stats:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ scores:
|
|||
documentation: 25
|
||||
security: 55
|
||||
ux: 68
|
||||
analytics:
|
||||
pageViewTracking: true
|
||||
customEvents: true
|
||||
authTracking: true
|
||||
landingTracking: true
|
||||
publicDashboard: true
|
||||
status: 'alpha'
|
||||
version: '0.2.0'
|
||||
stats:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ scores:
|
|||
documentation: 92
|
||||
security: 88
|
||||
ux: 82
|
||||
analytics:
|
||||
pageViewTracking: false
|
||||
customEvents: false
|
||||
authTracking: false
|
||||
landingTracking: false
|
||||
publicDashboard: false
|
||||
status: 'production'
|
||||
version: '0.2.0'
|
||||
stats:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ scores:
|
|||
documentation: 80
|
||||
security: 78
|
||||
ux: 60
|
||||
analytics:
|
||||
pageViewTracking: true
|
||||
customEvents: true
|
||||
authTracking: true
|
||||
landingTracking: true
|
||||
publicDashboard: true
|
||||
status: 'beta'
|
||||
version: '0.2.0'
|
||||
stats:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ scores:
|
|||
documentation: 85
|
||||
security: 68
|
||||
ux: 55
|
||||
analytics:
|
||||
pageViewTracking: true
|
||||
customEvents: false
|
||||
authTracking: true
|
||||
landingTracking: true
|
||||
publicDashboard: true
|
||||
status: 'beta'
|
||||
version: '0.2.0'
|
||||
stats:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ scores:
|
|||
documentation: 78
|
||||
security: 65
|
||||
ux: 55
|
||||
analytics:
|
||||
pageViewTracking: true
|
||||
customEvents: false
|
||||
authTracking: true
|
||||
landingTracking: false
|
||||
publicDashboard: true
|
||||
status: 'beta'
|
||||
version: '0.2.0'
|
||||
stats:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ scores:
|
|||
documentation: 78
|
||||
security: 80
|
||||
ux: 78
|
||||
analytics:
|
||||
pageViewTracking: true
|
||||
customEvents: true
|
||||
authTracking: true
|
||||
landingTracking: true
|
||||
publicDashboard: true
|
||||
status: 'production'
|
||||
version: '0.3.0'
|
||||
stats:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ scores:
|
|||
documentation: 62
|
||||
security: 55
|
||||
ux: 50
|
||||
analytics:
|
||||
pageViewTracking: true
|
||||
customEvents: false
|
||||
authTracking: true
|
||||
landingTracking: false
|
||||
publicDashboard: true
|
||||
status: 'alpha'
|
||||
version: '0.1.0'
|
||||
stats:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ scores:
|
|||
documentation: 90
|
||||
security: 85
|
||||
ux: 82
|
||||
analytics:
|
||||
pageViewTracking: true
|
||||
customEvents: true
|
||||
authTracking: true
|
||||
landingTracking: true
|
||||
publicDashboard: true
|
||||
status: 'mature'
|
||||
version: '0.2.0'
|
||||
stats:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ scores:
|
|||
documentation: 72
|
||||
security: 55
|
||||
ux: 55
|
||||
analytics:
|
||||
pageViewTracking: true
|
||||
customEvents: false
|
||||
authTracking: true
|
||||
landingTracking: false
|
||||
publicDashboard: true
|
||||
status: 'alpha'
|
||||
version: '0.1.0'
|
||||
stats:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ scores:
|
|||
documentation: 62
|
||||
security: 65
|
||||
ux: 72
|
||||
analytics:
|
||||
pageViewTracking: true
|
||||
customEvents: false
|
||||
authTracking: true
|
||||
landingTracking: false
|
||||
publicDashboard: true
|
||||
status: 'beta'
|
||||
version: '0.2.0'
|
||||
stats:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@ scores:
|
|||
documentation: 78
|
||||
security: 78
|
||||
ux: 75
|
||||
analytics:
|
||||
pageViewTracking: true
|
||||
customEvents: false
|
||||
authTracking: true
|
||||
landingTracking: false
|
||||
publicDashboard: true
|
||||
status: 'production'
|
||||
version: '0.6.0'
|
||||
stats:
|
||||
|
|
|
|||
|
|
@ -49,6 +49,12 @@ lighthouse:
|
|||
accessibility: 93
|
||||
bestPractices: 96
|
||||
seo: 100
|
||||
analytics:
|
||||
pageViewTracking: true
|
||||
customEvents: true
|
||||
authTracking: true
|
||||
landingTracking: true
|
||||
publicDashboard: true
|
||||
status: 'production'
|
||||
version: '1.1.0'
|
||||
stats:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ scores:
|
|||
documentation: 45
|
||||
security: 55
|
||||
ux: 35
|
||||
analytics:
|
||||
pageViewTracking: false
|
||||
customEvents: false
|
||||
authTracking: false
|
||||
landingTracking: false
|
||||
publicDashboard: false
|
||||
status: 'alpha'
|
||||
version: '0.0.1'
|
||||
stats:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ scores:
|
|||
documentation: 20
|
||||
security: 70
|
||||
ux: 75
|
||||
analytics:
|
||||
pageViewTracking: true
|
||||
customEvents: true
|
||||
authTracking: true
|
||||
landingTracking: true
|
||||
publicDashboard: true
|
||||
status: 'beta'
|
||||
version: '0.2.0'
|
||||
stats:
|
||||
|
|
|
|||
|
|
@ -203,6 +203,20 @@ Prüft 7 Konsistenz-Kriterien der Backend-API:
|
|||
| **Health Endpoint** | `/health` Endpoint vorhanden |
|
||||
| **Validation** | DTO-Validation mit class-validator auf allen Inputs |
|
||||
|
||||
### Analytics Maturity
|
||||
|
||||
Bewertet die Tiefe der Umami-Analytics-Integration pro App:
|
||||
|
||||
| Check | Beschreibung |
|
||||
| ---------------------- | ---------------------------------------------------------------------- |
|
||||
| **Page View Tracking** | Umami-Script via `hooks.server.ts` injiziert (automatische Page Views) |
|
||||
| **Custom Events** | App-spezifische Events implementiert (z.B. `TodoEvents`, `ChatEvents`) |
|
||||
| **Auth Tracking** | Login/Signup/Logout automatisch via `@manacore/shared-auth` getrackt |
|
||||
| **Landing Tracking** | Landing Page Events (CTA-Clicks, Pricing Views) via `Analytics.astro` |
|
||||
| **Public Dashboard** | Öffentliches Dashboard auf `stats.mana.how` konfiguriert |
|
||||
|
||||
Apps mit allen 5 Checks sind **analytics-mature** — sie erfassen nicht nur Page Views, sondern auch Feature-spezifische Nutzerinteraktionen, Auth-Flows und Landing-Conversions.
|
||||
|
||||
### Cross-App Consistency
|
||||
|
||||
Misst wie konsequent eine App die shared Packages des Monorepos nutzt:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
import type { WidgetConfig, WidgetSize } from '$lib/types/dashboard';
|
||||
import { getWidgetMeta } from '$lib/types/dashboard';
|
||||
import { dashboardStore } from '$lib/stores/dashboard.svelte';
|
||||
import { ManaCoreEvents } from '@manacore/shared-utils/analytics';
|
||||
|
||||
// Widget components
|
||||
import CreditsWidget from './widgets/CreditsWidget.svelte';
|
||||
|
|
@ -48,9 +49,11 @@
|
|||
|
||||
function handleSizeChange(size: WidgetSize) {
|
||||
dashboardStore.updateWidgetSize(widget.id, size);
|
||||
ManaCoreEvents.widgetResized(widget.type, size);
|
||||
}
|
||||
|
||||
function handleRemove() {
|
||||
ManaCoreEvents.widgetRemoved(widget.type);
|
||||
dashboardStore.removeWidget(widget.id);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { onboardingStore } from '$lib/stores/onboarding.svelte';
|
||||
import { ManaCoreEvents } from '@manacore/shared-utils/analytics';
|
||||
import WelcomeStep from './steps/WelcomeStep.svelte';
|
||||
import ProfileStep from './steps/ProfileStep.svelte';
|
||||
import AppsStep from './steps/AppsStep.svelte';
|
||||
|
|
@ -31,6 +32,7 @@
|
|||
onboardingStore.complete();
|
||||
onComplete();
|
||||
} else {
|
||||
ManaCoreEvents.onboardingStepCompleted(currentStepData.id, currentStep + 1);
|
||||
onboardingStore.completeStep(currentStepData.id);
|
||||
onboardingStore.nextStep();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
import type { ThemeVariant } from '@manacore/shared-theme';
|
||||
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
|
||||
import { setLocale, supportedLocales } from '$lib/i18n';
|
||||
import { ManaCoreEvents, AppEvents } from '@manacore/shared-utils/analytics';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
|
|
@ -68,6 +69,7 @@
|
|||
let currentLocale = $derived($locale || 'de');
|
||||
function handleLocaleChange(newLocale: string) {
|
||||
setLocale(newLocale as any);
|
||||
AppEvents.languageChanged(newLocale);
|
||||
}
|
||||
let languageItems = $derived(
|
||||
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
|
||||
|
|
@ -128,10 +130,12 @@
|
|||
|
||||
function handleToggleTheme() {
|
||||
theme.toggleMode();
|
||||
AppEvents.themeChanged(theme.isDark ? 'dark' : 'light');
|
||||
}
|
||||
|
||||
function handleThemeModeChange(mode: 'light' | 'dark' | 'system') {
|
||||
theme.setMode(mode);
|
||||
AppEvents.themeChanged(mode);
|
||||
}
|
||||
|
||||
async function handleSignOut() {
|
||||
|
|
@ -145,10 +149,12 @@
|
|||
|
||||
function handleOnboardingComplete() {
|
||||
onboardingStore.complete();
|
||||
ManaCoreEvents.onboardingCompleted();
|
||||
showOnboarding = false;
|
||||
}
|
||||
|
||||
function handleOnboardingSkip() {
|
||||
ManaCoreEvents.onboardingSkipped(onboardingStore.currentStep);
|
||||
onboardingStore.skip();
|
||||
showOnboarding = false;
|
||||
}
|
||||
|
|
@ -183,6 +189,7 @@
|
|||
onboardingStore.load();
|
||||
if (onboardingStore.shouldShow) {
|
||||
onboardingStore.start();
|
||||
ManaCoreEvents.onboardingStarted();
|
||||
showOnboarding = true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
formatCreditCost,
|
||||
type CreditOperationType,
|
||||
} from '@manacore/credit-operations';
|
||||
import { ManaCoreEvents } from '@manacore/shared-utils/analytics';
|
||||
|
||||
let balance = $state<CreditBalance | null>(null);
|
||||
let transactions = $state<CreditTransaction[]>([]);
|
||||
|
|
@ -105,6 +106,7 @@
|
|||
if (tab === 'packages') activeTab = 'packages';
|
||||
else if (tab === 'transactions') activeTab = 'transactions';
|
||||
else if (tab === 'costs') activeTab = 'costs';
|
||||
if (activeTab !== 'overview') ManaCoreEvents.creditsTabViewed(activeTab);
|
||||
|
||||
// Handle success/canceled from Stripe redirect
|
||||
const success = $page.url.searchParams.get('success');
|
||||
|
|
|
|||
|
|
@ -6,11 +6,17 @@
|
|||
import type { CreditBalance, CreditTransaction } from '$lib/api/credits';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { dashboardStore } from '$lib/stores/dashboard.svelte';
|
||||
import { ManaCoreEvents } from '@manacore/shared-utils/analytics';
|
||||
import DashboardGrid from '$lib/components/dashboard/DashboardGrid.svelte';
|
||||
|
||||
onMount(() => {
|
||||
dashboardStore.initialize();
|
||||
});
|
||||
|
||||
function handleToggleEditing() {
|
||||
dashboardStore.toggleEditing();
|
||||
ManaCoreEvents.dashboardEditToggled(dashboardStore.isEditing);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
|
|
@ -22,7 +28,7 @@
|
|||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => dashboardStore.toggleEditing()}
|
||||
onclick={handleToggleEditing}
|
||||
class="rounded-lg px-4 py-2 text-sm font-medium transition-colors {dashboardStore.isEditing
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted text-muted-foreground hover:bg-muted/80'}"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
type AppIconId,
|
||||
} from '@manacore/shared-branding';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { ManaCoreEvents } from '@manacore/shared-utils/analytics';
|
||||
|
||||
// Detect dev mode
|
||||
const isDev =
|
||||
|
|
@ -100,6 +101,7 @@
|
|||
}
|
||||
|
||||
function handleAppClick(app: ManaApp) {
|
||||
ManaCoreEvents.appOpened(app.id);
|
||||
const url = getAppUrl(app.id);
|
||||
if (url) {
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import type { CreditBalance } from '$lib/api/credits';
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import { ManaCoreEvents } from '@manacore/shared-utils/analytics';
|
||||
|
||||
let loading = $state(true);
|
||||
let savingProfile = $state(false);
|
||||
|
|
@ -45,6 +46,7 @@
|
|||
// TODO: Implement profile update API when available
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
profileSuccess = true;
|
||||
ManaCoreEvents.profileUpdated();
|
||||
} catch (e) {
|
||||
profileError = e instanceof Error ? e.message : 'Fehler beim Speichern';
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import { onMount, onDestroy } from 'svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { projectStore } from '$lib/stores/project.svelte';
|
||||
import { MukkeEvents } from '@manacore/shared-utils/analytics';
|
||||
import { audioStore } from '$lib/stores/audio.svelte';
|
||||
import { editorStore } from '$lib/stores/editor.svelte';
|
||||
import { MARKER_COLORS } from '@mukke/shared';
|
||||
|
|
@ -226,6 +227,7 @@
|
|||
if (!response.ok) throw new Error('Export failed');
|
||||
|
||||
const blob = await response.blob();
|
||||
MukkeEvents.projectExported(format);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import { libraryStore } from '$lib/stores/library.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { playerStore } from '$lib/stores/player.svelte';
|
||||
import { MukkeEvents } from '@manacore/shared-utils/analytics';
|
||||
import SongEditor from '$lib/components/SongEditor.svelte';
|
||||
import { ContextMenu, type ContextMenuItem } from '@manacore/shared-ui';
|
||||
import type { Song } from '@mukke/shared';
|
||||
|
|
@ -109,7 +110,9 @@
|
|||
async function handleToggleFavorite(id: string, e: Event) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const song = libraryStore.songs.find((s) => s.id === id);
|
||||
await libraryStore.toggleFavorite(id);
|
||||
MukkeEvents.songFavorited(!song?.favorite);
|
||||
}
|
||||
|
||||
function handleEditSong(song: Song, e: Event) {
|
||||
|
|
@ -120,6 +123,7 @@
|
|||
|
||||
function handlePlaySong(song: Song, index: number) {
|
||||
playerStore.playSong(song, libraryStore.songs, index);
|
||||
MukkeEvents.songPlayed();
|
||||
}
|
||||
|
||||
async function openInEditor(songId: string, e: Event) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { playlistStore } from '$lib/stores/playlist.svelte';
|
||||
import { MukkeEvents } from '@manacore/shared-utils/analytics';
|
||||
|
||||
let showCreateModal = $state(false);
|
||||
let newName = $state('');
|
||||
|
|
@ -16,6 +17,7 @@
|
|||
isCreating = true;
|
||||
try {
|
||||
await playlistStore.createPlaylist(newName.trim(), newDescription.trim() || undefined);
|
||||
MukkeEvents.playlistCreated();
|
||||
newName = '';
|
||||
newDescription = '';
|
||||
showCreateModal = false;
|
||||
|
|
@ -30,6 +32,7 @@
|
|||
e.stopPropagation();
|
||||
if (!confirm('Delete this playlist?')) return;
|
||||
await playlistStore.deletePlaylist(id);
|
||||
MukkeEvents.playlistDeleted();
|
||||
}
|
||||
|
||||
function truncate(text: string, max: number): string {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { page } from '$app/stores';
|
||||
import { playlistStore } from '$lib/stores/playlist.svelte';
|
||||
import { playerStore } from '$lib/stores/player.svelte';
|
||||
import { MukkeEvents } from '@manacore/shared-utils/analytics';
|
||||
import type { Song } from '@mukke/shared';
|
||||
|
||||
let isEditingName = $state(false);
|
||||
|
|
@ -32,6 +33,7 @@
|
|||
const playlist = playlistStore.currentPlaylist;
|
||||
if (!playlist || playlist.songs.length === 0) return;
|
||||
playerStore.playQueue(playlist.songs, 0);
|
||||
MukkeEvents.playlistPlayAll();
|
||||
}
|
||||
|
||||
function handleShufflePlay() {
|
||||
|
|
@ -39,6 +41,7 @@
|
|||
if (!playlist || playlist.songs.length === 0) return;
|
||||
const shuffled = shuffleArray(playlist.songs);
|
||||
playerStore.playQueue(shuffled, 0);
|
||||
MukkeEvents.playlistShufflePlay();
|
||||
}
|
||||
|
||||
function handlePlaySong(song: Song, index: number) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { projectStore } from '$lib/stores/project.svelte';
|
||||
import { MukkeEvents } from '@manacore/shared-utils/analytics';
|
||||
|
||||
let showCreateModal = $state(false);
|
||||
let newProjectTitle = $state('');
|
||||
|
|
@ -18,6 +19,7 @@
|
|||
isCreating = true;
|
||||
try {
|
||||
const project = await projectStore.createProject(newProjectTitle, newProjectDescription);
|
||||
MukkeEvents.projectCreated();
|
||||
showCreateModal = false;
|
||||
newProjectTitle = '';
|
||||
newProjectDescription = '';
|
||||
|
|
@ -32,6 +34,7 @@
|
|||
e.preventDefault();
|
||||
if (confirm('Are you sure you want to delete this project?')) {
|
||||
await projectStore.deleteProject(id);
|
||||
MukkeEvents.projectDeleted();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { libraryStore } from '$lib/stores/library.svelte';
|
||||
import { MukkeEvents } from '@manacore/shared-utils/analytics';
|
||||
|
||||
interface UploadFile {
|
||||
file: File;
|
||||
|
|
@ -90,6 +91,7 @@
|
|||
files[index].status = 'uploaded';
|
||||
files[index].progress = 100;
|
||||
files[index].songId = song.id;
|
||||
MukkeEvents.songUploaded();
|
||||
|
||||
// Auto-extract ID3 tags from the uploaded file
|
||||
try {
|
||||
|
|
@ -107,6 +109,7 @@
|
|||
} catch (e) {
|
||||
files[index].status = 'error';
|
||||
files[index].error = e instanceof Error ? e.message : 'Upload failed';
|
||||
MukkeEvents.songUploadFailed();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { decksStore } from '$lib/stores/decks.svelte';
|
||||
import { PresiEvents } from '@manacore/shared-utils/analytics';
|
||||
import { PageHeader, ContextMenu, type ContextMenuItem } from '@manacore/shared-ui';
|
||||
import {
|
||||
Plus,
|
||||
|
|
@ -74,6 +75,7 @@
|
|||
});
|
||||
|
||||
if (deck) {
|
||||
PresiEvents.deckCreated();
|
||||
showCreateModal = false;
|
||||
newDeckTitle = '';
|
||||
newDeckDescription = '';
|
||||
|
|
@ -90,6 +92,7 @@
|
|||
async function handleDelete() {
|
||||
if (!deckToDelete) return;
|
||||
await decksStore.deleteDeck(deckToDelete.id);
|
||||
PresiEvents.deckDeleted();
|
||||
showDeleteModal = false;
|
||||
deckToDelete = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { decksStore } from '$lib/stores/decks.svelte';
|
||||
import { PresiEvents } from '@manacore/shared-utils/analytics';
|
||||
import { shareApi } from '$lib/api/client';
|
||||
import type { ShareLink } from '$lib/api/client';
|
||||
import type { Slide, SlideContent } from '@presi/shared';
|
||||
|
|
@ -93,8 +94,10 @@
|
|||
|
||||
if (editingSlide) {
|
||||
await decksStore.updateSlide(editingSlide.id, { content });
|
||||
PresiEvents.slideEdited();
|
||||
} else {
|
||||
await decksStore.createSlide(deckId, { content });
|
||||
PresiEvents.slideCreated();
|
||||
}
|
||||
|
||||
isSaving = false;
|
||||
|
|
@ -109,6 +112,7 @@
|
|||
async function handleDeleteSlide() {
|
||||
if (!slideToDelete) return;
|
||||
await decksStore.deleteSlide(slideToDelete.id);
|
||||
PresiEvents.slideDeleted();
|
||||
showDeleteModal = false;
|
||||
slideToDelete = null;
|
||||
}
|
||||
|
|
@ -135,6 +139,7 @@
|
|||
// Update order values
|
||||
newSlides.forEach((s, i) => (s.order = i + 1));
|
||||
await decksStore.reorderSlides(newSlides);
|
||||
PresiEvents.slideReordered(direction);
|
||||
}
|
||||
|
||||
function addBulletPoint() {
|
||||
|
|
@ -170,6 +175,7 @@
|
|||
try {
|
||||
const newShare = await shareApi.createShare(deckId);
|
||||
shareLinks = [newShare, ...shareLinks];
|
||||
PresiEvents.shareLinkCreated();
|
||||
} catch (e) {
|
||||
console.error('Failed to create share link:', e);
|
||||
} finally {
|
||||
|
|
@ -181,6 +187,7 @@
|
|||
try {
|
||||
await shareApi.deleteShare(shareId);
|
||||
shareLinks = shareLinks.filter((s) => s.id !== shareId);
|
||||
PresiEvents.shareLinkDeleted();
|
||||
} catch (e) {
|
||||
console.error('Failed to delete share link:', e);
|
||||
}
|
||||
|
|
@ -195,6 +202,7 @@
|
|||
const url = getShareUrl(share.shareCode);
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
PresiEvents.shareLinkCopied();
|
||||
copiedLinkId = share.id;
|
||||
setTimeout(() => {
|
||||
copiedLinkId = null;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { decksStore } from '$lib/stores/decks.svelte';
|
||||
import { PresiEvents } from '@manacore/shared-utils/analytics';
|
||||
import type { Slide } from '@presi/shared';
|
||||
import {
|
||||
X,
|
||||
|
|
@ -28,8 +29,12 @@
|
|||
|
||||
const deckId = $page.params.id as string;
|
||||
|
||||
let maxSlideReached = $state(0);
|
||||
|
||||
onMount(() => {
|
||||
decksStore.loadDeck(deckId);
|
||||
decksStore.loadDeck(deckId).then(() => {
|
||||
PresiEvents.presentationStarted(decksStore.currentSlides.length);
|
||||
});
|
||||
|
||||
// Keyboard navigation
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
|
|
@ -92,6 +97,7 @@
|
|||
function nextSlide() {
|
||||
if (currentSlideIndex < decksStore.currentSlides.length - 1) {
|
||||
currentSlideIndex++;
|
||||
if (currentSlideIndex > maxSlideReached) maxSlideReached = currentSlideIndex;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -126,6 +132,7 @@
|
|||
}
|
||||
|
||||
function exitPresentation() {
|
||||
PresiEvents.presentationExited(elapsedSeconds, maxSlideReached + 1);
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { onMount, onDestroy } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { shareApi } from '$lib/api/client';
|
||||
import { PresiEvents } from '@manacore/shared-utils/analytics';
|
||||
import type { Slide } from '@presi/shared';
|
||||
import {
|
||||
CaretLeft,
|
||||
|
|
@ -34,6 +35,7 @@
|
|||
const data = await shareApi.getByCode(shareCode);
|
||||
deck = data;
|
||||
slides = data.slides || [];
|
||||
PresiEvents.sharedDeckViewed();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load shared deck';
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { quotesStore } from '$lib/stores/quotes.svelte';
|
||||
import { favoritesStore } from '$lib/stores/favorites.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { ZitareEvents } from '@manacore/shared-utils/analytics';
|
||||
import { _ } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -47,7 +48,13 @@
|
|||
|
||||
async function toggleFavorite() {
|
||||
if (!authStore.isAuthenticated) return;
|
||||
const wasFavorite = isFavorite;
|
||||
await favoritesStore.toggle(quote.id);
|
||||
if (wasFavorite) {
|
||||
ZitareEvents.quoteUnfavorited();
|
||||
} else {
|
||||
ZitareEvents.quoteFavorited(quote.category);
|
||||
}
|
||||
}
|
||||
|
||||
async function shareQuote() {
|
||||
|
|
@ -60,6 +67,7 @@
|
|||
} else {
|
||||
await navigator.clipboard.writeText(text);
|
||||
}
|
||||
ZitareEvents.quoteShared(quote.category);
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { quotesStore } from '$lib/stores/quotes.svelte';
|
||||
import { ZitareEvents } from '@manacore/shared-utils/analytics';
|
||||
import QuoteCard from '$lib/components/QuoteCard.svelte';
|
||||
|
||||
let isRefreshing = $state(false);
|
||||
|
|
@ -8,6 +9,7 @@
|
|||
async function loadNewQuote() {
|
||||
isRefreshing = true;
|
||||
quotesStore.loadRandomQuote();
|
||||
ZitareEvents.randomQuoteLoaded();
|
||||
// Small delay for visual feedback
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
isRefreshing = false;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { ZitareEvents } from '@manacore/shared-utils/analytics';
|
||||
import { CATEGORIES, getQuotesByCategory, type Category } from '@zitare/content';
|
||||
|
||||
// Category data with icons and gradients
|
||||
|
|
@ -82,7 +83,10 @@
|
|||
{#each CATEGORIES as category}
|
||||
{@const data = categoryData[category]}
|
||||
<button
|
||||
onclick={() => goto(`/category/${category}`)}
|
||||
onclick={() => {
|
||||
ZitareEvents.categoryViewed(category);
|
||||
goto(`/category/${category}`);
|
||||
}}
|
||||
class="group p-6 rounded-2xl bg-gradient-to-br {data.gradient} text-white text-left transition-transform hover:scale-105 hover:shadow-xl"
|
||||
>
|
||||
<div class="text-4xl mb-3">{data.icon}</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { ZitareEvents } from '@manacore/shared-utils/analytics';
|
||||
|
||||
interface QuoteList {
|
||||
id: string;
|
||||
|
|
@ -78,6 +79,7 @@
|
|||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
lists = [...lists, data.list];
|
||||
ZitareEvents.listCreated();
|
||||
showCreateModal = false;
|
||||
newListName = '';
|
||||
newListDescription = '';
|
||||
|
|
@ -101,6 +103,7 @@
|
|||
|
||||
if (response.ok) {
|
||||
lists = lists.filter((l) => l.id !== listId);
|
||||
ZitareEvents.listDeleted();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete list:', error);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,25 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { searchQuotes, type Quote } from '@zitare/content';
|
||||
import { ZitareEvents } from '@manacore/shared-utils/analytics';
|
||||
import { quotesStore } from '$lib/stores/quotes.svelte';
|
||||
import QuoteCard from '$lib/components/QuoteCard.svelte';
|
||||
|
||||
let searchTerm = $state('');
|
||||
let lastTrackedTerm = $state('');
|
||||
|
||||
// Search results
|
||||
let results = $derived<Quote[]>(
|
||||
searchTerm.length >= 2 ? searchQuotes(searchTerm, quotesStore.language) : []
|
||||
);
|
||||
|
||||
// Track search when results change (debounced by derived reactivity)
|
||||
$effect(() => {
|
||||
if (searchTerm.length >= 2 && searchTerm !== lastTrackedTerm) {
|
||||
lastTrackedTerm = searchTerm;
|
||||
ZitareEvents.searchPerformed(results.length);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { quotesStore } from '$lib/stores/quotes.svelte';
|
||||
import { ZitareEvents } from '@manacore/shared-utils/analytics';
|
||||
import type { SupportedLanguage } from '@zitare/content';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
|
||||
|
|
@ -16,7 +17,9 @@
|
|||
|
||||
function handleLanguageChange(event: Event) {
|
||||
const select = event.target as HTMLSelectElement;
|
||||
quotesStore.setLanguage(select.value as SupportedLanguage);
|
||||
const lang = select.value as SupportedLanguage;
|
||||
quotesStore.setLanguage(lang);
|
||||
ZitareEvents.quoteLanguageChanged(lang);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -255,6 +255,80 @@ export const ManaDeckEvents = {
|
|||
aiCardsGenerated: (count: number) => trackEvent('ai_cards_generated', { count }),
|
||||
};
|
||||
|
||||
/**
|
||||
* ManaCore Platform Events
|
||||
*/
|
||||
export const ManaCoreEvents = {
|
||||
appOpened: (appId: string) => trackEvent('app_opened', { app: appId }),
|
||||
navClicked: (destination: string) => trackEvent('nav_clicked', { destination }),
|
||||
onboardingStarted: () => trackEvent('onboarding_started'),
|
||||
onboardingStepCompleted: (step: string, stepNumber: number) =>
|
||||
trackEvent('onboarding_step_completed', { step, step_number: stepNumber }),
|
||||
onboardingCompleted: () => trackEvent('onboarding_completed'),
|
||||
onboardingSkipped: (atStep: number) => trackEvent('onboarding_skipped', { at_step: atStep }),
|
||||
dashboardEditToggled: (editing: boolean) => trackEvent('dashboard_edit_toggled', { editing }),
|
||||
widgetAdded: (widgetType: string) => trackEvent('widget_added', { widget_type: widgetType }),
|
||||
widgetRemoved: (widgetType: string) => trackEvent('widget_removed', { widget_type: widgetType }),
|
||||
widgetResized: (widgetType: string, size: string) =>
|
||||
trackEvent('widget_resized', { widget_type: widgetType, size }),
|
||||
creditsTabViewed: (tab: string) => trackEvent('credits_tab_viewed', { tab }),
|
||||
profileUpdated: () => trackEvent('profile_updated'),
|
||||
};
|
||||
|
||||
/**
|
||||
* Mukke App Events
|
||||
*/
|
||||
export const MukkeEvents = {
|
||||
songUploaded: () => trackEvent('song_uploaded'),
|
||||
songUploadFailed: () => trackEvent('song_upload_failed'),
|
||||
songPlayed: () => trackEvent('song_played'),
|
||||
songFavorited: (favorited: boolean) => trackEvent('song_favorited', { favorited }),
|
||||
songDeleted: () => trackEvent('song_deleted'),
|
||||
playlistCreated: () => trackEvent('playlist_created'),
|
||||
playlistDeleted: () => trackEvent('playlist_deleted'),
|
||||
playlistPlayAll: () => trackEvent('playlist_play_all'),
|
||||
playlistShufflePlay: () => trackEvent('playlist_shuffle_play'),
|
||||
projectCreated: () => trackEvent('project_created'),
|
||||
projectDeleted: () => trackEvent('project_deleted'),
|
||||
projectExported: (format: string) => trackEvent('project_exported', { format }),
|
||||
};
|
||||
|
||||
/**
|
||||
* Zitare App Events
|
||||
*/
|
||||
export const ZitareEvents = {
|
||||
randomQuoteLoaded: () => trackEvent('random_quote_loaded'),
|
||||
quoteShared: (category: string) => trackEvent('quote_shared', { category }),
|
||||
quoteFavorited: (category: string) => trackEvent('quote_favorited', { category }),
|
||||
quoteUnfavorited: () => trackEvent('quote_unfavorited'),
|
||||
categoryViewed: (category: string) => trackEvent('category_viewed', { category }),
|
||||
searchPerformed: (resultsCount: number) =>
|
||||
trackEvent('search_performed', { results: resultsCount }),
|
||||
listCreated: () => trackEvent('list_created'),
|
||||
listDeleted: () => trackEvent('list_deleted'),
|
||||
quoteLanguageChanged: (language: string) => trackEvent('quote_language_changed', { language }),
|
||||
};
|
||||
|
||||
/**
|
||||
* Presi App Events
|
||||
*/
|
||||
export const PresiEvents = {
|
||||
deckCreated: () => trackEvent('deck_created'),
|
||||
deckDeleted: () => trackEvent('deck_deleted'),
|
||||
slideCreated: () => trackEvent('slide_created'),
|
||||
slideEdited: () => trackEvent('slide_edited'),
|
||||
slideDeleted: () => trackEvent('slide_deleted'),
|
||||
slideReordered: (direction: 'up' | 'down') => trackEvent('slide_reordered', { direction }),
|
||||
presentationStarted: (slideCount: number) =>
|
||||
trackEvent('presentation_started', { slide_count: slideCount }),
|
||||
presentationExited: (duration: number, slidesViewed: number) =>
|
||||
trackEvent('presentation_exited', { duration_seconds: duration, slides_viewed: slidesViewed }),
|
||||
shareLinkCreated: () => trackEvent('share_link_created'),
|
||||
shareLinkCopied: () => trackEvent('share_link_copied'),
|
||||
shareLinkDeleted: () => trackEvent('share_link_deleted'),
|
||||
sharedDeckViewed: () => trackEvent('shared_deck_viewed'),
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscription/Payment Events
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue