diff --git a/website/client/.tool-versions b/website/client/.tool-versions index 604be079d..6018b907e 100644 --- a/website/client/.tool-versions +++ b/website/client/.tool-versions @@ -1 +1 @@ -nodejs 22.14.0 +nodejs 22.15.1 diff --git a/website/client/components/Home/TryItResultContent.vue b/website/client/components/Home/TryItResultContent.vue index fc0d380fc..9c3d0513c 100644 --- a/website/client/components/Home/TryItResultContent.vue +++ b/website/client/components/Home/TryItResultContent.vue @@ -2,12 +2,19 @@ import ace, { type Ace } from 'ace-builds'; import themeTomorrowUrl from 'ace-builds/src-noconflict/theme-tomorrow?url'; import themeTomorrowNightUrl from 'ace-builds/src-noconflict/theme-tomorrow_night?url'; -import { BarChart2, Copy, Download, GitFork, HeartHandshake, PackageSearch, Star } from 'lucide-vue-next'; +import { BarChart2, Copy, Download, GitFork, HeartHandshake, PackageSearch, Share, Star } from 'lucide-vue-next'; import { useData } from 'vitepress'; import { computed, onMounted, ref, watch } from 'vue'; import { VAceEditor } from 'vue3-ace-editor'; import type { PackResult } from '../api/client'; -import { copyToClipboard, downloadResult, formatTimestamp, getEditorOptions } from '../utils/resultViewer'; +import { + canShareFiles, + copyToClipboard, + downloadResult, + formatTimestamp, + getEditorOptions, + shareResult, +} from '../utils/resultViewer'; ace.config.setModuleUrl('ace/theme/tomorrow', themeTomorrowUrl); ace.config.setModuleUrl('ace/theme/tomorrow_night', themeTomorrowNightUrl); @@ -20,6 +27,8 @@ const props = defineProps<{ }>(); const copied = ref(false); +const shared = ref(false); +const canShare = ref(canShareFiles()); const { isDark } = useData(); const editorInstance = ref(null); @@ -57,6 +66,21 @@ const handleDownload = (event: Event) => { downloadResult(props.result.content, props.result.format, props.result); }; +const handleShare = async (event: Event) => { + event.preventDefault(); + event.stopPropagation(); + + const success = await shareResult(props.result.content, props.result.format, props.result); + if (success) { + shared.value = true; + setTimeout(() => { + shared.value = false; + }, 2000); + } else { + console.log('Share was cancelled or failed'); + } +}; + const handleEditorMount = (editor: Ace.Editor) => { editorInstance.value = editor; }; @@ -145,6 +169,17 @@ const supportMessage = computed(() => ({ Download +
+
diff --git a/website/client/components/utils/analytics.ts b/website/client/components/utils/analytics.ts index ea835be77..9f4b39532 100644 --- a/website/client/components/utils/analytics.ts +++ b/website/client/components/utils/analytics.ts @@ -32,6 +32,7 @@ export const AnalyticsAction = { // Output events COPY_OUTPUT: 'copy_output', DOWNLOAD_OUTPUT: 'download_output', + SHARE_OUTPUT: 'share_output', } as const; export type AnalyticsCategoryType = (typeof AnalyticsCategory)[keyof typeof AnalyticsCategory]; @@ -117,6 +118,14 @@ export const analyticsUtils = { label: format, }); }, + + trackShareOutput(format: string): void { + trackEvent({ + category: AnalyticsCategory.OUTPUT, + action: AnalyticsAction.SHARE_OUTPUT, + label: format, + }); + }, }; // Type definitions for window.gtag diff --git a/website/client/components/utils/resultViewer.ts b/website/client/components/utils/resultViewer.ts index 307e1cb18..fa807f91d 100644 --- a/website/client/components/utils/resultViewer.ts +++ b/website/client/components/utils/resultViewer.ts @@ -57,6 +57,51 @@ export function downloadResult(content: string, format: string, result: PackResu document.body.removeChild(a); } +/** + * Handle sharing with Web Share API as file + */ +export async function shareResult(content: string, format: string, result: PackResult): Promise { + try { + const repoName = formatRepositoryName(result.metadata.repository); + const extension = format === 'markdown' ? 'md' : format === 'xml' ? 'xml' : 'txt'; + const filename = `repomix-output-${repoName}.${extension}`; + + const mimeType = format === 'markdown' ? 'text/markdown' : format === 'xml' ? 'application/xml' : 'text/plain'; + const blob = new Blob([content], { type: mimeType }); + const file = new File([blob], filename, { type: mimeType }); + + const shareData = { + files: [file], + }; + + if (navigator.canShare?.(shareData)) { + await navigator.share(shareData); + analyticsUtils.trackShareOutput(format); + return true; + } + + return false; + } catch (err) { + console.error('Failed to share:', err); + return false; + } +} + +/** + * Check if Web Share API is supported for file sharing + */ +export function canShareFiles(): boolean { + if (navigator.canShare && typeof navigator.canShare === 'function') { + try { + const dummyFile = new File([''], 'dummy.txt', { type: 'text/plain' }); + return navigator.canShare({ files: [dummyFile] }); + } catch { + return false; + } + } + return false; +} + /** * Get Ace editor options */ diff --git a/website/client/package.json b/website/client/package.json index 0ca9bc703..26e463c6f 100644 --- a/website/client/package.json +++ b/website/client/package.json @@ -4,7 +4,8 @@ "scripts": { "docs:dev": "vitepress dev", "docs:build": "vitepress build", - "docs:preview": "vitepress preview" + "docs:preview": "vitepress preview", + "lint": "tsc --noEmit" }, "dependencies": { "jszip": "^3.10.1", diff --git a/website/server/package.json b/website/server/package.json index 542a0251b..c575abae3 100644 --- a/website/server/package.json +++ b/website/server/package.json @@ -4,6 +4,7 @@ "scripts": { "dev": "PORT=8080 tsx watch src/index.ts", "build": "tsc", + "lint": "tsc --noEmit", "start": "node dist/index.js", "clean": "rimraf dist", "cloud-deploy": "gcloud builds submit --config=cloudbuild.yaml ." diff --git a/website/server/src/index.ts b/website/server/src/index.ts index 776ea8094..2a95ec417 100644 --- a/website/server/src/index.ts +++ b/website/server/src/index.ts @@ -29,7 +29,19 @@ app.use('*', cloudLogger()); app.use( '/*', cors({ - origin: ['http://localhost:5173', 'https://repomix.com', 'https://api.repomix.com', 'https://*.repomix.pages.dev'], + origin: (origin) => { + const allowedOrigins = ['http://localhost:5173', 'https://repomix.com', 'https://api.repomix.com']; + + if (!origin || allowedOrigins.includes(origin)) { + return origin; + } + + if (origin.endsWith('.repomix.pages.dev')) { + return origin; + } + + return null; + }, allowMethods: ['GET', 'POST', 'OPTIONS'], allowHeaders: ['Content-Type'], maxAge: 86400,