Add media gallery with album organization and Puck integration

Backend:
- Add Album and MediaFile models for tenant-scoped media storage
- Add TenantStorageUsage model for per-tenant storage quota tracking
- Create StorageQuotaService with EntitlementService integration
- Add AlbumViewSet, MediaFileViewSet with bulk operations
- Add StorageUsageView for quota monitoring

Frontend:
- Create MediaGalleryPage with album management and file upload
- Add drag-and-drop upload with storage quota validation
- Create ImagePickerField custom Puck field for gallery integration
- Update Image, Testimonial components to use ImagePicker
- Add background image picker to Puck design controls
- Add gallery to sidebar navigation

Also includes:
- Puck marketing components (Hero, SplitContent, etc.)
- Enhanced ContactForm and BusinessHours components
- Platform login page improvements
- Site builder draft/preview enhancements

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-13 19:59:31 -05:00
parent e7733449dd
commit fbefccf436
58 changed files with 11590 additions and 477 deletions

View File

@@ -119,6 +119,18 @@ export const useIsAuthenticated = (): boolean => {
return !isLoading && !!user;
};
/**
* Get the redirect path based on user role
* Tenant users go to /dashboard/, platform users go to /
*/
const getRedirectPathForRole = (role: string): string => {
const tenantRoles = ['tenant_owner', 'tenant_manager', 'tenant_staff'];
if (tenantRoles.includes(role)) {
return '/dashboard/';
}
return '/';
};
/**
* Hook to masquerade as another user
*/
@@ -154,6 +166,7 @@ export const useMasquerade = () => {
}
const needsRedirect = targetSubdomain && currentHostname !== `${targetSubdomain}.${baseDomain}`;
const redirectPath = getRedirectPathForRole(user.role);
if (needsRedirect) {
// CRITICAL: Clear the session cookie BEFORE redirect
@@ -170,17 +183,17 @@ export const useMasquerade = () => {
// Pass tokens AND masquerading stack in URL (for cross-domain transfer)
const stackEncoded = encodeURIComponent(JSON.stringify(data.masquerade_stack || []));
const redirectUrl = buildSubdomainUrl(targetSubdomain, `/?access_token=${data.access}&refresh_token=${data.refresh}&masquerade_stack=${stackEncoded}`);
const redirectUrl = buildSubdomainUrl(targetSubdomain, `${redirectPath}?access_token=${data.access}&refresh_token=${data.refresh}&masquerade_stack=${stackEncoded}`);
window.location.href = redirectUrl;
return;
}
// If no redirect needed (same subdomain), we can just set cookies and reload
// If no redirect needed (same subdomain), we can just set cookies and navigate
setCookie('access_token', data.access, 7);
setCookie('refresh_token', data.refresh, 7);
queryClient.setQueryData(['currentUser'], data.user);
window.location.reload();
window.location.href = redirectPath;
},
});
};
@@ -227,6 +240,7 @@ export const useStopMasquerade = () => {
}
const needsRedirect = targetSubdomain && currentHostname !== `${targetSubdomain}.${baseDomain}`;
const redirectPath = getRedirectPathForRole(user.role);
if (needsRedirect) {
// CRITICAL: Clear the session cookie BEFORE redirect
@@ -242,17 +256,17 @@ export const useStopMasquerade = () => {
// Pass tokens AND masquerading stack in URL (for cross-domain transfer)
const stackEncoded = encodeURIComponent(JSON.stringify(data.masquerade_stack || []));
const redirectUrl = buildSubdomainUrl(targetSubdomain, `/?access_token=${data.access}&refresh_token=${data.refresh}&masquerade_stack=${stackEncoded}`);
const redirectUrl = buildSubdomainUrl(targetSubdomain, `${redirectPath}?access_token=${data.access}&refresh_token=${data.refresh}&masquerade_stack=${stackEncoded}`);
window.location.href = redirectUrl;
return;
}
// If no redirect needed (same subdomain), we can just set cookies and reload
// If no redirect needed (same subdomain), we can just set cookies and navigate
setCookie('access_token', data.access, 7);
setCookie('refresh_token', data.refresh, 7);
queryClient.setQueryData(['currentUser'], data.user);
window.location.reload();
window.location.href = redirectPath;
},
});
};