diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..57b08d7e --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# Environment Variables Template +# Copy this file to .env and fill in your values + +# Clerk Authentication +PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_example_key_here + +# Convex Database +PUBLIC_CONVEX_URL=https://example.convex.cloud diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts new file mode 100644 index 00000000..41f9e5dc --- /dev/null +++ b/src/lib/analytics.ts @@ -0,0 +1,26 @@ +import posthog from 'posthog-js'; +import { browser } from '$app/environment'; +import { persisted } from 'svelte-persisted-store'; + +export const trackerDialogClosed = persisted('trackerDialogClosed', false); + +export function initializeAnalytics() { + if (!browser) return; + + posthog.init('phc_jg4gOdigfHQD4MSgrSaO883dp2LjNJbJO7azv61UtI0', { + api_host: 'https://us.i.posthog.com', + person_profiles: 'always', + capture_exceptions: true + }); +} + +export async function checkTrackerBlocked(): Promise { + if (!browser) return false; + + try { + await fetch('https://us-assets.i.posthog.com/static/exception-autocapture.js'); + return false; + } catch { + return navigator.onLine; + } +} diff --git a/src/lib/components/app-sidebar.svelte b/src/lib/components/app-sidebar.svelte index 9b0a5d6e..d6da68ec 100644 --- a/src/lib/components/app-sidebar.svelte +++ b/src/lib/components/app-sidebar.svelte @@ -35,7 +35,11 @@ // App state and data import { preferencesStore } from '$lib/stores'; - import { gmaes } from '$lib/gmaes.js'; + import { createMainNavigation } from '$lib/navigation'; + import { handleGlobalKeydown, createSidebarShortcuts } from '$lib/keyboard-shortcuts'; + + // Games data + import { gmaes } from '$lib/gmaes'; import { settingsOpen } from '$lib/state.svelte'; import { mode } from 'mode-watcher'; @@ -61,110 +65,26 @@ let commandOpen = $state(false); function handleKeydown(e: KeyboardEvent) { - if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { - e.preventDefault(); - commandOpen = !commandOpen; + const shortcuts = createSidebarShortcuts({ + set: (value: boolean) => { + commandOpen = value; + } + }); + if (handleGlobalKeydown(e, shortcuts)) { + return; } + if (e.key === ',' && (e.metaKey || e.ctrlKey) && $preferencesStore.experimentalFeatures) { e.preventDefault(); settingsOpen.current = !settingsOpen.current; } } - const mainNavigation: { - title: string; - url: string; - icon?: any; - experimental: boolean; - items?: { - title: string; - url: string; - }[]; - }[] = $derived( - [ - { - title: 'Home', - icon: Home, - url: '/', - experimental: false - }, - { - title: 'Tools', - icon: Wrench, - experimental: false, - url: '', - items: [ - { - title: 'Calculator', - url: '/tools/calculator' - }, - { - title: 'Converter', - url: '/tools/converter' - }, - { - title: 'Rich Text Editor', - url: '/tools/rich-text-editor' - }, - { - title: 'Word Counter', - url: '/tools/word-counter' - }, - { - title: 'Password Generator', - url: '/tools/password-generator' - }, - { - title: 'Random Number Gen', - url: '/tools/random-number-generator' - } - ] - }, - { - title: 'Gmaes', - icon: Game, - experimental: true, - url: '', - items: [ - { - title: 'All Gmaes', - url: '/g' - }, - { - title: 'Request a Gmae', - url: 'https://github.com/EducationalTools/src/issues/new?assignees=&labels=gmae%2Cenhancement&projects=&template=gmae_request.yml&title=%5BGmae+Request%5D+' - }, - ...gmaes.map((gmae) => ({ - title: gmae.name, - url: `/g/${gmae.id}` - })) - ] - }, - { - title: 'Mirrors', - experimental: true, - url: '/mirrors', - icon: Copy - }, - { - title: 'Host a mirror', - experimental: true, - icon: Server, - url: '/mirrors/host' - }, - { - title: 'Backups', - experimental: true, - icon: History, - url: '/backups' - }, - { - title: 'About', - experimental: true, - icon: Info, - url: '/about' - } - ].filter((item) => !item.experimental || $preferencesStore.experimentalFeatures) + // Filter navigation based on experimental features + const filteredMainNavigation = $derived( + createMainNavigation(gmaes).filter( + (item) => !item.experimental || $preferencesStore.experimentalFeatures + ) ); @@ -230,7 +150,7 @@ - {#each mainNavigation as groupItem (groupItem.title)} + {#each filteredMainNavigation as groupItem (groupItem.title)} {@const Icon = groupItem.icon} {#if groupItem.items?.length} @@ -403,7 +323,7 @@ No results found. - {#each mainNavigation as groupItem (groupItem.title)} + {#each filteredMainNavigation as groupItem (groupItem.title)} {#if groupItem.items?.length} {#each groupItem.items as item (item.title)} diff --git a/src/lib/components/providers.svelte b/src/lib/components/providers.svelte new file mode 100644 index 00000000..8d555cba --- /dev/null +++ b/src/lib/components/providers.svelte @@ -0,0 +1,18 @@ + + + + + + {@render children()} + diff --git a/src/lib/components/tracker-dialog.svelte b/src/lib/components/tracker-dialog.svelte new file mode 100644 index 00000000..941f7609 --- /dev/null +++ b/src/lib/components/tracker-dialog.svelte @@ -0,0 +1,26 @@ + + + + + Notice + + We use Posthog to track errors and usage to improve EduTools. Please disable your tracker/ad + blocker to allow this. Don't worry, we won't add any ads. + + + ($trackerDialogClosed = true)}> + + + + + + + + diff --git a/src/lib/keyboard-shortcuts.ts b/src/lib/keyboard-shortcuts.ts new file mode 100644 index 00000000..bb46d383 --- /dev/null +++ b/src/lib/keyboard-shortcuts.ts @@ -0,0 +1,32 @@ +export interface KeyboardShortcut { + key: string; + metaKey?: boolean; + handler: () => void; + description: string; +} + +export function handleGlobalKeydown(event: KeyboardEvent, shortcuts: KeyboardShortcut[]): boolean { + for (const shortcut of shortcuts) { + const metaKeyMatch = shortcut.metaKey + ? event.metaKey || event.ctrlKey + : !event.metaKey && !event.ctrlKey; + + if (event.key === shortcut.key && metaKeyMatch) { + event.preventDefault(); + shortcut.handler(); + return true; + } + } + return false; +} + +export function createSidebarShortcuts(commandOpen: { set: (value: boolean) => void }) { + return [ + { + key: 'k', + metaKey: true, + handler: () => commandOpen.set(true), + description: 'Open search' + } + ]; +} diff --git a/src/lib/navigation.ts b/src/lib/navigation.ts new file mode 100644 index 00000000..a7d1c986 --- /dev/null +++ b/src/lib/navigation.ts @@ -0,0 +1,108 @@ +import Home from '@lucide/svelte/icons/home'; +import Wrench from '@lucide/svelte/icons/wrench'; +import Game from '@lucide/svelte/icons/gamepad-2'; +import Code from '@lucide/svelte/icons/code'; +import Server from '@lucide/svelte/icons/server'; +import Copy from '@lucide/svelte/icons/copy'; +import History from '@lucide/svelte/icons/history'; +import Info from '@lucide/svelte/icons/info'; + +export interface NavigationItem { + title: string; + icon: any; + url: string; + experimental?: boolean; + items?: { + title: string; + url: string; + }[]; +} + +export function createMainNavigation( + gmaes: Array<{ id: string; name: string }> = [] +): NavigationItem[] { + return [ + { + title: 'Home', + icon: Home, + url: '/', + experimental: false + }, + { + title: 'Tools', + icon: Wrench, + experimental: false, + url: '', + items: [ + { + title: 'Calculator', + url: '/tools/calculator' + }, + { + title: 'Converter', + url: '/tools/converter' + }, + { + title: 'Rich Text Editor', + url: '/tools/rich-text-editor' + }, + { + title: 'Word Counter', + url: '/tools/word-counter' + }, + { + title: 'Password Generator', + url: '/tools/password-generator' + }, + { + title: 'Random Number Gen', + url: '/tools/random-number-generator' + } + ] + }, + { + title: 'Gmaes', + icon: Game, + experimental: true, + url: '', + items: [ + { + title: 'All Gmaes', + url: '/g' + }, + { + title: 'Request a Gmae', + url: 'https://github.com/EducationalTools/src/issues/new?assignees=&labels=gmae%2Cenhancement&projects=&template=gmae_request.yml&title=%5BGmae+Request%5D+' + }, + ...gmaes.map((gmae) => ({ + title: gmae.name, + url: `/g/${gmae.id}` + })) + ] + }, + { + title: 'Mirrors', + experimental: true, + url: '/mirrors', + icon: Copy + }, + { + title: 'Host a mirror', + experimental: true, + icon: Server, + url: '/mirrors/host' + }, + { + title: 'Backups', + experimental: true, + icon: History, + url: '/backups' + }, + { + title: 'About', + experimental: true, + icon: Info, + url: '/about' + } + ]; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 71c78375..5cac0b6e 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -5,59 +5,44 @@ // Props let { children } = $props(); - // UI Components + // Core components import { Toaster } from '$lib/components/ui/sonner/index.js'; import * as Sidebar from '$lib/components/ui/sidebar/index.js'; import AppSidebar from '$lib/components/app-sidebar.svelte'; import Settings from '$lib/components/settings.svelte'; - import * as Dialog from '$lib/components/ui/dialog/index.js'; import PanicMode from '$lib/components/panic-mode.svelte'; import Cloak from '$lib/components/cloak.svelte'; + import Providers from '$lib/components/providers.svelte'; + import TrackerDialog from '$lib/components/tracker-dialog.svelte'; + import Identify from './identify.svelte'; - // Third-party utilities - import { ModeWatcher } from 'mode-watcher'; + // Utilities import clsx from 'clsx'; - import { ClerkProvider, GoogleOneTap } from 'svelte-clerk/client'; - import { PUBLIC_CLERK_PUBLISHABLE_KEY, PUBLIC_CONVEX_URL } from '$env/static/public'; - import { setupConvex } from 'convex-svelte'; - - setupConvex(PUBLIC_CONVEX_URL); - - import { persisted } from 'svelte-persisted-store'; - - import { preferencesStore } from '$lib/stores'; - import { goto } from '$app/navigation'; import { onMount } from 'svelte'; - import posthog from 'posthog-js'; - import { browser } from '$app/environment'; - import Button from '$lib/components/ui/button/button.svelte'; - import Identify from './identify.svelte'; + // Analytics and stores + import { initializeAnalytics, checkTrackerBlocked, trackerDialogClosed } from '$lib/analytics'; + import { preferencesStore } from '$lib/stores'; + // State let trackerBlockerDialog = $state(false); - const trackerDialogClosed = persisted('trackerDialogClosed', false); - - onMount(() => { + onMount(async () => { + // Handle experimental features URL parameter const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('experimental') === 'true') { $preferencesStore.experimentalFeatures = true; goto('/'); } - if (browser) { - posthog.init('phc_jg4gOdigfHQD4MSgrSaO883dp2LjNJbJO7azv61UtI0', { - api_host: 'https://us.i.posthog.com', - person_profiles: 'always', - capture_exceptions: true - }); - fetch('https://us-assets.i.posthog.com/static/exception-autocapture.js').catch(() => { - console.log(navigator.onLine); - console.log($trackerDialogClosed); + // Initialize analytics + initializeAnalytics(); - if (navigator.onLine && !$trackerDialogClosed) trackerBlockerDialog = true; - }); + // Check for tracker blocking + const isBlocked = await checkTrackerBlocked(); + if (isBlocked && !$trackerDialogClosed) { + trackerBlockerDialog = true; } }); @@ -66,36 +51,20 @@ EduTools - - - - Notice - We use Posthog to track errors and usage to improve EduTools. Please disable your - tracker/ad blocker to allow this. Don't worry, we won't add any ads. - - ($trackerDialogClosed = true)}> - - - - - - - - + + - - - +
+ - + + @@ -103,4 +72,4 @@ {@render children()} -
+ diff --git a/src/routes/tools/converter/+page.svelte b/src/routes/tools/converter/+page.svelte index 03c0576d..2fcee8c4 100644 --- a/src/routes/tools/converter/+page.svelte +++ b/src/routes/tools/converter/+page.svelte @@ -1,6 +1,5 @@ -
+