Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 website/client/.tool-versions
Original file line number Diff line number Diff line change
@@ -1 +1 @@
nodejs 22.14.0
nodejs 22.15.1
56 changes: 54 additions & 2 deletions website/client/components/Home/TryItResultContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<Ace.Editor | null>(null);

Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -145,6 +169,17 @@ const supportMessage = computed(() => ({
<Download :size="16" />
Download
</button>
<div v-if="canShare" class="mobile-only" style="flex-basis: 100%"></div>
<button
v-if="canShare"
class="action-button mobile-only"
@click="handleShare"
:class="{ shared }"
aria-label="Share output via mobile apps"
>
<Share :size="16" />
{{ shared ? 'Shared!' : 'Open with your app' }}
</button>
</div>
<div class="editor-container">
<VAceEditor
Expand Down Expand Up @@ -268,6 +303,7 @@ dd {

.output-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 12px;
background: var(--vp-c-bg);
Expand Down Expand Up @@ -298,6 +334,12 @@ dd {
border-color: var(--vp-c-brand-1);
}

.action-button.shared {
background: var(--vp-c-brand-1);
color: white;
border-color: var(--vp-c-brand-1);
}

.editor-container {
height: 100%;
width: 100%;
Expand Down Expand Up @@ -344,6 +386,10 @@ dd {
color: var(--vp-c-brand-1);
}

.mobile-only {
display: none;
}

@media (max-width: 768px) {
.content-wrapper {
grid-template-columns: 1fr;
Expand All @@ -354,6 +400,8 @@ dd {
.metadata-panel {
border-right: none;
border-bottom: 1px solid var(--vp-c-border);
max-height: 400px;
overflow-y: auto;
}

.output-panel {
Expand All @@ -367,5 +415,9 @@ dd {
.support-message {
max-width: 100%;
}

.mobile-only {
display: inline-flex;
}
}
</style>
9 changes: 9 additions & 0 deletions website/client/components/utils/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions website/client/components/utils/resultViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> {
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
*/
Expand Down
3 changes: 2 additions & 1 deletion website/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions website/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 ."
Expand Down
14 changes: 13 additions & 1 deletion website/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading