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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ node_modules/

# Build output
lib/
website/client/.vitepress/dist/

# Logs
*.log
Expand Down
21 changes: 19 additions & 2 deletions website/client/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
FROM node:23-alpine
# ==============================================================================
# Base stage
# ==============================================================================
FROM node:24-alpine AS base

# Install Git (required for VitePress)
RUN apk add --no-cache git

WORKDIR /app

# ==============================================================================
# Dependencies installation stage
# ==============================================================================
FROM base AS deps

# Copy package.json and package-lock.json
COPY package*.json ./

RUN npm i
# Install all dependencies (with npm cache optimization)
RUN npm ci && npm cache clean --force

# ==============================================================================
# Development stage
# ==============================================================================
FROM deps AS development

# Copy source code
COPY . .

EXPOSE 5173
Expand Down
145 changes: 36 additions & 109 deletions website/client/components/Home/TryIt.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,16 @@
</div>

<TryItPackOptions
v-model:format="inputFormat"
v-model:include-patterns="inputIncludePatterns"
v-model:ignore-patterns="inputIgnorePatterns"
v-model:file-summary="inputFileSummary"
v-model:directory-structure="inputDirectoryStructure"
v-model:remove-comments="inputRemoveComments"
v-model:remove-empty-lines="inputRemoveEmptyLines"
v-model:show-line-numbers="inputShowLineNumbers"
v-model:output-parsable="inputOutputParsable"
v-model:compress="inputCompress"
v-model:format="packOptions.format"
v-model:include-patterns="packOptions.includePatterns"
v-model:ignore-patterns="packOptions.ignorePatterns"
v-model:file-summary="packOptions.fileSummary"
v-model:directory-structure="packOptions.directoryStructure"
v-model:remove-comments="packOptions.removeComments"
v-model:remove-empty-lines="packOptions.removeEmptyLines"
v-model:show-line-numbers="packOptions.showLineNumbers"
v-model:output-parsable="packOptions.outputParsable"
v-model:compress="packOptions.compress"
/>

<div v-if="hasExecuted">
Expand All @@ -84,9 +84,8 @@

<script setup lang="ts">
import { FolderArchive, FolderOpen, Link2 } from 'lucide-vue-next';
import { computed, nextTick, onMounted, ref } from 'vue';
import type { PackResult } from '../api/client';
import { handlePackRequest } from '../utils/requestHandlers';
import { nextTick, onMounted } from 'vue';
import { usePackRequest } from '../../composables/usePackRequest';
import { isValidRemoteValue } from '../utils/validation';
import PackButton from './PackButton.vue';
import TryItFileUpload from './TryItFileUpload.vue';
Expand All @@ -95,102 +94,34 @@ import TryItPackOptions from './TryItPackOptions.vue';
import TryItResult from './TryItResult.vue';
import TryItUrlInput from './TryItUrlInput.vue';

// Form input states
const inputUrl = ref('');
const inputFormat = ref<'xml' | 'markdown' | 'plain'>('xml');
const inputRemoveComments = ref(false);
const inputRemoveEmptyLines = ref(false);
const inputShowLineNumbers = ref(false);
const inputFileSummary = ref(true);
const inputDirectoryStructure = ref(true);
const inputIncludePatterns = ref('');
const inputIgnorePatterns = ref('');
const inputOutputParsable = ref(false);
const inputRepositoryUrl = ref('');
const inputCompress = ref(false);

// Processing states
const loading = ref(false);
const error = ref<string | null>(null);
const result = ref<PackResult | null>(null);
const hasExecuted = ref(false);
const mode = ref<'url' | 'file' | 'folder'>('url');
const uploadedFile = ref<File | null>(null);

