file-upload

Cover image upload.

Cover image upload.

Загрузка превью…

src/file-upload/c-file-upload-10.vue

<!-- Иконки — путь lucide, идентичны react.tsx. `https://picsum.photos/...` заменён PIXEL. -->
<script setup lang="ts">
import { ref } from "vue"
import { Button } from "@/components/ui/button"
import { useFileUpload, type FileMetadata, type FileWithPreview } from "@/composables/use-file-upload"
import ErrorAlert from "./_shared/ErrorAlert.vue"

const PIXEL = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAUwAOw=="
const maxSize = 5 * 1024 * 1024

const defaultCoverImage: FileMetadata = { id: "default-cover", name: "cover-image.jpg", size: 2048000, type: "image/jpeg", url: PIXEL }
const coverImage = ref<FileWithPreview | null>({
  id: defaultCoverImage.id,
  file: defaultCoverImage,
  preview: defaultCoverImage.url,
})
const imageLoading = ref(true)

const [state, { handleDragEnter, handleDragLeave, handleDragOver, handleDrop, openFileDialog, handleFileChange }] = useFileUpload({
  maxFiles: 1,
  maxSize,
  accept: "image/*",
  multiple: false,
})
</script>

<template>
  <div class="w-full space-y-4">
    <div
      class="group border-border rounded-xl relative overflow-hidden border transition-all duration-200"
      :class="[
        state.isDragging
          ? 'border-primary bg-primary/5 border-dashed'
          : coverImage?.preview
            ? 'border-border bg-background hover:border-primary/50'
            : 'border-muted-foreground/25 bg-muted/30 hover:border-primary hover:bg-primary/5 border-dashed',
      ]"
      @dragenter="handleDragEnter"
      @dragleave="handleDragLeave"
      @dragover="handleDragOver"
      @drop="handleDrop"
    >
      <input type="file" accept="image/*" class="sr-only" @change="handleFileChange" />

      <div v-if="coverImage?.preview" class="relative aspect-21/9 w-full">
        <div v-if="imageLoading" class="bg-muted absolute inset-0 flex animate-pulse items-center justify-center">
          <div class="text-muted-foreground flex flex-col items-center gap-2">
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="24"
              height="24"
              viewBox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              stroke-width="2"
              stroke-linecap="round"
              stroke-linejoin="round"
              class="size-5"
            ><rect width="18" height="18" x="3" y="3" rx="2" ry="2" /><circle cx="9" cy="9" r="2" /><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" /></svg>
            <span class="text-sm">Loading image...</span>
          </div>
        </div>

        <img
          :src="coverImage.preview"
          alt="Cover"
          class="h-full w-full object-cover transition-opacity duration-300"
          :class="imageLoading ? 'opacity-0' : 'opacity-100'"
          @load="imageLoading = false"
          @error="imageLoading = false"
        />

        <div class="absolute inset-0 bg-black/0 transition-all duration-200 group-hover:bg-black/40" />

        <div class="absolute inset-0 flex items-center justify-center opacity-0 transition-opacity duration-200 group-hover:opacity-100">
          <div class="flex gap-2">
            <Button size="sm" variant="outline" @click="openFileDialog">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                width="24"
                height="24"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                stroke-width="2"
                stroke-linecap="round"
                stroke-linejoin="round"
              ><path d="M12 3v12" /><path d="m17 8-5-5-5 5" /><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /></svg>
              Change Cover
            </Button>
            <Button size="sm">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                width="24"
                height="24"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                stroke-width="2"
                stroke-linecap="round"
                stroke-linejoin="round"
              ><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
              Remove
            </Button>
          </div>
        </div>
      </div>
      <div v-else class="flex aspect-21/9 w-full cursor-pointer flex-col items-center justify-center gap-4 p-8 text-center" @click="openFileDialog">
        <div class="bg-primary/10 rounded-full p-4">
          <svg
            xmlns="http://www.w3.org/2000/svg"
            width="24"
            height="24"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
            class="text-primary size-8"
          ><path d="M12 13v8" /><path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242" /><path d="m8 17 4-4 4 4" /></svg>
        </div>

        <div class="space-y-2">
          <h3 class="text-lg font-semibold">Upload Cover Image</h3>
          <p class="text-muted-foreground text-sm">Drag and drop an image here, or click to browse</p>
          <p class="text-muted-foreground text-xs">Recommended size: 1200x514px • Max size: 5MB</p>
        </div>

        <Button variant="outline" size="sm">
          <svg
            xmlns="http://www.w3.org/2000/svg"
            width="24"
            height="24"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
          ><rect width="18" height="18" x="3" y="3" rx="2" ry="2" /><circle cx="9" cy="9" r="2" /><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" /></svg>
          Browse Files
        </Button>
      </div>
    </div>

    <ErrorAlert :errors="state.errors" />

    <div class="bg-muted/50 rounded-lg p-4">
      <h4 class="mb-2 text-sm font-medium">Cover Image Guidelines</h4>
      <ul class="text-muted-foreground space-y-1 text-xs">
        <li>• Use high-quality images with good lighting and composition</li>
        <li>• Recommended aspect ratio: 21:9 (ultrawide) for best results</li>
        <li>• Avoid images with important content near the edges</li>
        <li>• Supported formats: JPG, PNG, WebP</li>
      </ul>
    </div>
  </div>
</template>

src/file-upload/_shared/ErrorAlert.vue

<!--
  Общий блок "File upload error(s)" (`registry-reui/bases/radix/hooks/
  use-file-upload.ts` пробрасывает `errors: string[]`), повторяется во всех
  паттернах категории с одинаковой разметкой. Деталь реализации — не
  экспортируется из index.ts (тот же приём, что `calendar/_shared/
  CCalendar.vue`). В проверяемом (пустом/дефолтном) состоянии `errors`
  всегда пуст, поэтому этот компонент никогда не монтируется на диф.
-->
<script setup lang="ts">
import { Alert, AlertDescription, AlertTitle } from "@/components/reui/alert"

defineProps<{ errors: string[] }>()
</script>

<template>
  <Alert v-if="errors.length > 0" variant="destructive" class="mt-5">
    <svg
      xmlns="http://www.w3.org/2000/svg"
      width="24"
      height="24"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      stroke-width="2"
      stroke-linecap="round"
      stroke-linejoin="round"
    ><circle cx="12" cy="12" r="10" /><line x1="12" x2="12" y1="8" y2="12" /><line x1="12" x2="12.01" y1="16" y2="16" /></svg>
    <AlertTitle>File upload error(s)</AlertTitle>
    <AlertDescription>
      <p v-for="(error, index) in errors" :key="index" class="last:mb-0">{{ error }}</p>
    </AlertDescription>
  </Alert>
</template>

Установка

npx shadcn-vue@latest add https://revueui.rootapi.dev/r/c-file-upload-10.json

Зависимости реестра

Источник: порт из ReUI (Keenthemes, MIT)