Skip to content

Commit 7f274e7

Browse files
committed
refactor: ♻️ Optimize file uploader
1 parent 0ef2112 commit 7f274e7

9 files changed

+263
-203
lines changed

components/composer/composer.vue

+5-15
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import RichTextboxInput from "../inputs/rich-textbox-input.vue";
5353
import Note from "../social-elements/notes/note.vue";
5454
import Button from "./button.vue";
5555
// biome-ignore lint/style/useImportType: Biome doesn't see the Vue code
56-
import FileUploader from "./file-uploader.vue";
56+
import FileUploader, { type FileData } from "./uploader/uploader.vue";
5757
5858
const uploader = ref<InstanceType<typeof FileUploader> | undefined>(undefined);
5959
const { Control_Enter, Command_Enter, Control_Alt } = useMagicKeys();
@@ -71,15 +71,7 @@ const openFilePicker = () => {
7171
uploader.value?.openFilePicker();
7272
};
7373
74-
const files = ref<
75-
{
76-
id: string;
77-
file: File;
78-
progress: number;
79-
api_id?: string;
80-
alt_text?: string;
81-
}[]
82-
>([]);
74+
const files = ref<FileData[]>([]);
8375
8476
const handlePaste = (event: ClipboardEvent) => {
8577
if (event.clipboardData) {
@@ -95,6 +87,7 @@ const handlePaste = (event: ClipboardEvent) => {
9587
id: nanoid(),
9688
file,
9789
progress: 0,
90+
uploading: true,
9891
})),
9992
);
10093
}
@@ -109,11 +102,7 @@ watch(
109102
files,
110103
(newFiles) => {
111104
// If a file is uploading, set loading to true
112-
if (newFiles.some((file) => file.progress < 1)) {
113-
loading.value = true;
114-
} else {
115-
loading.value = false;
116-
}
105+
loading.value = newFiles.some((file) => file.uploading);
117106
},
118107
{
119108
deep: true,
@@ -143,6 +132,7 @@ onMounted(() => {
143132
id: nanoid(),
144133
file: new File([], file.url),
145134
progress: 1,
135+
uploading: false,
146136
api_id: file.id,
147137
alt_text: file.description ?? undefined,
148138
}));

components/composer/file-uploader.vue

-188
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<template>
2+
<Popover.Root :positioning="{
3+
strategy: 'fixed',
4+
}" @update:open="o => !o && $emit('update-alt-text', fileData.alt_text)">
5+
<Popover.Trigger aria-hidden="true"
6+
class="absolute top-1 left-1 p-1 bg-dark-800 ring-1 ring-white/5 text-white text-xs rounded size-6">
7+
<iconify-icon icon="tabler:alt" width="none" class="size-4" />
8+
</Popover.Trigger>
9+
<Popover.Positioner class="!z-[100]">
10+
<Popover.Content
11+
class="p-1 bg-dark-400 rounded text-sm ring-1 ring-white/10 shadow-lg text-gray-300 !min-w-72">
12+
<textarea :disabled="fileData.uploading" @keydown.enter.stop v-model="fileData.alt_text"
13+
placeholder="Add alt text"
14+
class="w-full p-2 text-sm prose prose-invert bg-dark-900 rounded focus:!ring-0 !ring-none !border-none !outline-none placeholder:text-zinc-500 appearance-none focus:!border-none focus:!outline-none" />
15+
<Button theme="secondary" @click="$emit('update-alt-text', fileData.alt_text)" class="w-full"
16+
:loading="fileData.uploading">
17+
<span>Edit</span>
18+
</Button>
19+
</Popover.Content>
20+
</Popover.Positioner>
21+
</Popover.Root>
22+
</template>
23+
24+
<script lang="ts" setup>
25+
import { Popover } from "@ark-ui/vue";
26+
import Button from "~/packages/ui/components/buttons/button.vue";
27+
import type { FileData } from "./uploader.vue";
28+
29+
const props = defineProps<{
30+
fileData: FileData;
31+
}>();
32+
33+
defineEmits<{
34+
"update-alt-text": [text?: string];
35+
}>();
36+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<template>
2+
<div role="button" tabindex="0" :class="[
3+
'size-28 bg-dark-800 rounded flex items-center relative justify-center ring-1 ring-white/20 overflow-hidden',
4+
fileData.uploading && 'animate-pulse'
5+
]" @keydown.enter="$emit('remove', fileData.id)">
6+
<PreviewContent :file="fileData.file" />
7+
<FileShadowOverlay />
8+
<FileSize :size="fileData.file.size" :uploading="fileData.uploading" />
9+
<RemoveButton @remove="$emit('remove', fileData.id)" />
10+
<AltTextEditor v-if="fileData.api_id" :file-data="fileData"
11+
@update-alt-text="(text) => $emit('update-alt-text', fileData.id, text)" />
12+
</div>
13+
</template>
14+
15+
<script lang="ts" setup>
16+
import AltTextEditor from "./alt-text-editor.vue";
17+
import FileShadowOverlay from "./file-shadow-overlay.vue";
18+
import FileSize from "./file-size.vue";
19+
import PreviewContent from "./preview-content.vue";
20+
import RemoveButton from "./remove-button.vue";
21+
import type { FileData } from "./uploader.vue";
22+
23+
defineProps<{
24+
fileData: FileData;
25+
}>();
26+
27+
defineEmits<{
28+
remove: [id: string];
29+
"update-alt-text": [id: string, text?: string];
30+
}>();
31+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
<div class="absolute inset-0 bg-black/70"></div>
3+
</template>
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<template>
2+
<div class="absolute bottom-1 right-1 p-1 bg-dark-800 text-white text-xs rounded cursor-default flex flex-row items-center gap-x-1"
3+
aria-label="File size">
4+
{{ formatBytes(size) }}
5+
<iconify-icon v-if="uploading" icon="tabler:loader-2" width="none"
6+
class="size-4 animate-spin text-primary-500" />
7+
</div>
8+
</template>
9+
10+
<script lang="ts" setup>
11+
const props = defineProps<{
12+
size: number;
13+
uploading: boolean;
14+
}>();
15+
16+
const formatBytes = (bytes: number) => {
17+
if (bytes === 0) {
18+
return "0 Bytes";
19+
}
20+
const k = 1000;
21+
const dm = 2;
22+
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
23+
const i = Math.floor(Math.log(bytes) / Math.log(k));
24+
return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
25+
};
26+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<template>
2+
<template v-if="file.type.startsWith('image/')">
3+
<img :src="createObjectURL(file)" class="w-full h-full object-cover cursor-default" alt="Preview of file" />
4+
</template>
5+
<template v-else-if="file.type.startsWith('video/')">
6+
<video :src="createObjectURL(file)" class="w-full h-full object-cover cursor-default" />
7+
</template>
8+
<template v-else>
9+
<iconify-icon :icon="getIcon(file.type)" width="none" class="size-6" />
10+
</template>
11+
</template>
12+
13+
<script lang="ts" setup>
14+
const props = defineProps<{
15+
file: File;
16+
}>();
17+
18+
const createObjectURL = URL.createObjectURL;
19+
20+
const getIcon = (mimeType: string) => {
21+
if (mimeType.startsWith("image/")) {
22+
return "tabler:photo";
23+
}
24+
if (mimeType.startsWith("video/")) {
25+
return "tabler:video";
26+
}
27+
if (mimeType.startsWith("audio/")) {
28+
return "tabler:music";
29+
}
30+
return "tabler:file";
31+
};
32+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<template>
2+
<button class="absolute top-1 right-1 p-1 bg-dark-800 text-white text-xs rounded size-6" role="button" tabindex="0"
3+
@pointerup="$emit('remove')" @keydown.enter="$emit('remove')">
4+
<iconify-icon icon="tabler:x" width="none" class="size-4" />
5+
</button>
6+
</template>
7+
8+
<script lang="ts" setup>
9+
defineEmits<{
10+
remove: [];
11+
}>();
12+
</script>

0 commit comments

Comments
 (0)