// Compute if the current mode's input is valid for submission
const isSubmitValid = computed(() => {
switch (mode.value) {
case 'url':
return !!inputUrl.value && isValidRemoteValue(inputUrl.value.trim());
case 'file':
case 'folder':
return !!uploadedFile.value;
default:
return false;
}
});
// Use composables for state management
const {
// Pack options
packOptions,

// Explicitly set the mode and handle related state changes
function setMode(newMode: 'url' | 'file' | 'folder') {
mode.value = newMode;
}
// Input states
inputUrl,
inputRepositoryUrl,
mode,
uploadedFile,

const TIMEOUT_MS = 30_000;
let requestController: AbortController | null = null;
// Request states
loading,
error,
result,
hasExecuted,

async function handleSubmit() {
// Check if current mode has valid input
if (!isSubmitValid.value) return;
// Computed
isSubmitValid,

// Cancel any pending request
if (requestController) {
requestController.abort();
}
requestController = new AbortController();

loading.value = true;
error.value = null;
result.value = null;
hasExecuted.value = true;
inputRepositoryUrl.value = inputUrl.value;

const timeoutId = setTimeout(() => {
if (requestController) {
requestController.abort('Request timed out');
throw new Error('Request timed out');
}
}, TIMEOUT_MS);

await handlePackRequest(
mode.value === 'url' ? inputUrl.value : '',
inputFormat.value,
{
removeComments: inputRemoveComments.value,
removeEmptyLines: inputRemoveEmptyLines.value,
showLineNumbers: inputShowLineNumbers.value,
fileSummary: inputFileSummary.value,
directoryStructure: inputDirectoryStructure.value,
includePatterns: inputIncludePatterns.value ? inputIncludePatterns.value.trim() : undefined,
ignorePatterns: inputIgnorePatterns.value ? inputIgnorePatterns.value.trim() : undefined,
outputParsable: inputOutputParsable.value,
compress: inputCompress.value,
},
{
onSuccess: (response) => {
result.value = response;
},
onError: (errorMessage) => {
error.value = errorMessage;
},
signal: requestController.signal,
file: mode.value === 'file' || mode.value === 'folder' ? uploadedFile.value || undefined : undefined,
},
);

clearTimeout(timeoutId);

loading.value = false;
requestController = null;
// Actions
setMode,
handleFileUpload,
submitRequest,
} = usePackRequest();

async function handleSubmit() {
await submitRequest();
}

function handleKeydown(event: KeyboardEvent) {
Expand All @@ -199,10 +130,6 @@ function handleKeydown(event: KeyboardEvent) {
}
}

function handleFileUpload(file: File) {
uploadedFile.value = file;
}

// Add repository parameter handling when component mounts
onMounted(() => {
// Get URL parameters from window location
Expand Down
94 changes: 50 additions & 44 deletions website/client/components/Home/TryItFileUpload.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
<script setup lang="ts">
import { AlertTriangle, FolderArchive } from 'lucide-vue-next';
import { ref } from 'vue';
import { useFileUpload } from '../../composables/useFileUpload';
import { useZipProcessor } from '../../composables/useZipProcessor';
import PackButton from './PackButton.vue';

const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB

const props = defineProps<{
loading: boolean;
showButton?: boolean;
Expand All @@ -14,74 +13,81 @@ const emit = defineEmits<{
upload: [file: File];
}>();

const fileInput = ref<HTMLInputElement | null>(null);
const dragActive = ref(false);
const selectedFile = ref<File | null>(null);
const errorMessage = ref<string | null>(null);

function validateFile(file: File): boolean {
errorMessage.value = null;

if (file.type !== 'application/zip' && !file.name.endsWith('.zip')) {
errorMessage.value = 'Please upload a ZIP file';
return false;
const { validateZipFile } = useZipProcessor();

const {
fileInput,
dragActive,
selectedItem: selectedFile,
errorMessage,
hasError,
isValid,
inputAttributes,
handleFileSelect,
handleDragOver,
handleDragLeave,
handleDrop,
triggerFileInput,
clearSelection,
} = useFileUpload({
mode: 'file',
placeholder: 'Drop your ZIP file here or click to browse (max 10MB)',
icon: 'file',
options: {
maxFileSize: 10 * 1024 * 1024, // 10MB
acceptedTypes: ['.zip'],
accept: '.zip',
validateFile: validateZipFile,
},
});

async function onFileSelect(files: FileList | null) {
const result = await handleFileSelect(files);
if (result.success && result.result) {
emit('upload', result.result);
}

if (file.size > MAX_FILE_SIZE) {
const sizeMB = (file.size / (1024 * 1024)).toFixed(1);
errorMessage.value = `File size (${sizeMB}MB) exceeds the 10MB limit`;
return false;
}

return true;
}

function handleFileSelect(files: FileList | null) {
if (!files || files.length === 0) return;

const file = files[0];
if (validateFile(file)) {
selectedFile.value = file;
emit('upload', file);
} else {
selectedFile.value = null;
async function onDrop(event: DragEvent) {
const result = await handleDrop(event);
if (result.success && result.result) {
emit('upload', result.result);
}
}

function triggerFileInput() {
fileInput.value?.click();
function clearFile() {
clearSelection();
}
</script>

<template>
<div class="upload-wrapper">
<div
class="upload-container"
:class="{ 'drag-active': dragActive, 'has-error': errorMessage }"
@dragover.prevent="dragActive = true"
@dragleave="dragActive = false"
@drop.prevent="handleFileSelect($event.dataTransfer?.files || null)"
:class="{ 'drag-active': dragActive, 'has-error': hasError }"
@dragover.prevent="handleDragOver"
@dragleave="handleDragLeave"
@drop.prevent="onDrop"
@click="triggerFileInput"
>
<input
ref="fileInput"
type="file"
accept=".zip"
v-bind="inputAttributes"
class="hidden-input"
@change="(e) => handleFileSelect((e.target as HTMLInputElement).files)"
@change="(e) => onFileSelect((e.target as HTMLInputElement).files)"
/>
<div class="upload-content">
<div class="upload-icon">
<AlertTriangle v-if="errorMessage" class="icon-error" size="20" />
<AlertTriangle v-if="hasError" class="icon-error" size="20" />
<FolderArchive v-else class="icon-folder" size="20" />
</div>
<div class="upload-text">
<p v-if="errorMessage" class="error-message">
{{ errorMessage }}
</p>
<p v-else-if="selectedFile" class="selected-file">
Selected: {{ selectedFile.name }}
<button class="clear-button" @click.stop="selectedFile = null">×</button>
Selected: {{ selectedFile }}
<button class="clear-button" @click.stop="clearFile">×</button>
</p>
<template v-else>
<p>Drop your ZIP file here or click to browse (max 10MB)</p>
Expand All @@ -93,7 +99,7 @@ function triggerFileInput() {
<div v-if="showButton" class="pack-button-container">
<PackButton
:loading="loading"
:isValid="!!selectedFile"
:isValid="isValid"
/>
</div>
</template>
Expand Down
Loading
Loading