Files
speckle-server/packages/frontend-2/pages/index.vue
T
huanld c99f40bb20
Release pipeline / Get version (push) Has been cancelled
Release pipeline / Get Chart Name (push) Has been cancelled
Release pipeline / tests (push) Has been cancelled
Release pipeline / builds (push) Has been cancelled
Release pipeline / builds-ghcr (push) Has been cancelled
Release pipeline / test-deployments (push) Has been cancelled
Release pipeline / deploy (push) Has been cancelled
Release pipeline / Helm chart oci (push) Has been cancelled
Release pipeline / npm (push) Has been cancelled
Release pipeline / snyk (push) Has been cancelled
feat: customize speckle-server for ATAD - auth bypass, file upload, frontend cleanup
2026-04-21 16:32:12 +07:00

227 lines
6.7 KiB
Vue

<template>
<div class="min-h-screen flex items-center justify-center bg-foundation-page p-4">
<div class="bg-foundation p-8 rounded-xl shadow-lg border border-primary-muted max-w-lg w-full">
<h1 class="text-3xl font-bold mb-4 text-center text-primary">Speckle Viewer Lite</h1>
<p class="text-sm text-foreground-2 mb-8 text-center">
Upload an IFC file to instantly convert and view it in 3D.
</p>
<div
class="border-2 border-dashed rounded-xl p-10 flex flex-col items-center justify-center transition-colors text-center cursor-pointer"
:class="isDragging ? 'border-primary bg-primary/10' : 'border-outline-2 hover:border-primary-muted'"
@dragover.prevent="isDragging = true"
@dragleave.prevent="isDragging = false"
@drop.prevent="onDrop"
@click="!uploadStatus && fileInput.click()"
>
<template v-if="uploadStatus">
<div class="text-primary font-medium animate-pulse">{{ uploadStatus }}</div>
<div v-if="progress > 0" class="w-full bg-outline-3 rounded-full mt-4 h-2 max-w-xs transition-all overflow-hidden relative">
<div class="bg-primary h-full transition-all" :style="{ width: `${progress}%` }"></div>
</div>
</template>
<template v-else>
<!-- Using standard SVG to minimize dependencies on unimported icons -->
<svg class="h-10 w-10 text-primary mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
<div class="font-medium text-foreground">Click to upload or drag and drop</div>
<p class="text-xs text-foreground-2 mt-2">IFC files only</p>
</template>
</div>
<input
ref="fileInput"
type="file"
class="hidden"
accept=".ifc"
@change="onFileSelect"
/>
<div v-if="errorMsg" class="mt-4 p-4 bg-danger-muted text-danger rounded-lg text-sm text-center">
{{ errorMsg }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useNuxtApp } from '#app'
import { gql } from '@apollo/client/core'
definePageMeta({ layout: 'empty' })
const router = useRouter()
const isDragging = ref(false)
const uploadStatus = ref('')
const progress = ref(0)
const fileInput = ref()
const errorMsg = ref('')
const { $apollo } = useNuxtApp()
const onDrop = (e: DragEvent) => {
isDragging.value = false
if (uploadStatus.value) return
const file = e.dataTransfer?.files[0]
if (file) handleFile(file)
}
const onFileSelect = (e: Event) => {
if (uploadStatus.value) return
const target = e.target as HTMLInputElement
const file = target.files?.[0]
if (file) {
handleFile(file)
// reset input so the same file could be selected again
target.value = ''
}
}
const handleFile = async (file: File) => {
if (!file.name.toLowerCase().endsWith('.ifc')) {
errorMsg.value = 'Please upload a valid .ifc file.'
return
}
try {
errorMsg.value = ''
uploadStatus.value = 'Creating Bucket...'
progress.value = 5
// 1. Create a public project
const projectMut = await $apollo.defaultClient.mutate({
mutation: gql`
mutation ProjectCreate($input: ProjectCreateInput!) {
projectMutations {
create(input: $input) {
id
}
}
}
`,
variables: {
input: {
name: file.name.substring(0, file.name.lastIndexOf('.')),
description: "Public Upload",
visibility: "PUBLIC"
}
}
})
const projectId = projectMut.data.projectMutations.create.id
// 2. Generate Upload URL
uploadStatus.value = 'Preparing upload...'
progress.value = 10
const uploadUrlMut = await $apollo.defaultClient.mutate({
mutation: gql`
mutation GenerateUploadUrl($input: GenerateFileUploadUrlInput!) {
fileUploadMutations {
generateUploadUrl(input: $input) {
fileId
url
}
}
}
`,
variables: {
input: {
projectId,
fileName: file.name
}
}
})
const { url, fileId } = uploadUrlMut.data.fileUploadMutations.generateUploadUrl
// 3. Create Model Branch
uploadStatus.value = 'Creating model...'
progress.value = 15
const modelMut = await $apollo.defaultClient.mutate({
mutation: gql`
mutation ModelCreate($input: CreateModelInput!) {
modelMutations {
create(input: $input) {
id
}
}
}
`,
variables: {
input: {
projectId,
name: "Upload"
}
}
})
const modelId = modelMut.data.modelMutations.create.id
// 4. Upload File directly to Presigned URL
uploadStatus.value = 'Uploading file...'
progress.value = 20
const etag = await new Promise<string>((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('PUT', url, true)
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
progress.value = 20 + Math.floor((e.loaded / e.total) * 70) // up to 90%
}
}
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
progress.value = 90
resolve(xhr.getResponseHeader('ETag') || '"none"') // Fallback if Minio strips Etag from headers without CORS
} else {
reject(new Error('Upload failed with status: ' + xhr.status))
}
}
xhr.onerror = () => reject(new Error('Network error during upload.'))
xhr.send(file)
})
// 5. Start File Import
uploadStatus.value = 'Finalizing conversion...'
progress.value = 95
await $apollo.defaultClient.mutate({
mutation: gql`
mutation StartFileImport($input: StartFileImportInput!) {
fileUploadMutations {
startFileImport(input: $input) {
id
}
}
}
`,
variables: {
input: {
projectId,
modelId,
fileId,
etag,
}
}
})
uploadStatus.value = 'Redirecting to viewer...'
progress.value = 100
setTimeout(() => {
router.push(`/projects/${projectId}/models/${modelId}`)
}, 500)
} catch (error: any) {
console.error(error)
errorMsg.value = error.message || 'An error occurred during upload. Check console.'
uploadStatus.value = ''
progress.value = 0
}
}
</script>