Skip to content

Commit 4731cc5

Browse files
authored
Fix/pictique issues (#775)
* fix: update CreatePostModal layout and button styling * fix: reduce "new group" button size and set on top right corner * fix: prevent empty messages, prevent spamming send button, improve styling * fix: improve profile image upload handling and error messaging * fix: profile placeholder url * Add invitation to start conversation * fix: enhance image upload validation, error handling in CreatePostModal & styling * fix: refine image upload handling and validation across components * fix: improve image upload validation and error handling across components * fix: update maximum file size for profile picture upload to 500KB
1 parent ea537a2 commit 4731cc5

12 files changed

Lines changed: 393 additions & 103 deletions

File tree

Lines changed: 128 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,103 @@
11
<script lang="ts">
22
import { closeCreatePostModal, createPost } from '$lib/stores/posts';
33
import { Button, Modal } from '$lib/ui';
4+
import { formatSize, validateFileSize } from '$lib/utils/fileValidation';
45
56
let { open = $bindable() }: { open: boolean } = $props();
67
8+
interface UploadItem {
9+
file: File;
10+
dataUrl: string;
11+
}
12+
713
let text = $state('');
8-
let images = $state<string[]>([]);
14+
let uploadItems = $state<UploadItem[]>([]);
915
let isSubmitting = $state(false);
16+
let error = $state('');
17+
18+
const MAX_TOTAL_SIZE = 5 * 1024 * 1024; // 5MB total
19+
const MAX_FILE_SIZE = 1 * 1024 * 1024; // 1MB per individual image
20+
21+
let totalSize = $derived(uploadItems.reduce((sum, item) => sum + item.file.size, 0));
22+
let usagePercentage = $derived((totalSize / MAX_TOTAL_SIZE) * 100);
23+
let remainingSize = $derived(MAX_TOTAL_SIZE - totalSize);
1024
1125
const handleImageUpload = (event: Event) => {
1226
const input = event.target as HTMLInputElement;
1327
if (!input.files?.length) return;
1428
1529
const file = input.files[0];
30+
31+
const validation = validateFileSize(file, MAX_FILE_SIZE, totalSize, MAX_TOTAL_SIZE);
32+
if (!validation.valid) {
33+
error = validation.error || 'Invalid file';
34+
input.value = '';
35+
return;
36+
}
37+
38+
error = '';
39+
1640
const reader = new FileReader();
1741
reader.onload = (e) => {
1842
const result = e.target?.result;
1943
if (typeof result === 'string') {
20-
console.log(result);
21-
images = [...images, result];
44+
// Atomically add both file and dataUrl together
45+
uploadItems = [...uploadItems, { file, dataUrl: result }];
46+
} else {
47+
error = 'Failed to read image file';
2248
}
2349
};
50+
reader.onerror = () => {
51+
error = 'Error reading image file';
52+
};
2453
reader.readAsDataURL(file);
54+
input.value = '';
2555
};
2656
2757
const removeImage = (index: number) => {
28-
images = images.filter((_, i) => i !== index);
58+
uploadItems = uploadItems.filter((_, i) => i !== index);
59+
error = '';
2960
};
3061
3162
const handleSubmit = async () => {
32-
if (!text.trim() && images.length === 0) return;
63+
if (!text.trim() && uploadItems.length === 0) return;
3364
3465
try {
3566
isSubmitting = true;
67+
const images = uploadItems.map((item) => item.dataUrl);
3668
await createPost(text, images);
3769
closeCreatePostModal();
3870
text = '';
39-
images = [];
40-
} catch (error) {
41-
console.error('Failed to create post:', error);
71+
uploadItems = [];
72+
error = '';
73+
} catch (err) {
74+
error = err instanceof Error ? err.message : String(err);
75+
console.error('Failed to create post:', err);
4276
} finally {
4377
isSubmitting = false;
4478
}
4579
};
4680
</script>
4781

4882
<Modal {open} onclose={closeCreatePostModal}>
49-
<div class="w-full max-w-2xl rounded-lg bg-white p-6">
83+
<div class="w-[80vw] max-w-sm rounded-lg bg-white p-6 sm:max-w-md md:max-w-lg lg:max-w-2xl">
5084
<div class="mb-4 flex items-center justify-between">
5185
<h2 class="text-xl font-semibold">Create Post</h2>
5286
<button
5387
type="button"
5488
class="rounded-full p-2 hover:bg-gray-100"
5589
onclick={closeCreatePostModal}
5690
>
57-
91+
{@render Cross()}
5892
</button>
5993
</div>
6094

95+
{#if error}
96+
<div class="mb-4 rounded-md bg-red-500 px-4 py-2 text-sm text-white">
97+
{error}
98+
</div>
99+
{/if}
100+
61101
<div class="mb-4">
62102
<!-- svelte-ignore element_invalid_self_closing_tag -->
63103
<textarea
@@ -68,45 +108,95 @@
68108
/>
69109
</div>
70110

71-
{#if images.length > 0}
72-
<div class="mb-4 grid grid-cols-2 gap-4">
73-
{#each images as image, index (index)}
74-
<div class="relative">
75-
<!-- svelte-ignore a11y_img_redundant_alt -->
76-
<img
77-
src={image}
78-
alt="Post image"
79-
class="aspect-square w-full rounded-lg object-cover"
80-
/>
81-
<button
82-
type="button"
83-
class="absolute top-2 right-2 rounded-full bg-black/50 p-1 text-white hover:bg-black/70"
84-
onclick={() => removeImage(index)}
85-
>
86-
87-
</button>
88-
</div>
89-
{/each}
111+
<div class="mb-4 grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4">
112+
{#each uploadItems as item, index (index)}
113+
<div class="relative">
114+
<!-- svelte-ignore a11y_img_redundant_alt -->
115+
<img
116+
src={item.dataUrl}
117+
alt="Post image"
118+
class="aspect-square w-full rounded-lg object-cover"
119+
/>
120+
<button
121+
type="button"
122+
class="absolute top-2 right-2 rounded-full bg-black/50 p-1 text-white hover:bg-black/70"
123+
onclick={() => removeImage(index)}
124+
aria-label="Remove image"
125+
>
126+
{@render Cross()}
127+
</button>
128+
</div>
129+
{/each}
130+
131+
<!-- Add Photo Frame -->
132+
{#if remainingSize > 0}
133+
<label
134+
class="flex aspect-square cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 transition-colors hover:border-gray-400 hover:bg-gray-100"
135+
>
136+
<svg
137+
xmlns="http://www.w3.org/2000/svg"
138+
class="h-8 w-8 text-gray-400"
139+
fill="none"
140+
viewBox="0 0 24 24"
141+
stroke="currentColor"
142+
stroke-width="2"
143+
>
144+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
145+
</svg>
146+
<span class="text-sm font-medium text-gray-600">Add Photo</span>
147+
<input
148+
type="file"
149+
accept="image/*"
150+
class="hidden"
151+
onchange={handleImageUpload}
152+
/>
153+
</label>
154+
{/if}
155+
</div>
156+
157+
{#if uploadItems.length > 1}
158+
<div class="mb-4">
159+
<div class="mb-2 flex items-center justify-between text-sm">
160+
<span class="text-gray-600">
161+
{formatSize(totalSize)} / {formatSize(MAX_TOTAL_SIZE)}
162+
</span>
163+
<span class="text-gray-600">{formatSize(remainingSize)} remaining</span>
164+
</div>
165+
<div class="h-2 w-full overflow-hidden rounded-full bg-gray-200">
166+
<div
167+
class="h-full transition-all duration-300"
168+
class:bg-red-500={usagePercentage >= 90}
169+
class:bg-yellow-500={usagePercentage >= 70 && usagePercentage < 90}
170+
class:bg-green-500={usagePercentage < 70}
171+
style="width: {Math.min(usagePercentage, 100)}%"
172+
></div>
173+
</div>
90174
</div>
91175
{/if}
92176

93-
<div class="flex items-center justify-between gap-2">
94-
<label
95-
class="w-full cursor-pointer rounded-full bg-gray-100 px-4 py-3 text-center hover:bg-gray-200"
96-
>
97-
<input type="file" accept="image/*" class="hidden" onchange={handleImageUpload} />
98-
Add Photo
99-
</label>
100-
177+
<div class="flex justify-end">
101178
<Button
102179
variant="secondary"
103180
size="sm"
104181
callback={handleSubmit}
105182
isLoading={isSubmitting}
106-
disabled={!text.trim() && images.length === 0}
183+
disabled={!text.trim() && uploadItems.length === 0}
107184
>
108185
Post
109186
</Button>
110187
</div>
111188
</div>
112189
</Modal>
190+
191+
{#snippet Cross()}
192+
<svg
193+
xmlns="http://www.w3.org/2000/svg"
194+
class="h-4 w-4"
195+
fill="none"
196+
viewBox="0 0 24 24"
197+
stroke="currentColor"
198+
stroke-width="2"
199+
>
200+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
201+
</svg>
202+
{/snippet}

platforms/pictique/src/lib/fragments/MessageInput/MessageInput.svelte

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script lang="ts">
22
import { Avatar, Input } from '$lib/ui';
3+
import Button from '$lib/ui/Button/Button.svelte';
34
import { cn } from '$lib/utils';
45
import { SentIcon } from '@hugeicons/core-free-icons';
56
import { HugeiconsIcon } from '@hugeicons/svelte';
@@ -28,6 +29,19 @@
2829
}: IMessageInputProps = $props();
2930
3031
const cBase = 'flex items-center justify-between gap-2';
32+
33+
let isSubmitting = $state(false);
34+
let isDisabled = $derived(!value.trim() || isSubmitting);
35+
36+
const handleSubmit = async () => {
37+
if (isDisabled) return;
38+
isSubmitting = true;
39+
try {
40+
await handleSend();
41+
} finally {
42+
isSubmitting = false;
43+
}
44+
};
3145
</script>
3246

3347
<div {...restProps} class={cn([cBase, restProps.class].join(' '))}>
@@ -40,15 +54,18 @@
4054
bind:value
4155
{placeholder}
4256
onkeydown={(e) => {
43-
if (e.key === 'Enter') handleSend();
57+
if (e.key === 'Enter' && !isDisabled) handleSubmit();
4458
}}
4559
/>
4660
<!-- svelte-ignore a11y_click_events_have_key_events -->
4761
<!-- svelte-ignore a11y_no_static_element_interactions -->
48-
<div
49-
class="bg-grey flex aspect-square h-13 w-13 items-center justify-center rounded-full"
50-
onclick={handleSend}
62+
<Button
63+
class="bg-grey flex aspect-square h-13 w-13 items-center justify-center rounded-full px-0 transition-opacity {isDisabled
64+
? 'cursor-not-allowed opacity-50'
65+
: 'cursor-pointer hover:opacity-80'}"
66+
callback={handleSubmit}
67+
disabled={isDisabled}
5168
>
52-
<HugeiconsIcon size="24px" icon={SentIcon} color="var(--color-black-400)" />
53-
</div>
69+
<HugeiconsIcon size="24px" icon={SentIcon} color="var(--color-black-700)" />
70+
</Button>
5471
</div>

platforms/pictique/src/lib/fragments/SideBar/SideBar.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
}
1616
let {
1717
activeTab = $bindable('home'),
18-
profileSrc = 'images/user.png',
18+
profileSrc = '/images/user.png',
1919
handlePost,
2020
...restProps
2121
}: ISideBarProps = $props();
@@ -164,7 +164,7 @@
164164
/>
165165
</span>
166166
<h3
167-
class={`${activeTab === 'profile' ? 'text-brand-burnt-orange' : 'text-black-800'} mt-[4px]`}
167+
class={`${activeTab === 'profile' ? 'text-brand-burnt-orange' : 'text-black-800'} mt-1`}
168168
>
169169
Profile
170170
</h3>

platforms/pictique/src/lib/fragments/UploadedPostView/UploadedPostView.svelte

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,18 @@
2222

2323
<article
2424
{...restProps}
25-
class={cn(
26-
[
27-
'flex min-h-max max-w-screen flex-row items-center gap-4 overflow-x-auto pr-4 pl-0.5',
28-
restProps.class
29-
].join(' ')
30-
)}
25+
class={cn(['grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4', restProps.class].join(' '))}
3126
>
32-
{#each images as image, i (i)}
33-
<div class={cn(['group relative mt-3 mb-2 shrink-0'])}>
27+
{#each images as image, i (image.url)}
28+
<div class="group relative aspect-square">
3429
<Cross
35-
class="absolute top-0 right-0 hidden translate-x-1/2 -translate-y-1/2 cursor-pointer group-hover:block"
30+
class="absolute top-2 right-2 z-10 cursor-pointer rounded-full bg-black/50 p-1 text-white hover:bg-black/70"
3631
onclick={() => callback(i)}
3732
/>
3833
<img
3934
src={image.url}
4035
alt={image.alt}
41-
class={cn(['rounded-lg outline-[#DA4A11] group-hover:outline-2', width, height])}
36+
class="h-full w-full rounded-lg object-cover outline-[#DA4A11] group-hover:outline-2"
4237
/>
4338
</div>
4439
{/each}

platforms/pictique/src/lib/stores/posts.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,17 @@ export const createPost = async (text: string, images: string[]) => {
7272
try {
7373
isLoading.set(true);
7474
error.set(null);
75-
const response = await apiClient.post('/api/posts', {
75+
76+
const payload = {
7677
text,
7778
images: images.map((img) => img)
78-
});
79+
};
80+
81+
// Log payload size for debugging
82+
const payloadSize = new Blob([JSON.stringify(payload)]).size;
83+
console.log(`Payload size: ${(payloadSize / 1024).toFixed(2)} KB (${images.length} images)`);
84+
85+
const response = await apiClient.post('/api/posts', payload);
7986
resetFeed();
8087
await fetchFeed(1, 10, false);
8188
return response.data;

platforms/pictique/src/lib/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export type userProfile = {
7373
export type Image = {
7474
url: string;
7575
alt: string;
76+
size?: number;
7677
};
7778

7879
export type GroupInfo = {

0 commit comments

Comments
 (0)