updated general project settings
This commit is contained in:
parent
024c04e05a
commit
3d06d927cf
@ -97,7 +97,8 @@ After seeding, login with credentials configured in `backend/.env`:
|
||||
└── docker/ # Docker Compose setup
|
||||
├── docker-compose.yml
|
||||
├── start-backend.sh
|
||||
└── wait-for-it.sh
|
||||
├── wait-for-it.sh
|
||||
└── README.md # Docker documentation
|
||||
```
|
||||
|
||||
## Key Workflows
|
||||
@ -225,7 +226,7 @@ EMAIL_PASS=...
|
||||
### Frontend (`frontend/.env.local`)
|
||||
|
||||
```env
|
||||
NEXT_PUBLIC_BACK_API=http://localhost:8080
|
||||
NEXT_PUBLIC_BACK_API=http://localhost:8080/api
|
||||
```
|
||||
|
||||
## Common Commands
|
||||
|
||||
@ -85,28 +85,33 @@ backend/src/
|
||||
├── helpers.js # Utility functions (wrapAsync)
|
||||
│
|
||||
├── auth/ # Passport.js authentication strategies
|
||||
│ └── passport.js # JWT, Google, Microsoft strategies
|
||||
│ └── auth.js # JWT, Google, Microsoft strategies
|
||||
│
|
||||
├── db/
|
||||
│ ├── models/ # Sequelize model definitions
|
||||
│ ├── db.config.js # Database connection config (per environment)
|
||||
│ ├── models/ # Sequelize model definitions (16 models)
|
||||
│ ├── api/ # Database access layer (CRUD per model)
|
||||
│ ├── migrations/ # Database migrations
|
||||
│ └── seeders/ # Seed data (admin users, permissions, roles)
|
||||
│
|
||||
├── routes/ # Express route handlers
|
||||
├── routes/ # Express route handlers (22 routes)
|
||||
│ ├── auth.js # Authentication endpoints
|
||||
│ ├── projects.js # Project CRUD + publishing
|
||||
│ ├── projects.js # Project CRUD
|
||||
│ ├── tour_pages.js # Tour page management
|
||||
│ ├── assets.js # Asset management
|
||||
│ ├── file.js # File upload/download, presigned URLs
|
||||
│ ├── publish.js # Publishing workflow
|
||||
│ ├── search.js # Global search
|
||||
│ └── ... # Other entity routes
|
||||
│
|
||||
├── services/ # Business logic layer
|
||||
├── services/ # Business logic layer (21 services)
|
||||
│ ├── auth.js # Auth service (JWT, OAuth)
|
||||
│ ├── publish.js # Publishing workflow logic
|
||||
│ ├── file.js # File storage abstraction
|
||||
│ ├── search.js # Global search service
|
||||
│ ├── email/ # Email templates and sending
|
||||
│ └── ... # Other services
|
||||
│ ├── notifications/ # Error classes and i18n messages
|
||||
│ └── ... # Other entity services
|
||||
│
|
||||
├── middlewares/
|
||||
│ ├── check-permissions.js # RBAC permission checking
|
||||
@ -120,8 +125,10 @@ backend/src/
|
||||
│ └── service.factory.js # Generate service classes
|
||||
│
|
||||
└── utils/
|
||||
├── env-validation.js # Environment variable validation
|
||||
└── ...
|
||||
├── env-validation.js # Environment variable validation (Joi)
|
||||
├── errors.js # Custom error classes
|
||||
├── logger.js # Pino logger configuration
|
||||
└── index.js # Utils barrel export
|
||||
```
|
||||
|
||||
## Database Setup
|
||||
@ -196,6 +203,10 @@ DELETE /api/{entity}/:id # Soft delete record
|
||||
| `element_type_defaults` | Global element default settings |
|
||||
| `project_element_defaults` | Project-specific element settings |
|
||||
| `project_audio_tracks` | Background audio for projects |
|
||||
| `publish_events` | Publishing history and status tracking |
|
||||
| `pwa_caches` | PWA cache manifests for offline support |
|
||||
| `presigned_url_requests` | S3 presigned URL request tracking |
|
||||
| `access_logs` | User access audit trail |
|
||||
| `users` | User accounts |
|
||||
| `roles` | User roles |
|
||||
| `permissions` | Granular permissions |
|
||||
|
||||
@ -23,9 +23,6 @@ class ProjectsDBApi extends GenericDBApi {
|
||||
'logo_url',
|
||||
'favicon_url',
|
||||
'og_image_url',
|
||||
'theme_config_json',
|
||||
'custom_css_json',
|
||||
'cdn_base_url',
|
||||
];
|
||||
}
|
||||
|
||||
@ -44,7 +41,6 @@ class ProjectsDBApi extends GenericDBApi {
|
||||
'slug',
|
||||
'description',
|
||||
'logo_url',
|
||||
'cdn_base_url',
|
||||
'createdAt',
|
||||
];
|
||||
}
|
||||
@ -66,9 +62,6 @@ class ProjectsDBApi extends GenericDBApi {
|
||||
logo_url: data.logo_url || null,
|
||||
favicon_url: data.favicon_url || null,
|
||||
og_image_url: data.og_image_url || null,
|
||||
theme_config_json: data.theme_config_json || null,
|
||||
custom_css_json: data.custom_css_json || null,
|
||||
cdn_base_url: data.cdn_base_url || null,
|
||||
};
|
||||
}
|
||||
|
||||
@ -95,7 +88,8 @@ class ProjectsDBApi extends GenericDBApi {
|
||||
const queryWhere = { ...where };
|
||||
|
||||
// Runtime access: filter by project slug
|
||||
if (runtimeProjectSlug) {
|
||||
// Skip if finding by ID (unambiguous lookup)
|
||||
if (runtimeProjectSlug && !where.id) {
|
||||
queryWhere.slug = runtimeProjectSlug;
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
async up(queryInterface, _Sequelize) {
|
||||
await queryInterface.removeColumn('projects', 'theme_config_json');
|
||||
await queryInterface.removeColumn('projects', 'custom_css_json');
|
||||
await queryInterface.removeColumn('projects', 'cdn_base_url');
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn('projects', 'theme_config_json', {
|
||||
type: Sequelize.JSON,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('projects', 'custom_css_json', {
|
||||
type: Sequelize.JSON,
|
||||
allowNull: true,
|
||||
});
|
||||
await queryInterface.addColumn('projects', 'cdn_base_url', {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
};
|
||||
@ -53,18 +53,6 @@ module.exports = function (sequelize, DataTypes) {
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
|
||||
theme_config_json: {
|
||||
type: DataTypes.JSON,
|
||||
},
|
||||
|
||||
custom_css_json: {
|
||||
type: DataTypes.JSON,
|
||||
},
|
||||
|
||||
cdn_base_url: {
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
|
||||
importHash: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
|
||||
@ -24,76 +24,32 @@ const AccessLogs = db.access_logs;
|
||||
const ProjectsData = [
|
||||
{
|
||||
name: 'Cardiff Arena Tour',
|
||||
|
||||
slug: 'cardiff-arena',
|
||||
|
||||
description: 'Interactive arena tour for visitors and event planners.',
|
||||
|
||||
logo_url: 'https://cdn.platform.com/cardiff/logo.png',
|
||||
|
||||
favicon_url: 'https://cdn.platform.com/cardiff/favicon.ico',
|
||||
|
||||
og_image_url: 'https://cdn.platform.com/cardiff/og.jpg',
|
||||
|
||||
theme_config_json:
|
||||
'{colors:{primary:#0EA5E9,background:#0B1220},fonts:{base:Inter}}',
|
||||
|
||||
custom_css_json: '{buttons:{radius:14px}}',
|
||||
|
||||
cdn_base_url: 'https://cdn.platform.com/cardiff',
|
||||
|
||||
is_deleted: true,
|
||||
|
||||
deleted_at_time: new Date('2026-01-01T00:00:00Z'),
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Riverside Park Walkthrough',
|
||||
|
||||
slug: 'riverside-park',
|
||||
|
||||
description: 'Offline-ready guided walkthrough for the city park.',
|
||||
|
||||
logo_url: 'https://cdn.platform.com/riverside/logo.png',
|
||||
|
||||
favicon_url: 'https://cdn.platform.com/riverside/favicon.ico',
|
||||
|
||||
og_image_url: 'https://cdn.platform.com/riverside/og.jpg',
|
||||
|
||||
theme_config_json:
|
||||
'{colors:{primary:#22C55E,background:#FFFFFF},fonts:{base:Manrope}}',
|
||||
|
||||
custom_css_json: '{header:{shadow:md}}',
|
||||
|
||||
cdn_base_url: 'https://cdn.platform.com/riverside',
|
||||
|
||||
is_deleted: false,
|
||||
|
||||
deleted_at_time: new Date('2026-01-01T00:00:00Z'),
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Mall Central Experience',
|
||||
|
||||
slug: 'mall-central',
|
||||
|
||||
description: 'Retail complex presentation with navigation and galleries.',
|
||||
|
||||
logo_url: 'https://cdn.platform.com/mall/logo.png',
|
||||
|
||||
favicon_url: 'https://cdn.platform.com/mall/favicon.ico',
|
||||
|
||||
og_image_url: 'https://cdn.platform.com/mall/og.jpg',
|
||||
|
||||
theme_config_json:
|
||||
'{colors:{primary:#A855F7,background:#0F172A},fonts:{base:Poppins}}',
|
||||
|
||||
custom_css_json: '{cards:{border:1px solid rgba(255,255,255,0.12)}}',
|
||||
|
||||
cdn_base_url: 'https://cdn.platform.com/mall',
|
||||
|
||||
is_deleted: false,
|
||||
|
||||
deleted_at_time: new Date('2026-01-01T00:00:00Z'),
|
||||
},
|
||||
];
|
||||
|
||||
@ -9,9 +9,6 @@ const PUBLIC_RUNTIME_ENTITY_FIELDS = {
|
||||
'logo_url',
|
||||
'favicon_url',
|
||||
'og_image_url',
|
||||
'theme_config_json',
|
||||
'custom_css_json',
|
||||
'cdn_base_url',
|
||||
],
|
||||
tour_pages: [
|
||||
'id',
|
||||
|
||||
@ -38,19 +38,10 @@ router.use(checkCrudPermissions('projects'));
|
||||
* og_image_url:
|
||||
* type: string
|
||||
* default: og_image_url
|
||||
* theme_config_json:
|
||||
* type: string
|
||||
* default: theme_config_json
|
||||
* custom_css_json:
|
||||
* type: string
|
||||
* default: custom_css_json
|
||||
* cdn_base_url:
|
||||
* type: string
|
||||
* default: cdn_base_url
|
||||
|
||||
|
||||
|
||||
*
|
||||
|
||||
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -357,9 +348,6 @@ router.get(
|
||||
'logo_url',
|
||||
'favicon_url',
|
||||
'og_image_url',
|
||||
'theme_config_json',
|
||||
'custom_css_json',
|
||||
'cdn_base_url',
|
||||
];
|
||||
const opts = { fields };
|
||||
try {
|
||||
|
||||
@ -98,9 +98,6 @@ module.exports = class ProjectsService {
|
||||
logo_url: sourceProject.logo_url,
|
||||
favicon_url: sourceProject.favicon_url,
|
||||
og_image_url: sourceProject.og_image_url,
|
||||
theme_config_json: sourceProject.theme_config_json,
|
||||
custom_css_json: sourceProject.custom_css_json,
|
||||
cdn_base_url: sourceProject.cdn_base_url,
|
||||
},
|
||||
{
|
||||
currentUser,
|
||||
|
||||
@ -49,22 +49,11 @@ module.exports = class SearchService {
|
||||
|
||||
projects: [
|
||||
'name',
|
||||
|
||||
'slug',
|
||||
|
||||
'description',
|
||||
|
||||
'logo_url',
|
||||
|
||||
'favicon_url',
|
||||
|
||||
'og_image_url',
|
||||
|
||||
'theme_config_json',
|
||||
|
||||
'custom_css_json',
|
||||
|
||||
'cdn_base_url',
|
||||
],
|
||||
|
||||
assets: ['name', 'cdn_url', 'storage_key', 'mime_type', 'checksum'],
|
||||
|
||||
@ -95,17 +95,19 @@ frontend/src/
|
||||
│ └── {entity}/ # Entity-specific slices
|
||||
│ └── {entity}Slice.ts
|
||||
│
|
||||
├── hooks/ # Custom React hooks
|
||||
│ ├── useFormSync.ts # Form state synchronization
|
||||
│ ├── useEntityTable.ts # Data grid with CRUD
|
||||
├── hooks/ # Custom React hooks (32 hooks)
|
||||
│ ├── usePreloadOrchestrator.ts # Asset preloading with S3 presigned URLs
|
||||
│ ├── usePageSwitch.ts # Page navigation with preloaded blob URLs
|
||||
│ ├── useNeighborGraph.ts # Page navigation graph (maxDepth: 1)
|
||||
│ ├── useTransitionPlayback.ts # Video transitions
|
||||
│ ├── usePageNavigation.ts # Runtime page history
|
||||
│ ├── useReversePlayback.ts # Reverse video playback
|
||||
│ ├── useConstructorElements.ts # Manage canvas elements
|
||||
│ ├── useConstructorPageActions.ts # Save, publish, create pages
|
||||
│ ├── useOfflineMode.ts # PWA offline detection
|
||||
│ └── ... # Other hooks
|
||||
│ ├── useFormSync.ts # Form state synchronization
|
||||
│ ├── useEntityTable.ts # Data grid with CRUD
|
||||
│ └── ... # Other hooks (see Custom Hooks section)
|
||||
│
|
||||
├── types/ # TypeScript definitions
|
||||
│ ├── constructor.ts # Tour builder types
|
||||
@ -116,16 +118,28 @@ frontend/src/
|
||||
│ └── ... # Other types
|
||||
│
|
||||
├── lib/ # Utility libraries
|
||||
│ ├── elementStyles.ts # Element styling utilities
|
||||
│ ├── imagePreDecode.ts # Image pre-decoding
|
||||
│ ├── mediaDuration.ts # Video/audio duration
|
||||
│ ├── assetUrl.ts # CDN URL resolution
|
||||
│ ├── constructorHelpers.ts # Constructor page helpers
|
||||
│ ├── elementDefaults.ts # Element default values
|
||||
│ ├── elementEffects.ts # Element effect utilities
|
||||
│ ├── elementStyles.ts # Element styling utilities
|
||||
│ ├── extractPageLinks.ts # Extract navigation links from pages
|
||||
│ ├── parseJson.ts # Safe JSON parsing
|
||||
│ ├── fonts.ts # Font configuration
|
||||
│ ├── imagePreDecode.ts # Image pre-decoding
|
||||
│ ├── logger.ts # Client-side logging
|
||||
│ ├── mediaDuration.ts # Video/audio duration
|
||||
│ ├── mediaHelpers.ts # Media utilities
|
||||
│ ├── navigationHelpers.ts # Navigation utilities
|
||||
│ ├── parseJson.ts # Safe JSON parsing
|
||||
│ ├── slugHelpers.ts # Slug generation utilities
|
||||
│ ├── tourFlowHelpers.ts # Tour flow utilities
|
||||
│ ├── offline/ # Offline utilities
|
||||
│ │ └── StorageManager.ts # Cache API storage for assets
|
||||
│ └── offlineDb/ # IndexedDB (Dexie) setup
|
||||
│ │ ├── DownloadEventBus.ts # Download event handling
|
||||
│ │ ├── DownloadManager.ts # Download queue management
|
||||
│ │ └── StorageManager.ts # Cache API storage for assets
|
||||
│ └── offlineDb/ # IndexedDB (Dexie)
|
||||
│ ├── schema.ts # Dexie database schema
|
||||
│ └── OfflineDbManager.ts # Offline data management
|
||||
│
|
||||
├── layouts/ # Page layouts
|
||||
│ ├── Authenticated.tsx # Logged-in users layout
|
||||
@ -135,6 +149,7 @@ frontend/src/
|
||||
│ └── _theme.css # Tailwind theme overrides
|
||||
│
|
||||
├── config/ # Configuration files
|
||||
│ ├── offline.config.ts # Offline/PWA settings
|
||||
│ └── preload.config.ts # Preload priorities and settings
|
||||
├── schemas/ # Validation schemas (Zod)
|
||||
├── factories/ # Component/hook factories
|
||||
@ -207,22 +222,64 @@ dispatch(create({ data: newProject }));
|
||||
|
||||
## Custom Hooks
|
||||
|
||||
### Runtime & Preloading Hooks
|
||||
|
||||
| Hook | Purpose |
|
||||
|------|---------|
|
||||
| `useFormSync` | Sync form state with Redux |
|
||||
| `useEntityTable` | Data grid with sorting, filtering, pagination |
|
||||
| `usePreloadOrchestrator` | Asset preloading with S3 presigned URLs and ready blob URLs |
|
||||
| `usePageSwitch` | Page navigation using preloaded blob URLs (O(1) instant lookup) |
|
||||
| `useNeighborGraph` | Build page navigation graph (maxDepth: 1) |
|
||||
| `useTransitionPlayback` | Video transition playback coordination |
|
||||
| `usePageNavigation` | Runtime page history management |
|
||||
| `useReversePlayback` | Reverse video playback |
|
||||
| `usePageDataLoader` | Load pages with environment filtering |
|
||||
| `usePreloadProgress` | Track preload progress |
|
||||
| `useBackgroundTransition` | Background transition effects |
|
||||
|
||||
### PWA & Offline Hooks
|
||||
|
||||
| Hook | Purpose |
|
||||
|------|---------|
|
||||
| `useOfflineMode` | Detect offline/online status |
|
||||
| `usePWAPreload` | Preload assets for offline |
|
||||
| `useStorageQuota` | Monitor IndexedDB usage |
|
||||
| `useNetworkAware` | Network-aware operations |
|
||||
|
||||
### Constructor Hooks
|
||||
|
||||
| Hook | Purpose |
|
||||
|------|---------|
|
||||
| `useConstructorElements` | Manage canvas elements |
|
||||
| `useConstructorPageActions` | Save, publish, create pages |
|
||||
| `useCanvasElementDrag` | Element drag-and-drop |
|
||||
| `useCanvasElapsedTime` | Track canvas elapsed time |
|
||||
| `useTransitionPreview` | Preview transitions |
|
||||
| `useElementEffects` | Element visual effects |
|
||||
| `useIconPreload` | Preload element icons |
|
||||
| `useMediaDurationProbe` | Probe media duration |
|
||||
|
||||
### Form & Table Hooks
|
||||
|
||||
| Hook | Purpose |
|
||||
|------|---------|
|
||||
| `useFormSync` | Sync form state with Redux |
|
||||
| `useEntityTable` | Data grid with sorting, filtering, pagination |
|
||||
| `useEditPageSync` | Sync edit page state |
|
||||
| `useFilterItems` | Filter items in lists |
|
||||
| `useCSVHandling` | CSV import/export |
|
||||
|
||||
### Utility Hooks
|
||||
|
||||
| Hook | Purpose |
|
||||
|------|---------|
|
||||
| `useDraggable` | Generic draggable behavior |
|
||||
| `useOutsideClick` | Detect clicks outside element |
|
||||
| `useDashboardCounts` | Dashboard statistics |
|
||||
| `useProjectAssets` | Project asset management |
|
||||
| `useDevCompilationStatus` | Dev server compilation status |
|
||||
|
||||
**Component-specific hooks:**
|
||||
- `useElementSettingsForm` (`components/ElementSettings/`) - Element settings form state (~60 fields)
|
||||
- `useElementSettingsForm` (`components/ElementSettings/`) - Element settings form state
|
||||
|
||||
## Element Types
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1,122 +0,0 @@
|
||||
/**
|
||||
* PWA Loading Overlay Component
|
||||
*
|
||||
* Displays a "We prepare a demo" overlay during first load
|
||||
* with pulsating animation and progress tracking for asset preloading.
|
||||
* Uses project theme colors from theme_config_json when available.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
type PWALoadingOverlayProps = {
|
||||
isVisible: boolean;
|
||||
progress: number; // 0-100
|
||||
projectName?: string;
|
||||
themeConfig?: {
|
||||
primaryColor?: string;
|
||||
backgroundColor?: string;
|
||||
textColor?: string;
|
||||
};
|
||||
onComplete?: () => void;
|
||||
};
|
||||
|
||||
const PWALoadingOverlay: React.FC<PWALoadingOverlayProps> = ({
|
||||
isVisible,
|
||||
progress,
|
||||
projectName,
|
||||
themeConfig,
|
||||
onComplete,
|
||||
}) => {
|
||||
const [shouldRender, setShouldRender] = useState(isVisible);
|
||||
|
||||
// Handle fade-out animation before unmounting
|
||||
useEffect(() => {
|
||||
if (!isVisible && shouldRender) {
|
||||
const timer = setTimeout(() => {
|
||||
setShouldRender(false);
|
||||
onComplete?.();
|
||||
}, 500); // Match CSS transition duration
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
if (isVisible) {
|
||||
setShouldRender(true);
|
||||
}
|
||||
}, [isVisible, shouldRender, onComplete]);
|
||||
|
||||
if (!shouldRender) return null;
|
||||
|
||||
const primaryColor = themeConfig?.primaryColor || '#3b82f6';
|
||||
const backgroundColor = themeConfig?.backgroundColor || '#1f2937';
|
||||
const textColor = themeConfig?.textColor || '#ffffff';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed inset-0 z-[9999] flex flex-col items-center justify-center transition-opacity duration-500 ${
|
||||
isVisible ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
||||
}`}
|
||||
style={{ backgroundColor }}
|
||||
>
|
||||
{/* Pulsating logo/spinner */}
|
||||
<div
|
||||
className='mb-8 h-20 w-20 rounded-full animate-pulse'
|
||||
style={{
|
||||
backgroundColor: primaryColor,
|
||||
boxShadow: `0 0 60px ${primaryColor}40`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Project name */}
|
||||
{projectName && (
|
||||
<h1
|
||||
className='mb-4 text-2xl font-bold tracking-wide'
|
||||
style={{ color: textColor }}
|
||||
>
|
||||
{projectName}
|
||||
</h1>
|
||||
)}
|
||||
|
||||
{/* Loading message */}
|
||||
<p
|
||||
className='mb-6 text-lg animate-pulse'
|
||||
style={{ color: textColor, opacity: 0.9 }}
|
||||
>
|
||||
We prepare a demo
|
||||
</p>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div
|
||||
className='w-64 h-2 rounded-full overflow-hidden'
|
||||
style={{ backgroundColor: `${textColor}20` }}
|
||||
>
|
||||
<div
|
||||
className='h-full rounded-full transition-all duration-300 ease-out'
|
||||
style={{
|
||||
width: `${Math.min(100, Math.max(0, progress))}%`,
|
||||
backgroundColor: primaryColor,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Progress percentage */}
|
||||
<p className='mt-3 text-sm' style={{ color: textColor, opacity: 0.7 }}>
|
||||
{Math.round(progress)}%
|
||||
</p>
|
||||
|
||||
{/* Loading indicator dots */}
|
||||
<div className='mt-8 flex gap-2'>
|
||||
{[0, 1, 2].map((index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='h-2 w-2 rounded-full animate-bounce'
|
||||
style={{
|
||||
backgroundColor: primaryColor,
|
||||
animationDelay: `${index * 0.15}s`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PWALoadingOverlay;
|
||||
@ -132,39 +132,6 @@ const CardProjects = ({
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div className='flex justify-between gap-x-4 py-3'>
|
||||
<dt className=' text-gray-500 dark:text-dark-600'>
|
||||
ThemeconfigJSON
|
||||
</dt>
|
||||
<dd className='flex items-start gap-x-2'>
|
||||
<div className='font-medium line-clamp-4'>
|
||||
{item.theme_config_json}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div className='flex justify-between gap-x-4 py-3'>
|
||||
<dt className=' text-gray-500 dark:text-dark-600'>
|
||||
CustomCSSJSON
|
||||
</dt>
|
||||
<dd className='flex items-start gap-x-2'>
|
||||
<div className='font-medium line-clamp-4'>
|
||||
{item.custom_css_json}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div className='flex justify-between gap-x-4 py-3'>
|
||||
<dt className=' text-gray-500 dark:text-dark-600'>
|
||||
CDNbaseURL
|
||||
</dt>
|
||||
<dd className='flex items-start gap-x-2'>
|
||||
<div className='font-medium line-clamp-4'>
|
||||
{item.cdn_base_url}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div className='flex justify-between gap-x-4 py-3'>
|
||||
<dt className=' text-gray-500 dark:text-dark-600'>
|
||||
Isdeleted
|
||||
|
||||
@ -81,25 +81,6 @@ const ListProjects = ({
|
||||
<p className={'line-clamp-2'}>{item.og_image_url}</p>
|
||||
</div>
|
||||
|
||||
<div className={'flex-1 px-3'}>
|
||||
<p className={'text-xs text-gray-500 '}>
|
||||
ThemeconfigJSON
|
||||
</p>
|
||||
<p className={'line-clamp-2'}>{item.theme_config_json}</p>
|
||||
</div>
|
||||
|
||||
<div className={'flex-1 px-3'}>
|
||||
<p className={'text-xs text-gray-500 '}>
|
||||
CustomCSSJSON
|
||||
</p>
|
||||
<p className={'line-clamp-2'}>{item.custom_css_json}</p>
|
||||
</div>
|
||||
|
||||
<div className={'flex-1 px-3'}>
|
||||
<p className={'text-xs text-gray-500 '}>CDNbaseURL</p>
|
||||
<p className={'line-clamp-2'}>{item.cdn_base_url}</p>
|
||||
</div>
|
||||
|
||||
<div className={'flex-1 px-3'}>
|
||||
<p className={'text-xs text-gray-500 '}>Isdeleted</p>
|
||||
<p className={'line-clamp-2'}>
|
||||
|
||||
@ -25,24 +25,6 @@ const PROJECTS_COLUMNS: ColumnMetadata[] = [
|
||||
type: 'text',
|
||||
editable: true,
|
||||
},
|
||||
{
|
||||
field: 'theme_config_json',
|
||||
headerName: 'ThemeconfigJSON',
|
||||
type: 'text',
|
||||
editable: true,
|
||||
},
|
||||
{
|
||||
field: 'custom_css_json',
|
||||
headerName: 'CustomCSSJSON',
|
||||
type: 'text',
|
||||
editable: true,
|
||||
},
|
||||
{
|
||||
field: 'cdn_base_url',
|
||||
headerName: 'CDNbaseURL',
|
||||
type: 'text',
|
||||
editable: true,
|
||||
},
|
||||
{
|
||||
field: 'is_deleted',
|
||||
headerName: 'Isdeleted',
|
||||
|
||||
@ -22,9 +22,9 @@ import { OfflineToggle } from './Offline/OfflineToggle';
|
||||
import RuntimeElement from './RuntimeElement';
|
||||
import GalleryCarouselOverlay from './UiElements/GalleryCarouselOverlay';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
import { getPageTitle } from '../config';
|
||||
import { usePreloadOrchestrator } from '../hooks/usePreloadOrchestrator';
|
||||
import { usePageDataLoader } from '../hooks/usePageDataLoader';
|
||||
import { useProjectAssets } from '../hooks/useProjectAssets';
|
||||
import { extractPageLinksAndElements } from '../lib/extractPageLinks';
|
||||
import { usePageSwitch } from '../hooks/usePageSwitch';
|
||||
import { useTransitionPlayback } from '../hooks/useTransitionPlayback';
|
||||
@ -37,26 +37,6 @@ import {
|
||||
} from '../lib/navigationHelpers';
|
||||
import type { TransitionPhase } from '../types/presentation';
|
||||
|
||||
/**
|
||||
* Parse custom_css_json from project for font styling
|
||||
*/
|
||||
const parseCustomCss = (
|
||||
json: string | Record<string, unknown> | null | undefined,
|
||||
): { fontFamily: string; fontStretch: string } => {
|
||||
const defaults = { fontFamily: '', fontStretch: '' };
|
||||
if (!json) return defaults;
|
||||
|
||||
try {
|
||||
const parsed = typeof json === 'string' ? JSON.parse(json) : json;
|
||||
return {
|
||||
fontFamily: String(parsed?.fontFamily || ''),
|
||||
fontStretch: String(parsed?.fontStretch || ''),
|
||||
};
|
||||
} catch {
|
||||
return defaults;
|
||||
}
|
||||
};
|
||||
|
||||
interface RuntimePresentationProps {
|
||||
projectSlug: string;
|
||||
environment: 'stage' | 'production';
|
||||
@ -78,6 +58,9 @@ export default function RuntimePresentation({
|
||||
},
|
||||
);
|
||||
|
||||
// Resolve project assets (favicon, og_image, logo) to presigned URLs
|
||||
const { faviconUrl, ogImageUrl } = useProjectAssets(project);
|
||||
|
||||
const [selectedPageId, setSelectedPageId] = useState<string | null>(null);
|
||||
const [pageHistory, setPageHistory] = useState<string[]>([]);
|
||||
const [transitionPreview, setTransitionPreview] = useState<{
|
||||
@ -408,7 +391,42 @@ export default function RuntimePresentation({
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle(project?.name || 'Presentation')}</title>
|
||||
<title>{project?.name || 'Presentation'}</title>
|
||||
{faviconUrl && <link key="favicon" rel="icon" href={faviconUrl} />}
|
||||
{ogImageUrl && (
|
||||
<>
|
||||
<meta key="og:image" property="og:image" content={ogImageUrl} />
|
||||
<meta
|
||||
key="twitter:image:src"
|
||||
property="twitter:image:src"
|
||||
content={ogImageUrl}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{project?.name && (
|
||||
<>
|
||||
<meta key="og:title" property="og:title" content={project.name} />
|
||||
<meta
|
||||
key="twitter:title"
|
||||
property="twitter:title"
|
||||
content={project.name}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{project?.description && (
|
||||
<>
|
||||
<meta
|
||||
key="og:description"
|
||||
property="og:description"
|
||||
content={project.description}
|
||||
/>
|
||||
<meta
|
||||
key="twitter:description"
|
||||
property="twitter:description"
|
||||
content={project.description}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Head>
|
||||
|
||||
<div
|
||||
|
||||
95
frontend/src/hooks/useProjectAssets.ts
Normal file
95
frontend/src/hooks/useProjectAssets.ts
Normal file
@ -0,0 +1,95 @@
|
||||
/**
|
||||
* useProjectAssets Hook
|
||||
*
|
||||
* Preloads and resolves project assets (favicon, og_image, logo) to presigned URLs.
|
||||
* Handles both storage_key (relative paths) and legacy cdn_url (full S3 URLs) formats.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import {
|
||||
extractStoragePath,
|
||||
queuePresignedUrls,
|
||||
resolveAssetPlaybackUrl,
|
||||
} from '../lib/assetUrl';
|
||||
|
||||
interface ProjectAssets {
|
||||
faviconUrl: string | null;
|
||||
ogImageUrl: string | null;
|
||||
logoUrl: string | null;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
interface ProjectAssetInput {
|
||||
favicon_url?: string;
|
||||
og_image_url?: string;
|
||||
logo_url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to preload and resolve project assets (favicon, og_image, logo).
|
||||
* Extracts storage paths from URLs (handles both formats) and resolves them
|
||||
* to presigned URLs for access.
|
||||
*/
|
||||
export function useProjectAssets(project: ProjectAssetInput | null): ProjectAssets {
|
||||
const [assets, setAssets] = useState<ProjectAssets>({
|
||||
faviconUrl: null,
|
||||
ogImageUrl: null,
|
||||
logoUrl: null,
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
// Track the last project URLs to avoid redundant processing
|
||||
const lastUrlsRef = useRef<string>('');
|
||||
|
||||
const resolveAssets = useCallback(async () => {
|
||||
if (!project) {
|
||||
setAssets({
|
||||
faviconUrl: null,
|
||||
ogImageUrl: null,
|
||||
logoUrl: null,
|
||||
isLoading: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract storage paths from URLs (handles both formats)
|
||||
const faviconPath = extractStoragePath(project.favicon_url || '');
|
||||
const ogImagePath = extractStoragePath(project.og_image_url || '');
|
||||
const logoPath = extractStoragePath(project.logo_url || '');
|
||||
|
||||
// Collect paths that need presigning (relative paths only)
|
||||
const pathsToPresign = [faviconPath, ogImagePath, logoPath].filter(
|
||||
(path) => path && !path.startsWith('http'),
|
||||
);
|
||||
|
||||
// Queue presigned URL requests
|
||||
if (pathsToPresign.length > 0) {
|
||||
await queuePresignedUrls(pathsToPresign);
|
||||
}
|
||||
|
||||
// Resolve to playback URLs (presigned if available, otherwise proxy)
|
||||
setAssets({
|
||||
faviconUrl: faviconPath ? resolveAssetPlaybackUrl(faviconPath) : null,
|
||||
ogImageUrl: ogImagePath ? resolveAssetPlaybackUrl(ogImagePath) : null,
|
||||
logoUrl: logoPath ? resolveAssetPlaybackUrl(logoPath) : null,
|
||||
isLoading: false,
|
||||
});
|
||||
}, [project]);
|
||||
|
||||
useEffect(() => {
|
||||
// Create a key from the URLs to detect changes
|
||||
const urlsKey = `${project?.favicon_url || ''}|${project?.og_image_url || ''}|${project?.logo_url || ''}`;
|
||||
|
||||
// Skip if URLs haven't changed
|
||||
if (urlsKey === lastUrlsRef.current) {
|
||||
return;
|
||||
}
|
||||
lastUrlsRef.current = urlsKey;
|
||||
|
||||
// Reset loading state and resolve
|
||||
setAssets((prev) => ({ ...prev, isLoading: true }));
|
||||
resolveAssets();
|
||||
}, [project?.favicon_url, project?.og_image_url, project?.logo_url, resolveAssets]);
|
||||
|
||||
return assets;
|
||||
}
|
||||
@ -295,6 +295,24 @@ export const isRelativeStoragePath = (url: string): boolean => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract relative storage path from a full S3 URL.
|
||||
* Converts: https://bucket.s3.region.amazonaws.com/prefix/assets/projectId/file.ext
|
||||
* To: assets/projectId/file.ext
|
||||
* Returns original if already a relative path or not an S3 URL.
|
||||
*/
|
||||
export const extractStoragePath = (url: string): string => {
|
||||
const normalized = url?.trim() || '';
|
||||
if (!normalized) return '';
|
||||
if (isRelativeStoragePath(normalized)) return normalized;
|
||||
|
||||
// Extract path starting from 'assets/'
|
||||
const s3Match = normalized.match(
|
||||
/^https?:\/\/[^/]+\.s3\.[^/]+\.amazonaws\.com\/[^/]+\/(assets\/.+)$/,
|
||||
);
|
||||
return s3Match ? s3Match[1] : normalized;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves an asset path to its full playback URL.
|
||||
*
|
||||
|
||||
@ -282,19 +282,19 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
<meta name='description' content={description} />
|
||||
<meta property='og:url' content={url} />
|
||||
<meta property='og:site_name' content='https://flatlogic.com/' />
|
||||
<meta property='og:title' content={title} />
|
||||
<meta property='og:description' content={description} />
|
||||
<meta property='og:image' content={image} />
|
||||
<meta key='og:title' property='og:title' content={title} />
|
||||
<meta key='og:description' property='og:description' content={description} />
|
||||
<meta key='og:image' property='og:image' content={image} />
|
||||
<meta property='og:image:type' content='image/png' />
|
||||
<meta property='og:image:width' content={imageWidth} />
|
||||
<meta property='og:image:height' content={imageHeight} />
|
||||
<meta property='twitter:card' content='summary_large_image' />
|
||||
<meta property='twitter:title' content={title} />
|
||||
<meta property='twitter:description' content={description} />
|
||||
<meta property='twitter:image:src' content={image} />
|
||||
<meta key='twitter:title' property='twitter:title' content={title} />
|
||||
<meta key='twitter:description' property='twitter:description' content={description} />
|
||||
<meta key='twitter:image:src' property='twitter:image:src' content={image} />
|
||||
<meta property='twitter:image:width' content={imageWidth} />
|
||||
<meta property='twitter:image:height' content={imageHeight} />
|
||||
<link rel='icon' href='/favicon.svg' />
|
||||
<link key='favicon' rel='icon' href='/favicon.svg' />
|
||||
<link rel='manifest' href='/manifest.json' />
|
||||
<meta name='theme-color' content='#3B82F6' />
|
||||
<meta name='mobile-web-app-capable' content='yes' />
|
||||
|
||||
@ -26,7 +26,6 @@ import { useAppDispatch, useAppSelector } from '../../stores/hooks';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { Project } from '../../types/entities';
|
||||
import { logger } from '../../lib/logger';
|
||||
import { FONT_OPTIONS, getFontByKey, getFontKeyFromValues } from '../../lib/fonts';
|
||||
|
||||
const initVals = {
|
||||
name: '',
|
||||
@ -35,107 +34,16 @@ const initVals = {
|
||||
logo_url: '',
|
||||
favicon_url: '',
|
||||
og_image_url: '',
|
||||
// Theme config fields (stored as JSON in theme_config_json)
|
||||
themePrimaryColor: '',
|
||||
themeBackgroundColor: '',
|
||||
themeTextColor: '',
|
||||
// Custom CSS fields (stored as JSON in custom_css_json)
|
||||
customFontFamily: '',
|
||||
customFontStretch: '',
|
||||
cdn_base_url: '',
|
||||
is_deleted: false,
|
||||
deleted_at_time: new Date(),
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse theme_config_json into individual fields
|
||||
*/
|
||||
const parseThemeConfig = (
|
||||
json: string | Record<string, unknown> | null | undefined,
|
||||
): { primaryColor: string; backgroundColor: string; textColor: string } => {
|
||||
const defaults = { primaryColor: '', backgroundColor: '', textColor: '' };
|
||||
if (!json) return defaults;
|
||||
|
||||
try {
|
||||
const parsed = typeof json === 'string' ? JSON.parse(json) : json;
|
||||
return {
|
||||
primaryColor: String(parsed?.primaryColor || ''),
|
||||
backgroundColor: String(parsed?.backgroundColor || ''),
|
||||
textColor: String(parsed?.textColor || ''),
|
||||
};
|
||||
} catch {
|
||||
return defaults;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse custom_css_json into individual fields
|
||||
*/
|
||||
const parseCustomCss = (
|
||||
json: string | Record<string, unknown> | null | undefined,
|
||||
): { fontFamily: string; fontStretch: string } => {
|
||||
const defaults = { fontFamily: '', fontStretch: '' };
|
||||
if (!json) return defaults;
|
||||
|
||||
try {
|
||||
const parsed = typeof json === 'string' ? JSON.parse(json) : json;
|
||||
return {
|
||||
fontFamily: String(parsed?.fontFamily || ''),
|
||||
fontStretch: String(parsed?.fontStretch || ''),
|
||||
};
|
||||
} catch {
|
||||
return defaults;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Build theme_config_json from individual fields
|
||||
*/
|
||||
const buildThemeConfigJson = (values: {
|
||||
themePrimaryColor: string;
|
||||
themeBackgroundColor: string;
|
||||
themeTextColor: string;
|
||||
}): Record<string, string> | null => {
|
||||
const config: Record<string, string> = {};
|
||||
|
||||
if (values.themePrimaryColor.trim()) {
|
||||
config.primaryColor = values.themePrimaryColor.trim();
|
||||
}
|
||||
if (values.themeBackgroundColor.trim()) {
|
||||
config.backgroundColor = values.themeBackgroundColor.trim();
|
||||
}
|
||||
if (values.themeTextColor.trim()) {
|
||||
config.textColor = values.themeTextColor.trim();
|
||||
}
|
||||
|
||||
return Object.keys(config).length > 0 ? config : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build custom_css_json from individual fields
|
||||
*/
|
||||
const buildCustomCssJson = (values: {
|
||||
customFontFamily: string;
|
||||
customFontStretch: string;
|
||||
}): Record<string, string> | null => {
|
||||
const config: Record<string, string> = {};
|
||||
|
||||
if (values.customFontFamily.trim()) {
|
||||
config.fontFamily = values.customFontFamily.trim();
|
||||
}
|
||||
if (values.customFontStretch.trim()) {
|
||||
config.fontStretch = values.customFontStretch.trim();
|
||||
}
|
||||
|
||||
return Object.keys(config).length > 0 ? config : null;
|
||||
};
|
||||
|
||||
const EditProjectsPage = () => {
|
||||
const router = useRouter();
|
||||
const dispatch = useAppDispatch();
|
||||
const [initialValues, setInitialValues] = useState(initVals);
|
||||
const [logoAssets, setLogoAssets] = useState<
|
||||
{ id: string; cdn_url: string; name: string }[]
|
||||
{ id: string; cdn_url: string; storage_key?: string; name: string }[]
|
||||
>([]);
|
||||
const [isLoadingLogoAssets, setIsLoadingLogoAssets] = useState(false);
|
||||
|
||||
@ -207,16 +115,6 @@ const EditProjectsPage = () => {
|
||||
if (typeof project === 'object' && project !== null) {
|
||||
const projectData = project as unknown as Record<string, unknown>;
|
||||
|
||||
// Parse theme_config_json into individual fields
|
||||
const themeConfig = parseThemeConfig(
|
||||
projectData.theme_config_json as string | Record<string, unknown>,
|
||||
);
|
||||
|
||||
// Parse custom_css_json into individual fields
|
||||
const customCss = parseCustomCss(
|
||||
projectData.custom_css_json as string | Record<string, unknown>,
|
||||
);
|
||||
|
||||
setInitialValues({
|
||||
name: String(projectData.name || ''),
|
||||
slug: String(projectData.slug || ''),
|
||||
@ -224,12 +122,6 @@ const EditProjectsPage = () => {
|
||||
logo_url: String(projectData.logo_url || ''),
|
||||
favicon_url: String(projectData.favicon_url || ''),
|
||||
og_image_url: String(projectData.og_image_url || ''),
|
||||
themePrimaryColor: themeConfig.primaryColor,
|
||||
themeBackgroundColor: themeConfig.backgroundColor,
|
||||
themeTextColor: themeConfig.textColor,
|
||||
customFontFamily: customCss.fontFamily,
|
||||
customFontStretch: customCss.fontStretch,
|
||||
cdn_base_url: String(projectData.cdn_base_url || ''),
|
||||
is_deleted: Boolean(projectData.is_deleted),
|
||||
deleted_at_time: projectData.deleted_at_time
|
||||
? new Date(projectData.deleted_at_time as string)
|
||||
@ -239,19 +131,6 @@ const EditProjectsPage = () => {
|
||||
}, [project]);
|
||||
|
||||
const handleSubmit = async (data: typeof initVals) => {
|
||||
// Build JSON fields from individual values
|
||||
const theme_config_json = buildThemeConfigJson({
|
||||
themePrimaryColor: data.themePrimaryColor,
|
||||
themeBackgroundColor: data.themeBackgroundColor,
|
||||
themeTextColor: data.themeTextColor,
|
||||
});
|
||||
|
||||
const custom_css_json = buildCustomCssJson({
|
||||
customFontFamily: data.customFontFamily,
|
||||
customFontStretch: data.customFontStretch,
|
||||
});
|
||||
|
||||
// Prepare data for API (exclude expanded fields, include JSON)
|
||||
const apiData: Partial<Project> = {
|
||||
name: data.name,
|
||||
slug: data.slug,
|
||||
@ -259,9 +138,6 @@ const EditProjectsPage = () => {
|
||||
logo_url: data.logo_url,
|
||||
favicon_url: data.favicon_url,
|
||||
og_image_url: data.og_image_url,
|
||||
theme_config_json: theme_config_json,
|
||||
custom_css_json: custom_css_json,
|
||||
cdn_base_url: data.cdn_base_url,
|
||||
};
|
||||
|
||||
await dispatch(update({ id: id as string, data: apiData }));
|
||||
@ -330,13 +206,13 @@ const EditProjectsPage = () => {
|
||||
: 'Select logo from Assets'}
|
||||
</option>
|
||||
{logoAssets.map((asset) => (
|
||||
<option key={asset.id} value={asset.cdn_url}>
|
||||
<option key={asset.id} value={asset.storage_key || asset.cdn_url}>
|
||||
{(asset.name || '').replace(/^\[[^\]]+\]\s*/, '')}
|
||||
</option>
|
||||
))}
|
||||
{values.logo_url &&
|
||||
!logoAssets.some(
|
||||
(asset) => asset.cdn_url === values.logo_url,
|
||||
(asset) => (asset.storage_key || asset.cdn_url) === values.logo_url,
|
||||
) && (
|
||||
<option value={values.logo_url}>
|
||||
{values.logo_url}
|
||||
@ -363,13 +239,13 @@ const EditProjectsPage = () => {
|
||||
: 'Select favicon from Assets logos'}
|
||||
</option>
|
||||
{logoAssets.map((asset) => (
|
||||
<option key={asset.id} value={asset.cdn_url}>
|
||||
<option key={asset.id} value={asset.storage_key || asset.cdn_url}>
|
||||
{(asset.name || '').replace(/^\[[^\]]+\]\s*/, '')}
|
||||
</option>
|
||||
))}
|
||||
{values.favicon_url &&
|
||||
!logoAssets.some(
|
||||
(asset) => asset.cdn_url === values.favicon_url,
|
||||
(asset) => (asset.storage_key || asset.cdn_url) === values.favicon_url,
|
||||
) && (
|
||||
<option value={values.favicon_url}>
|
||||
{values.favicon_url}
|
||||
@ -396,13 +272,13 @@ const EditProjectsPage = () => {
|
||||
: 'Select OG image from Assets logos'}
|
||||
</option>
|
||||
{logoAssets.map((asset) => (
|
||||
<option key={asset.id} value={asset.cdn_url}>
|
||||
<option key={asset.id} value={asset.storage_key || asset.cdn_url}>
|
||||
{(asset.name || '').replace(/^\[[^\]]+\]\s*/, '')}
|
||||
</option>
|
||||
))}
|
||||
{values.og_image_url &&
|
||||
!logoAssets.some(
|
||||
(asset) => asset.cdn_url === values.og_image_url,
|
||||
(asset) => (asset.storage_key || asset.cdn_url) === values.og_image_url,
|
||||
) && (
|
||||
<option value={values.og_image_url}>
|
||||
{values.og_image_url}
|
||||
@ -421,56 +297,6 @@ const EditProjectsPage = () => {
|
||||
</a>
|
||||
)}
|
||||
|
||||
<FormField label='Theme Primary Color'>
|
||||
<Field name='themePrimaryColor' placeholder='e.g. #E7DDB5' />
|
||||
</FormField>
|
||||
|
||||
<FormField label='Theme Background Color'>
|
||||
<Field
|
||||
name='themeBackgroundColor'
|
||||
placeholder='e.g. #131C22'
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label='Theme Text Color'>
|
||||
<Field name='themeTextColor' placeholder='e.g. #FFFFFF' />
|
||||
</FormField>
|
||||
|
||||
<FormField label='Custom Font Family'>
|
||||
<Field name='customFontFamily'>
|
||||
{({ field, form }: { field: { value: string }; form: { setFieldValue: (name: string, value: string) => void; values: typeof initVals } }) => (
|
||||
<select
|
||||
className='w-full rounded border border-gray-300 px-3 py-2'
|
||||
value={getFontKeyFromValues(form.values.customFontFamily, form.values.customFontStretch)}
|
||||
onChange={(e) => {
|
||||
const fontKey = e.target.value;
|
||||
if (!fontKey) {
|
||||
form.setFieldValue('customFontFamily', '');
|
||||
form.setFieldValue('customFontStretch', '');
|
||||
} else {
|
||||
const font = getFontByKey(fontKey);
|
||||
if (font) {
|
||||
form.setFieldValue('customFontFamily', font.fontFamily);
|
||||
form.setFieldValue('customFontStretch', font.fontStretch || '');
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value=''>Not set</option>
|
||||
{FONT_OPTIONS.map((font) => (
|
||||
<option key={font.key} value={font.key}>
|
||||
{font.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
<FormField label='CDN Base URL'>
|
||||
<Field name='cdn_base_url' placeholder='CDN Base URL' />
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type='submit' color='info' label='Submit' />
|
||||
|
||||
@ -32,9 +32,6 @@ const initialValues = {
|
||||
logo_url: '',
|
||||
favicon_url: '',
|
||||
og_image_url: '',
|
||||
theme_config_json: '',
|
||||
custom_css_json: '',
|
||||
cdn_base_url: '',
|
||||
is_deleted: false,
|
||||
deleted_at_time: '',
|
||||
};
|
||||
@ -97,26 +94,6 @@ const ProjectsNew = () => {
|
||||
<Field name='og_image_url' placeholder='OG Image URL' />
|
||||
</FormField>
|
||||
|
||||
<FormField label='Theme Config JSON' hasTextareaHeight>
|
||||
<Field
|
||||
name='theme_config_json'
|
||||
as='textarea'
|
||||
placeholder='Theme Config JSON'
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label='Custom CSS JSON' hasTextareaHeight>
|
||||
<Field
|
||||
name='custom_css_json'
|
||||
as='textarea'
|
||||
placeholder='Custom CSS JSON'
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label='CDN Base URL'>
|
||||
<Field name='cdn_base_url' placeholder='CDN Base URL' />
|
||||
</FormField>
|
||||
|
||||
<FormField label='Is Deleted' labelFor='is_deleted'>
|
||||
<Field
|
||||
name='is_deleted'
|
||||
|
||||
@ -36,10 +36,6 @@ const ProjectsTablesPage = () => {
|
||||
{ label: 'LogoURL', title: 'logo_url' },
|
||||
{ label: 'FaviconURL', title: 'favicon_url' },
|
||||
{ label: 'OGImageURL', title: 'og_image_url' },
|
||||
{ label: 'ThemeconfigJSON', title: 'theme_config_json' },
|
||||
{ label: 'CustomCSSJSON', title: 'custom_css_json' },
|
||||
{ label: 'CDNbaseURL', title: 'cdn_base_url' },
|
||||
|
||||
{ label: 'Deletedat', title: 'deleted_at_time', date: true },
|
||||
]);
|
||||
|
||||
|
||||
@ -26,9 +26,6 @@ interface ProjectWithRelations extends Project {
|
||||
logo_url?: string;
|
||||
favicon_url?: string;
|
||||
og_image_url?: string;
|
||||
theme_config_json?: string;
|
||||
custom_css_json?: string;
|
||||
cdn_base_url?: string;
|
||||
is_deleted?: boolean;
|
||||
deleted_at_time?: string | Date;
|
||||
project_memberships_project?: Array<{
|
||||
@ -189,27 +186,6 @@ const ProjectsView = () => {
|
||||
<p>{project?.og_image_url}</p>
|
||||
</div>
|
||||
|
||||
<FormField label='Multi Text' hasTextareaHeight>
|
||||
<textarea
|
||||
className={'w-full'}
|
||||
disabled
|
||||
value={project?.theme_config_json}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label='Multi Text' hasTextareaHeight>
|
||||
<textarea
|
||||
className={'w-full'}
|
||||
disabled
|
||||
value={project?.custom_css_json}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div className={'mb-4'}>
|
||||
<p className={'block font-bold mb-2'}>CDNbaseURL</p>
|
||||
<p>{project?.cdn_base_url}</p>
|
||||
</div>
|
||||
|
||||
<FormField label='Isdeleted'>
|
||||
<SwitchField
|
||||
field={{ name: 'is_deleted', value: project?.is_deleted }}
|
||||
|
||||
@ -41,9 +41,6 @@ export interface Project extends BaseEntity {
|
||||
logo_url?: string;
|
||||
favicon_url?: string;
|
||||
og_image_url?: string;
|
||||
theme_config_json?: string;
|
||||
custom_css_json?: string;
|
||||
cdn_base_url?: string;
|
||||
}
|
||||
|
||||
// Asset entity
|
||||
|
||||
@ -15,6 +15,9 @@ export interface RuntimeProject {
|
||||
name?: string;
|
||||
slug?: string;
|
||||
description?: string;
|
||||
logo_url?: string;
|
||||
favicon_url?: string;
|
||||
og_image_url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user