Emp perfis #25

Merged
killer-cf merged 4 commits from emp-perfis into master 2025-11-15 00:58:00 +00:00
25 changed files with 1584 additions and 1608 deletions
Showing only changes of commit d8da7e2a05 - Show all commits

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { useConvexClient } from "convex-svelte"; import { useConvexClient } from 'convex-svelte';
import { resolve } from '$app/paths';
import { import {
ExternalLink, ExternalLink,
FileText, FileText,
@@ -7,8 +8,8 @@
Upload, Upload,
Trash2, Trash2,
Eye, Eye,
RefreshCw, RefreshCw
} from "lucide-svelte"; } from 'lucide-svelte';
interface Props { interface Props {
label: string; label: string;
@@ -27,48 +28,47 @@
disabled = false, disabled = false,
required = false, required = false,
onUpload, onUpload,
onRemove, onRemove
}: Props = $props(); }: Props = $props();
const client = useConvexClient(); const client = useConvexClient() as unknown as {
storage: {
getUrl: (id: string) => Promise<string | null>;
};
};
let fileInput: HTMLInputElement; let fileInput: HTMLInputElement;
let uploading = $state(false); let uploading = $state(false);
let error = $state<string | null>(null); let error = $state<string | null>(null);
let fileName = $state<string>(""); let fileName = $state<string>('');
let fileType = $state<string>(""); let fileType = $state<string>('');
let previewUrl = $state<string | null>(null); let previewUrl = $state<string | null>(null);
let fileUrl = $state<string | null>(null); let fileUrl = $state<string | null>(null);
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
const ALLOWED_TYPES = [ const ALLOWED_TYPES = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png'];
"application/pdf",
"image/jpeg",
"image/jpg",
"image/png",
];
// Buscar URL do arquivo quando houver um storageId // Buscar URL do arquivo quando houver um storageId
$effect(() => { $effect(() => {
if (value && !fileName) { if (value && !fileName) {
// Tem storageId mas não é um upload recente // Tem storageId mas não é um upload recente
loadExistingFile(value); void loadExistingFile(value);
} }
}); });
async function loadExistingFile(storageId: string) { async function loadExistingFile(storageId: string) {
try { try {
const url = await client.storage.getUrl(storageId as any); const url = await client.storage.getUrl(storageId);
if (url) { if (url) {
async function handleFileSelect(event: Event) { fileUrl = url;
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
// Detectar tipo pelo URL ou assumir PDF // Detectar tipo pelo URL ou assumir PDF
if (url.includes('.pdf') || url.includes('application/pdf')) { if (url.includes('.pdf') || url.includes('application/pdf')) {
fileType = 'application/pdf'; fileType = 'application/pdf';
} else { } else {
fileType = 'image/jpeg'; fileType = 'image/jpeg';
previewUrl = url; // Para imagens, a URL serve como preview // Para imagens, a URL serve como preview
previewUrl = url;
} }
} }
} catch (err) { } catch (err) {
@@ -76,19 +76,27 @@
} }
} }
async function handleFileSelect(event: Event) {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (!file) {
return;
}
error = null; error = null;
// Validate file size // Validate file size
if (file.size > MAX_FILE_SIZE) { if (file.size > MAX_FILE_SIZE) {
error = "Arquivo muito grande. Tamanho máximo: 10MB"; error = 'Arquivo muito grande. Tamanho máximo: 10MB';
target.value = ""; target.value = '';
return; return;
} }
// Validate file type // Validate file type
if (!ALLOWED_TYPES.includes(file.type)) { if (!ALLOWED_TYPES.includes(file.type)) {
error = "Tipo de arquivo não permitido. Use PDF ou imagens (JPG, PNG)"; error = 'Tipo de arquivo não permitido. Use PDF ou imagens (JPG, PNG)';
target.value = ""; target.value = '';
return; return;
} }
@@ -98,38 +106,52 @@
fileType = file.type; fileType = file.type;
// Create preview for images // Create preview for images
if (file.type.startsWith("image/")) { if (file.type.startsWith('image/')) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
previewUrl = e.target?.result as string; const result = e.target?.result;
if (typeof result === 'string') {
previewUrl = result;
}
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
} else {
previewUrl = null;
} }
await onUpload(file); await onUpload(file);
} catch (err: any) { } catch (err: unknown) {
error = err?.message || "Erro ao fazer upload do arquivo"; if (err instanceof Error) {
error = err.message || 'Erro ao fazer upload do arquivo';
} else {
error = 'Erro ao fazer upload do arquivo';
}
previewUrl = null; previewUrl = null;
} finally { } finally {
uploading = false; uploading = false;
target.value = ""; target.value = '';
} }
} }
async function handleRemove() { async function handleRemove() {
if (!confirm("Tem certeza que deseja remover este arquivo?")) { if (!confirm('Tem certeza que deseja remover este arquivo?')) {
return; return;
} }
try { try {
uploading = true; uploading = true;
await onRemove(); await onRemove();
fileName = ""; fileName = '';
fileType = ""; fileType = '';
previewUrl = null; previewUrl = null;
fileUrl = null; fileUrl = null;
} catch (err: any) { error = null;
error = err?.message || "Erro ao remover arquivo"; } catch (err: unknown) {
if (err instanceof Error) {
error = err.message || 'Erro ao remover arquivo';
} else {
error = 'Erro ao remover arquivo';
}
} finally { } finally {
uploading = false; uploading = false;
} }
@@ -137,7 +159,7 @@
function handleView() { function handleView() {
if (fileUrl) { if (fileUrl) {
window.open(fileUrl, "_blank"); window.open(fileUrl, '_blank');
} }
} }
@@ -148,18 +170,15 @@
<div class="form-control w-full"> <div class="form-control w-full">
<label class="label" for="file-upload-input"> <label class="label" for="file-upload-input">
<span class="label-text font-medium flex items-center gap-2"> <span class="label-text flex items-center gap-2 font-medium">
{label} {label}
{#if required} {#if required}
<span class="text-error">*</span> <span class="text-error">*</span>
{/if} {/if}
{#if helpUrl} {#if helpUrl}
<div <div class="tooltip tooltip-right" data-tip="Clique para acessar o link">
class="tooltip tooltip-right"
data-tip="Clique para acessar o link"
>
<a <a
href={helpUrl} href={resolve(helpUrl)}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="text-primary hover:text-primary-focus transition-colors" class="text-primary hover:text-primary-focus transition-colors"
@@ -194,17 +213,17 @@
</div> </div>
{:else} {:else}
<div class="bg-success/10 flex h-12 w-12 items-center justify-center rounded"> <div class="bg-success/10 flex h-12 w-12 items-center justify-center rounded">
<File class="text-success h-6 w-6" strokeWidth={2} /> <FileIcon class="text-success h-6 w-6" strokeWidth={2} />
</div> </div>
{/if} {/if}
</div> </div>
<!-- File info --> <!-- File info -->
<div class="flex-1 min-w-0"> <div class="min-w-0 flex-1">
<p class="text-sm font-medium truncate"> <p class="truncate text-sm font-medium">
{fileName || "Arquivo anexado"} {fileName || 'Arquivo anexado'}
</p> </p>
<p class="text-xs text-base-content/60"> <p class="text-base-content/60 text-xs">
{#if uploading} {#if uploading}
Carregando... Carregando...
{:else} {:else}