Coves frontend - a photon fork
1<script lang="ts">
2 import { profile } from '$lib/app/auth.svelte'
3 import { errorMessage } from '$lib/app/error'
4 import { t } from '$lib/app/i18n'
5 import { uploadImage } from '$lib/app/util.svelte'
6 import { Button, toast } from 'mono-svelte'
7 import { DocumentPlus, Icon } from 'svelte-hero-icons/dist'
8 import { expoOut } from 'svelte/easing'
9 import { slide } from 'svelte/transition'
10 import ProgressBar from '../info/ProgressBar.svelte'
11
12 interface Props {
13 image?: FileList | null
14 preview?: boolean
15 multiple?: boolean
16 onupload?: (urls: string[]) => void
17 }
18
19 let {
20 image = $bindable(null),
21 preview = true,
22 multiple = true,
23 onupload,
24 }: Props = $props()
25 let progress = $state(1)
26
27 let previewURLs = $derived(
28 preview && image ? Array.from(image).map(URL.createObjectURL) : undefined,
29 )
30
31 async function upload() {
32 if (!profile.current?.jwt || image == null) return
33
34 progress = 0
35
36 try {
37 const uploaded = (
38 await Promise.all(
39 Array.from(image).map((i) =>
40 uploadImage(i, profile.current.instance, profile.current.jwt!)
41 .then((uploaded) => {
42 progress += 1 / (image?.length ?? 0)
43 return uploaded
44 })
45 .catch((err) => {
46 toast({ content: errorMessage(err), type: 'error' })
47 return 'Failed to upload'
48 }),
49 ),
50 )
51 ).filter((i) => i != undefined)
52
53 if (!uploaded) throw new Error('Image upload returned undefined')
54
55 onupload?.(uploaded)
56 progress = 1
57 } catch (err) {
58 toast({
59 content: errorMessage(err),
60 type: 'error',
61 })
62 }
63
64 progress = 1
65 }
66</script>
67
68{#if progress != 1}
69 <div transition:slide={{ duration: 500, easing: expoOut }} class="w-full">
70 <ProgressBar {progress} />
71 </div>
72{/if}
73<form
74 class="flex flex-col gap-4"
75 onsubmit={(e) => {
76 e.preventDefault()
77 upload()
78 }}
79>
80 <label
81 for="image-input"
82 class="p-4 border-2 border-dashed rounded-xl cursor-pointer transition-colors
83 border-slate-300 dark:border-zinc-700 hover:border-primary-900 dark:hover:border-primary-100"
84 >
85 {#if previewURLs}
86 <div
87 class="grid gap-x-2 gap-y-px max-h-64 overflow-auto
88 {multiple ? 'grid-cols-3 grid-rows-2' : 'grid-cols-1 grid-rows-1'}"
89 >
90 {#each previewURLs as file}
91 <img
92 src={file}
93 onload={() => URL.revokeObjectURL(file)}
94 alt={file}
95 class="w-full rounded-md h-full object-cover transition-all
96 border border-slate-200 dark:border-zinc-800 ring-1
97 ring-slate-50 dark:ring-zinc-950 bg-white dark:bg-zinc-950"
98 />
99 {/each}
100 </div>
101 {:else}
102 <div
103 class="flex flex-col justify-center w-full items-center gap-2
104 text-slate-600 dark:text-zinc-400"
105 >
106 <Icon src={DocumentPlus} size="32" />
107 <span class="font-medium text-sm">{$t('form.post.selectFile')}</span>
108 </div>
109 {/if}
110 <input
111 {multiple}
112 type="file"
113 accept="image/*"
114 class="hidden"
115 id="image-input"
116 bind:files={image}
117 />
118 </label>
119 <Button
120 loading={progress != 1}
121 disabled={progress != 1 || image == null}
122 submit
123 color="primary"
124 size="lg"
125 >
126 Upload
127 </Button>
128</form>