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

@@ -56,6 +56,7 @@ const TrialExpired = React.lazy(() => import('./pages/TrialExpired'));
const Upgrade = React.lazy(() => import('./pages/Upgrade'));
// Import platform pages
const PlatformLoginPage = React.lazy(() => import('./pages/platform/PlatformLoginPage'));
const PlatformDashboard = React.lazy(() => import('./pages/platform/PlatformDashboard'));
const PlatformBusinesses = React.lazy(() => import('./pages/platform/PlatformBusinesses'));
const PlatformSupportPage = React.lazy(() => import('./pages/platform/PlatformSupport'));
@@ -115,6 +116,7 @@ const PageEditor = React.lazy(() => import('./pages/PageEditor')); // Import Pag
const PublicPage = React.lazy(() => import('./pages/PublicPage')); // Import PublicPage
const BookingFlow = React.lazy(() => import('./pages/BookingFlow')); // Import Booking Flow
const Locations = React.lazy(() => import('./pages/Locations')); // Import Locations management page
const MediaGalleryPage = React.lazy(() => import('./pages/MediaGalleryPage')); // Import Media Gallery page
// Settings pages
const SettingsLayout = React.lazy(() => import('./layouts/SettingsLayout'));
@@ -368,7 +370,28 @@ const AppContent: React.FC = () => {
);
}
// For root domain or platform subdomain, show marketing site / login
// For platform subdomain, only /platform/login exists - everything else renders nothing
if (isPlatformSubdomain) {
const path = window.location.pathname;
const allowedPaths = ['/platform/login', '/mfa-verify', '/verify-email'];
// If not an allowed path, render nothing
if (!allowedPaths.includes(path)) {
return null;
}
return (
<Suspense fallback={<LoadingScreen />}>
<Routes>
<Route path="/platform/login" element={<PlatformLoginPage />} />
<Route path="/mfa-verify" element={<MFAVerifyPage />} />
<Route path="/verify-email" element={<VerifyEmail />} />
</Routes>
</Suspense>
);
}
// For root domain, show marketing site with business user login
return (
<Suspense fallback={<LoadingScreen />}>
<Routes>
@@ -660,6 +683,13 @@ const AppContent: React.FC = () => {
return (
<Suspense fallback={<LoadingScreen />}>
<Routes>
{/* Public routes outside BusinessLayout */}
<Route path="/" element={<PublicPage />} />
<Route path="/book" element={<BookingFlow />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/sign/:token" element={<ContractSigning />} />
{/* Dashboard routes inside BusinessLayout */}
<Route
element={
<BusinessLayout
@@ -672,9 +702,6 @@ const AppContent: React.FC = () => {
/>
}
>
{/* Redirect root to dashboard */}
<Route path="/" element={<Navigate to="/dashboard" replace />} />
{/* Trial and Upgrade Routes */}
<Route path="/dashboard/trial-expired" element={<TrialExpired />} />
<Route path="/dashboard/upgrade" element={<Upgrade />} />
@@ -902,6 +929,16 @@ const AppContent: React.FC = () => {
)
}
/>
<Route
path="/dashboard/gallery"
element={
hasAccess(['owner', 'manager']) ? (
<MediaGalleryPage />
) : (
<Navigate to="/dashboard" />
)
}
/>
{/* Settings Routes with Nested Layout */}
{hasAccess(['owner']) ? (
<Route path="/dashboard/settings" element={<SettingsLayout />}>
@@ -925,8 +962,10 @@ const AppContent: React.FC = () => {
)}
<Route path="/dashboard/profile" element={<ProfileSettings />} />
<Route path="/dashboard/verify-email" element={<VerifyEmail />} />
<Route path="*" element={<Navigate to="/dashboard" />} />
</Route>
{/* Catch-all redirects to home */}
<Route path="*" element={<Navigate to="/" />} />
</Routes>
</Suspense>
);