Skip to content

Commit 07fba18

Browse files
committed
feat: org upload photos
1 parent 5ca89d1 commit 07fba18

File tree

15 files changed

+523
-105
lines changed

15 files changed

+523
-105
lines changed

apps/dashboard/app/(main)/organizations/[slug]/components/organization-logo-uploader.tsx

Lines changed: 84 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client';
22

3+
import Image from 'next/image';
34
import { useRef, useState } from 'react';
45
import ReactCrop, {
56
type Crop,
@@ -18,11 +19,10 @@ import {
1819
DialogTitle,
1920
} from '@/components/ui/dialog';
2021
import { Input } from '@/components/ui/input';
21-
import { Label } from '@/components/ui/label';
2222
import { type Organization, useOrganizations } from '@/hooks/use-organizations';
2323
import { getOrganizationInitials } from '@/lib/utils';
2424
import 'react-image-crop/dist/ReactCrop.css';
25-
import { UploadSimpleIcon } from '@phosphor-icons/react';
25+
import { TrashIcon, UploadSimpleIcon } from '@phosphor-icons/react';
2626
import { getCroppedImage } from '@/lib/canvas-utils';
2727

2828
interface OrganizationLogoUploaderProps {
@@ -32,8 +32,12 @@ interface OrganizationLogoUploaderProps {
3232
export function OrganizationLogoUploader({
3333
organization,
3434
}: OrganizationLogoUploaderProps) {
35-
const { uploadOrganizationLogo, isUploadingOrganizationLogo } =
36-
useOrganizations();
35+
const {
36+
uploadOrganizationLogo,
37+
isUploadingOrganizationLogo,
38+
deleteOrganizationLogo,
39+
isDeletingOrganizationLogo,
40+
} = useOrganizations();
3741
const [preview, setPreview] = useState(organization.logo);
3842
const [imageSrc, setImageSrc] = useState<string | null>(null);
3943
const [crop, setCrop] = useState<Crop>();
@@ -42,20 +46,24 @@ export function OrganizationLogoUploader({
4246
const fileInputRef = useRef<HTMLInputElement>(null);
4347
const imageRef = useRef<HTMLImageElement>(null);
4448

45-
const handleModalOpenChange = (isOpen: boolean) => {
46-
if (!isOpen && fileInputRef.current) {
49+
const resetCropState = () => {
50+
setImageSrc(null);
51+
setCrop(undefined);
52+
setCompletedCrop(undefined);
53+
if (fileInputRef.current) {
4754
fileInputRef.current.value = '';
4855
}
56+
};
57+
58+
const handleModalOpenChange = (isOpen: boolean) => {
4959
if (!isOpen) {
50-
setImageSrc(null);
51-
setCrop(undefined);
52-
setCompletedCrop(undefined);
60+
resetCropState();
5361
}
5462
setIsModalOpen(isOpen);
5563
};
5664

57-
function onImageLoad(e: React.SyntheticEvent<HTMLImageElement>) {
58-
const { width, height } = e.currentTarget;
65+
function onImageLoad(img: HTMLImageElement) {
66+
const { naturalWidth: width, naturalHeight: height } = img;
5967
const percentCrop = centerCrop(
6068
makeAspectCrop(
6169
{
@@ -70,6 +78,7 @@ export function OrganizationLogoUploader({
7078
height
7179
);
7280
setCrop(percentCrop);
81+
7382
const pixelCrop = {
7483
unit: 'px' as const,
7584
x: Math.round((percentCrop.x / 100) * width),
@@ -105,31 +114,51 @@ export function OrganizationLogoUploader({
105114
'logo.png'
106115
);
107116

108-
const formData = new FormData();
109-
formData.append('file', croppedFile);
110-
111-
uploadOrganizationLogo(
112-
{ organizationId: organization.id, formData },
113-
{
114-
onSuccess: (data) => {
115-
setPreview(data.url);
116-
handleModalOpenChange(false);
117-
toast.success('Logo updated successfully!');
118-
},
119-
onError: (error) => {
120-
toast.error(error.message || 'Failed to upload logo.');
117+
const reader = new FileReader();
118+
reader.onloadend = () => {
119+
const fileData = reader.result as string;
120+
uploadOrganizationLogo(
121+
{
122+
organizationId: organization.id,
123+
fileData,
124+
fileName: croppedFile.name,
125+
fileType: croppedFile.type,
121126
},
122-
}
123-
);
127+
{
128+
onSuccess: (data) => {
129+
setPreview(data.logoUrl);
130+
handleModalOpenChange(false);
131+
setTimeout(() => resetCropState(), 100);
132+
},
133+
onError: (error) => {
134+
toast.error(error.message || 'Failed to upload logo.');
135+
},
136+
}
137+
);
138+
};
139+
reader.readAsDataURL(croppedFile);
124140
} catch (e) {
125141
toast.error('Failed to crop image.');
126142
console.error(e);
127143
}
128144
};
129145

146+
const handleDeleteLogo = () => {
147+
deleteOrganizationLogo(
148+
{ organizationId: organization.id },
149+
{
150+
onSuccess: () => {
151+
setPreview(null);
152+
},
153+
onError: (error) => {
154+
toast.error(error.message || 'Failed to delete logo.');
155+
},
156+
}
157+
);
158+
};
159+
130160
return (
131161
<div className="space-y-3">
132-
<Label>Organization Logo</Label>
133162
<div className="flex items-center gap-4">
134163
<div className="group relative">
135164
<Avatar className="h-20 w-20 border-2 border-border/50 shadow-sm">
@@ -152,6 +181,27 @@ export function OrganizationLogoUploader({
152181
<p className="text-muted-foreground text-sm">
153182
Click the image to upload a new one.
154183
</p>
184+
{preview && (
185+
<Button
186+
className="rounded"
187+
disabled={isDeletingOrganizationLogo}
188+
onClick={handleDeleteLogo}
189+
size="sm"
190+
variant="outline"
191+
>
192+
{isDeletingOrganizationLogo ? (
193+
<>
194+
<div className="mr-2 h-3 w-3 animate-spin rounded-full border border-muted-foreground/30 border-t-muted-foreground" />
195+
Deleting...
196+
</>
197+
) : (
198+
<>
199+
<TrashIcon className="mr-2 h-4 w-4" size={16} />
200+
Remove Logo
201+
</>
202+
)}
203+
</Button>
204+
)}
155205
<Input
156206
accept="image/png, image/jpeg, image/gif"
157207
className="hidden"
@@ -176,12 +226,17 @@ export function OrganizationLogoUploader({
176226
setCrop(percentCrop);
177227
setCompletedCrop(pixelCrop);
178228
}}
229+
onComplete={(pixelCrop) => {
230+
setCompletedCrop(pixelCrop);
231+
}}
179232
>
180-
<img
233+
<Image
181234
alt="Crop preview"
182-
onLoad={onImageLoad}
235+
height={600}
236+
onLoadingComplete={onImageLoad}
183237
ref={imageRef}
184238
src={imageSrc}
239+
width={800}
185240
/>
186241
</ReactCrop>
187242
)}

apps/dashboard/app/(main)/organizations/[slug]/components/settings-tab.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,6 @@ export function SettingsTab({ organization }: SettingsTabProps) {
5555
const [isSaving, setIsSaving] = useState(false);
5656
const [isDeleting, setIsDeleting] = useState(false);
5757

58-
// API Keys dialogs state - TODO: Re-enable when dialog components are fixed
59-
// const [showCreateKey, setShowCreateKey] = useState(false);
60-
// const [showKeyDetail, setShowKeyDetail] = useState(false);
61-
// const [selectedKeyId, setSelectedKeyId] = useState<string | null>(null);
62-
6358
const { updateOrganizationAsync, deleteOrganizationAsync } =
6459
useOrganizations();
6560

@@ -154,7 +149,6 @@ export function SettingsTab({ organization }: SettingsTabProps) {
154149
<CardContent className="space-y-6">
155150
{/* Logo Upload Section */}
156151
<div className="space-y-4">
157-
<Label className="font-medium text-sm">Organization Logo</Label>
158152
<OrganizationLogoUploader organization={organization} />
159153
</div>
160154

apps/dashboard/components/layout/navigation/navigation-config.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ export const categoryConfig = {
296296
],
297297
defaultCategory: 'websites',
298298
navigationMap: {
299-
websites: [], // Will be populated dynamically
299+
websites: [],
300300
organizations: organizationNavigation,
301301
billing: billingNavigation,
302302
settings: personalNavigation,
@@ -370,7 +370,6 @@ export const createLoadingWebsitesNavigation = (): NavigationSection[] => [
370370
},
371371
];
372372

373-
// Function to get navigation with dynamic websites data
374373
export const getNavigationWithWebsites = (
375374
pathname: string,
376375
websites: Array<{ id: string; name: string | null; domain: string }> = [],

apps/dashboard/components/organizations/create-organization-dialog.tsx

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,11 @@ export function CreateOrganizationDialog({
6161
isOpen,
6262
onClose,
6363
}: CreateOrganizationDialogProps) {
64-
const { createOrganizationAsync, isCreatingOrganization } =
65-
useOrganizations();
64+
const {
65+
createOrganizationAsync,
66+
isCreatingOrganization,
67+
uploadOrganizationLogoAsync,
68+
} = useOrganizations();
6669
// const router = useRouter();
6770

6871
// Form state
@@ -80,6 +83,7 @@ export function CreateOrganizationDialog({
8083
const [crop, setCrop] = useState<Crop>();
8184
const [completedCrop, setCompletedCrop] = useState<PixelCrop>();
8285
const [isCropModalOpen, setIsCropModalOpen] = useState(false);
86+
const [logoFile, setLogoFile] = useState<File | null>(null);
8387
const fileInputRef = useRef<HTMLInputElement>(null);
8488
const imageRef = useRef<HTMLImageElement | null>(null);
8589

@@ -100,7 +104,10 @@ export function CreateOrganizationDialog({
100104
const resetForm = () => {
101105
setFormData({ name: '', slug: '', logo: '', metadata: {} });
102106
setPreview(null);
107+
setLogoFile(null);
103108
setSlugManuallyEdited(false);
109+
resetCropState();
110+
setIsCropModalOpen(false);
104111
};
105112

106113
// Close dialog
@@ -133,14 +140,18 @@ export function CreateOrganizationDialog({
133140
);
134141

135142
// Image crop modal handlers
136-
const handleCropModalOpenChange = (open: boolean) => {
137-
if (!open && fileInputRef.current) {
143+
const resetCropState = () => {
144+
setImageSrc(null);
145+
setCrop(undefined);
146+
setCompletedCrop(undefined);
147+
if (fileInputRef.current) {
138148
fileInputRef.current.value = '';
139149
}
150+
};
151+
152+
const handleCropModalOpenChange = (open: boolean) => {
140153
if (!open) {
141-
setImageSrc(null);
142-
setCrop(undefined);
143-
setCompletedCrop(undefined);
154+
resetCropState();
144155
}
145156
setIsCropModalOpen(open);
146157
};
@@ -187,11 +198,13 @@ export function CreateOrganizationDialog({
187198
completedCrop,
188199
'logo.png'
189200
);
201+
202+
setLogoFile(croppedFile);
203+
190204
const reader = new FileReader();
191205
reader.onloadend = () => {
192206
const dataUrl = reader.result as string;
193207
setPreview(dataUrl);
194-
setFormData((prev) => ({ ...prev, logo: dataUrl }));
195208
handleCropModalOpenChange(false);
196209
toast.success('Logo saved successfully!');
197210
};
@@ -201,7 +214,6 @@ export function CreateOrganizationDialog({
201214
}
202215
};
203216

204-
// Organization initials for avatar fallback
205217
const getOrganizationInitials = (name: string) =>
206218
name
207219
.split(' ')
@@ -210,13 +222,39 @@ export function CreateOrganizationDialog({
210222
.toUpperCase()
211223
.slice(0, 2);
212224

213-
// Submit handler
214225
const handleSubmit = async () => {
215226
if (!isFormValid) {
216227
return;
217228
}
218229
try {
219-
await createOrganizationAsync(formData);
230+
const organization = await createOrganizationAsync({
231+
name: formData.name,
232+
slug: formData.slug,
233+
metadata: formData.metadata,
234+
});
235+
236+
if (logoFile && organization?.id) {
237+
try {
238+
const reader = new FileReader();
239+
const fileData = await new Promise<string>((resolve) => {
240+
reader.onloadend = () => resolve(reader.result as string);
241+
reader.readAsDataURL(logoFile);
242+
});
243+
244+
await uploadOrganizationLogoAsync({
245+
organizationId: organization.id,
246+
fileData,
247+
fileName: logoFile.name,
248+
fileType: logoFile.type,
249+
});
250+
} catch (logoError) {
251+
toast.warning(
252+
'Organization created, but logo upload failed. You can upload it later from settings.'
253+
);
254+
console.error('Logo upload failed:', logoError);
255+
}
256+
}
257+
220258
handleClose();
221259
// router.push('/organizations');
222260
} catch {
@@ -446,6 +484,9 @@ export function CreateOrganizationDialog({
446484
setCrop(percentCrop);
447485
setCompletedCrop(pixelCrop);
448486
}}
487+
onComplete={(pixelCrop) => {
488+
setCompletedCrop(pixelCrop);
489+
}}
449490
>
450491
<Image
451492
alt="Crop preview"

0 commit comments

Comments
 (0)