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/ # Docker Compose setup
|
||||||
├── docker-compose.yml
|
├── docker-compose.yml
|
||||||
├── start-backend.sh
|
├── start-backend.sh
|
||||||
└── wait-for-it.sh
|
├── wait-for-it.sh
|
||||||
|
└── README.md # Docker documentation
|
||||||
```
|
```
|
||||||
|
|
||||||
## Key Workflows
|
## Key Workflows
|
||||||
@ -225,7 +226,7 @@ EMAIL_PASS=...
|
|||||||
### Frontend (`frontend/.env.local`)
|
### Frontend (`frontend/.env.local`)
|
||||||
|
|
||||||
```env
|
```env
|
||||||
NEXT_PUBLIC_BACK_API=http://localhost:8080
|
NEXT_PUBLIC_BACK_API=http://localhost:8080/api
|
||||||
```
|
```
|
||||||
|
|
||||||
## Common Commands
|
## Common Commands
|
||||||
|
|||||||
@ -85,28 +85,33 @@ backend/src/
|
|||||||
├── helpers.js # Utility functions (wrapAsync)
|
├── helpers.js # Utility functions (wrapAsync)
|
||||||
│
|
│
|
||||||
├── auth/ # Passport.js authentication strategies
|
├── auth/ # Passport.js authentication strategies
|
||||||
│ └── passport.js # JWT, Google, Microsoft strategies
|
│ └── auth.js # JWT, Google, Microsoft strategies
|
||||||
│
|
│
|
||||||
├── db/
|
├── 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)
|
│ ├── api/ # Database access layer (CRUD per model)
|
||||||
│ ├── migrations/ # Database migrations
|
│ ├── migrations/ # Database migrations
|
||||||
│ └── seeders/ # Seed data (admin users, permissions, roles)
|
│ └── seeders/ # Seed data (admin users, permissions, roles)
|
||||||
│
|
│
|
||||||
├── routes/ # Express route handlers
|
├── routes/ # Express route handlers (22 routes)
|
||||||
│ ├── auth.js # Authentication endpoints
|
│ ├── auth.js # Authentication endpoints
|
||||||
│ ├── projects.js # Project CRUD + publishing
|
│ ├── projects.js # Project CRUD
|
||||||
│ ├── tour_pages.js # Tour page management
|
│ ├── tour_pages.js # Tour page management
|
||||||
│ ├── assets.js # Asset management
|
│ ├── assets.js # Asset management
|
||||||
|
│ ├── file.js # File upload/download, presigned URLs
|
||||||
│ ├── publish.js # Publishing workflow
|
│ ├── publish.js # Publishing workflow
|
||||||
|
│ ├── search.js # Global search
|
||||||
│ └── ... # Other entity routes
|
│ └── ... # Other entity routes
|
||||||
│
|
│
|
||||||
├── services/ # Business logic layer
|
├── services/ # Business logic layer (21 services)
|
||||||
│ ├── auth.js # Auth service (JWT, OAuth)
|
│ ├── auth.js # Auth service (JWT, OAuth)
|
||||||
│ ├── publish.js # Publishing workflow logic
|
│ ├── publish.js # Publishing workflow logic
|
||||||
│ ├── file.js # File storage abstraction
|
│ ├── file.js # File storage abstraction
|
||||||
|
│ ├── search.js # Global search service
|
||||||
│ ├── email/ # Email templates and sending
|
│ ├── email/ # Email templates and sending
|
||||||
│ └── ... # Other services
|
│ ├── notifications/ # Error classes and i18n messages
|
||||||
|
│ └── ... # Other entity services
|
||||||
│
|
│
|
||||||
├── middlewares/
|
├── middlewares/
|
||||||
│ ├── check-permissions.js # RBAC permission checking
|
│ ├── check-permissions.js # RBAC permission checking
|
||||||
@ -120,8 +125,10 @@ backend/src/
|
|||||||
│ └── service.factory.js # Generate service classes
|
│ └── service.factory.js # Generate service classes
|
||||||
│
|
│
|
||||||
└── utils/
|
└── 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
|
## Database Setup
|
||||||
@ -196,6 +203,10 @@ DELETE /api/{entity}/:id # Soft delete record
|
|||||||
| `element_type_defaults` | Global element default settings |
|
| `element_type_defaults` | Global element default settings |
|
||||||
| `project_element_defaults` | Project-specific element settings |
|
| `project_element_defaults` | Project-specific element settings |
|
||||||
| `project_audio_tracks` | Background audio for projects |
|
| `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 |
|
| `users` | User accounts |
|
||||||
| `roles` | User roles |
|
| `roles` | User roles |
|
||||||
| `permissions` | Granular permissions |
|
| `permissions` | Granular permissions |
|
||||||
|
|||||||
@ -23,9 +23,6 @@ class ProjectsDBApi extends GenericDBApi {
|
|||||||
'logo_url',
|
'logo_url',
|
||||||
'favicon_url',
|
'favicon_url',
|
||||||
'og_image_url',
|
'og_image_url',
|
||||||
'theme_config_json',
|
|
||||||
'custom_css_json',
|
|
||||||
'cdn_base_url',
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,7 +41,6 @@ class ProjectsDBApi extends GenericDBApi {
|
|||||||
'slug',
|
'slug',
|
||||||
'description',
|
'description',
|
||||||
'logo_url',
|
'logo_url',
|
||||||
'cdn_base_url',
|
|
||||||
'createdAt',
|
'createdAt',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -66,9 +62,6 @@ class ProjectsDBApi extends GenericDBApi {
|
|||||||
logo_url: data.logo_url || null,
|
logo_url: data.logo_url || null,
|
||||||
favicon_url: data.favicon_url || null,
|
favicon_url: data.favicon_url || null,
|
||||||
og_image_url: data.og_image_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 };
|
const queryWhere = { ...where };
|
||||||
|
|
||||||
// Runtime access: filter by project slug
|
// Runtime access: filter by project slug
|
||||||
if (runtimeProjectSlug) {
|
// Skip if finding by ID (unambiguous lookup)
|
||||||
|
if (runtimeProjectSlug && !where.id) {
|
||||||
queryWhere.slug = runtimeProjectSlug;
|
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,
|
type: DataTypes.TEXT,
|
||||||
},
|
},
|
||||||
|
|
||||||
theme_config_json: {
|
|
||||||
type: DataTypes.JSON,
|
|
||||||
},
|
|
||||||
|
|
||||||
custom_css_json: {
|
|
||||||
type: DataTypes.JSON,
|
|
||||||
},
|
|
||||||
|
|
||||||
cdn_base_url: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
},
|
|
||||||
|
|
||||||
importHash: {
|
importHash: {
|
||||||
type: DataTypes.STRING(255),
|
type: DataTypes.STRING(255),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
|
|||||||
@ -24,76 +24,32 @@ const AccessLogs = db.access_logs;
|
|||||||
const ProjectsData = [
|
const ProjectsData = [
|
||||||
{
|
{
|
||||||
name: 'Cardiff Arena Tour',
|
name: 'Cardiff Arena Tour',
|
||||||
|
|
||||||
slug: 'cardiff-arena',
|
slug: 'cardiff-arena',
|
||||||
|
|
||||||
description: 'Interactive arena tour for visitors and event planners.',
|
description: 'Interactive arena tour for visitors and event planners.',
|
||||||
|
|
||||||
logo_url: 'https://cdn.platform.com/cardiff/logo.png',
|
logo_url: 'https://cdn.platform.com/cardiff/logo.png',
|
||||||
|
|
||||||
favicon_url: 'https://cdn.platform.com/cardiff/favicon.ico',
|
favicon_url: 'https://cdn.platform.com/cardiff/favicon.ico',
|
||||||
|
|
||||||
og_image_url: 'https://cdn.platform.com/cardiff/og.jpg',
|
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,
|
is_deleted: true,
|
||||||
|
|
||||||
deleted_at_time: new Date('2026-01-01T00:00:00Z'),
|
deleted_at_time: new Date('2026-01-01T00:00:00Z'),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'Riverside Park Walkthrough',
|
name: 'Riverside Park Walkthrough',
|
||||||
|
|
||||||
slug: 'riverside-park',
|
slug: 'riverside-park',
|
||||||
|
|
||||||
description: 'Offline-ready guided walkthrough for the city park.',
|
description: 'Offline-ready guided walkthrough for the city park.',
|
||||||
|
|
||||||
logo_url: 'https://cdn.platform.com/riverside/logo.png',
|
logo_url: 'https://cdn.platform.com/riverside/logo.png',
|
||||||
|
|
||||||
favicon_url: 'https://cdn.platform.com/riverside/favicon.ico',
|
favicon_url: 'https://cdn.platform.com/riverside/favicon.ico',
|
||||||
|
|
||||||
og_image_url: 'https://cdn.platform.com/riverside/og.jpg',
|
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,
|
is_deleted: false,
|
||||||
|
|
||||||
deleted_at_time: new Date('2026-01-01T00:00:00Z'),
|
deleted_at_time: new Date('2026-01-01T00:00:00Z'),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'Mall Central Experience',
|
name: 'Mall Central Experience',
|
||||||
|
|
||||||
slug: 'mall-central',
|
slug: 'mall-central',
|
||||||
|
|
||||||
description: 'Retail complex presentation with navigation and galleries.',
|
description: 'Retail complex presentation with navigation and galleries.',
|
||||||
|
|
||||||
logo_url: 'https://cdn.platform.com/mall/logo.png',
|
logo_url: 'https://cdn.platform.com/mall/logo.png',
|
||||||
|
|
||||||
favicon_url: 'https://cdn.platform.com/mall/favicon.ico',
|
favicon_url: 'https://cdn.platform.com/mall/favicon.ico',
|
||||||
|
|
||||||
og_image_url: 'https://cdn.platform.com/mall/og.jpg',
|
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,
|
is_deleted: false,
|
||||||
|
|
||||||
deleted_at_time: new Date('2026-01-01T00:00:00Z'),
|
deleted_at_time: new Date('2026-01-01T00:00:00Z'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -9,9 +9,6 @@ const PUBLIC_RUNTIME_ENTITY_FIELDS = {
|
|||||||
'logo_url',
|
'logo_url',
|
||||||
'favicon_url',
|
'favicon_url',
|
||||||
'og_image_url',
|
'og_image_url',
|
||||||
'theme_config_json',
|
|
||||||
'custom_css_json',
|
|
||||||
'cdn_base_url',
|
|
||||||
],
|
],
|
||||||
tour_pages: [
|
tour_pages: [
|
||||||
'id',
|
'id',
|
||||||
|
|||||||
@ -38,15 +38,6 @@ router.use(checkCrudPermissions('projects'));
|
|||||||
* og_image_url:
|
* og_image_url:
|
||||||
* type: string
|
* type: string
|
||||||
* default: og_image_url
|
* 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',
|
'logo_url',
|
||||||
'favicon_url',
|
'favicon_url',
|
||||||
'og_image_url',
|
'og_image_url',
|
||||||
'theme_config_json',
|
|
||||||
'custom_css_json',
|
|
||||||
'cdn_base_url',
|
|
||||||
];
|
];
|
||||||
const opts = { fields };
|
const opts = { fields };
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -98,9 +98,6 @@ module.exports = class ProjectsService {
|
|||||||
logo_url: sourceProject.logo_url,
|
logo_url: sourceProject.logo_url,
|
||||||
favicon_url: sourceProject.favicon_url,
|
favicon_url: sourceProject.favicon_url,
|
||||||
og_image_url: sourceProject.og_image_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,
|
currentUser,
|
||||||
|
|||||||
@ -49,22 +49,11 @@ module.exports = class SearchService {
|
|||||||
|
|
||||||
projects: [
|
projects: [
|
||||||
'name',
|
'name',
|
||||||
|
|
||||||
'slug',
|
'slug',
|
||||||
|
|
||||||
'description',
|
'description',
|
||||||
|
|
||||||
'logo_url',
|
'logo_url',
|
||||||
|
|
||||||
'favicon_url',
|
'favicon_url',
|
||||||
|
|
||||||
'og_image_url',
|
'og_image_url',
|
||||||
|
|
||||||
'theme_config_json',
|
|
||||||
|
|
||||||
'custom_css_json',
|
|
||||||
|
|
||||||
'cdn_base_url',
|
|
||||||
],
|
],
|
||||||
|
|
||||||
assets: ['name', 'cdn_url', 'storage_key', 'mime_type', 'checksum'],
|
assets: ['name', 'cdn_url', 'storage_key', 'mime_type', 'checksum'],
|
||||||
|
|||||||
@ -95,17 +95,19 @@ frontend/src/
|
|||||||
│ └── {entity}/ # Entity-specific slices
|
│ └── {entity}/ # Entity-specific slices
|
||||||
│ └── {entity}Slice.ts
|
│ └── {entity}Slice.ts
|
||||||
│
|
│
|
||||||
├── hooks/ # Custom React hooks
|
├── hooks/ # Custom React hooks (32 hooks)
|
||||||
│ ├── useFormSync.ts # Form state synchronization
|
|
||||||
│ ├── useEntityTable.ts # Data grid with CRUD
|
|
||||||
│ ├── usePreloadOrchestrator.ts # Asset preloading with S3 presigned URLs
|
│ ├── usePreloadOrchestrator.ts # Asset preloading with S3 presigned URLs
|
||||||
│ ├── usePageSwitch.ts # Page navigation with preloaded blob URLs
|
│ ├── usePageSwitch.ts # Page navigation with preloaded blob URLs
|
||||||
│ ├── useNeighborGraph.ts # Page navigation graph (maxDepth: 1)
|
│ ├── useNeighborGraph.ts # Page navigation graph (maxDepth: 1)
|
||||||
│ ├── useTransitionPlayback.ts # Video transitions
|
│ ├── useTransitionPlayback.ts # Video transitions
|
||||||
│ ├── usePageNavigation.ts # Runtime page history
|
│ ├── usePageNavigation.ts # Runtime page history
|
||||||
│ ├── useReversePlayback.ts # Reverse video playback
|
│ ├── useReversePlayback.ts # Reverse video playback
|
||||||
|
│ ├── useConstructorElements.ts # Manage canvas elements
|
||||||
|
│ ├── useConstructorPageActions.ts # Save, publish, create pages
|
||||||
│ ├── useOfflineMode.ts # PWA offline detection
|
│ ├── 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
|
├── types/ # TypeScript definitions
|
||||||
│ ├── constructor.ts # Tour builder types
|
│ ├── constructor.ts # Tour builder types
|
||||||
@ -116,16 +118,28 @@ frontend/src/
|
|||||||
│ └── ... # Other types
|
│ └── ... # Other types
|
||||||
│
|
│
|
||||||
├── lib/ # Utility libraries
|
├── lib/ # Utility libraries
|
||||||
│ ├── elementStyles.ts # Element styling utilities
|
|
||||||
│ ├── imagePreDecode.ts # Image pre-decoding
|
|
||||||
│ ├── mediaDuration.ts # Video/audio duration
|
|
||||||
│ ├── assetUrl.ts # CDN URL resolution
|
│ ├── 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
|
│ ├── 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
|
│ ├── 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
|
│ ├── offline/ # Offline utilities
|
||||||
│ │ └── StorageManager.ts # Cache API storage for assets
|
│ │ ├── DownloadEventBus.ts # Download event handling
|
||||||
│ └── offlineDb/ # IndexedDB (Dexie) setup
|
│ │ ├── 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
|
├── layouts/ # Page layouts
|
||||||
│ ├── Authenticated.tsx # Logged-in users layout
|
│ ├── Authenticated.tsx # Logged-in users layout
|
||||||
@ -135,6 +149,7 @@ frontend/src/
|
|||||||
│ └── _theme.css # Tailwind theme overrides
|
│ └── _theme.css # Tailwind theme overrides
|
||||||
│
|
│
|
||||||
├── config/ # Configuration files
|
├── config/ # Configuration files
|
||||||
|
│ ├── offline.config.ts # Offline/PWA settings
|
||||||
│ └── preload.config.ts # Preload priorities and settings
|
│ └── preload.config.ts # Preload priorities and settings
|
||||||
├── schemas/ # Validation schemas (Zod)
|
├── schemas/ # Validation schemas (Zod)
|
||||||
├── factories/ # Component/hook factories
|
├── factories/ # Component/hook factories
|
||||||
@ -207,22 +222,64 @@ dispatch(create({ data: newProject }));
|
|||||||
|
|
||||||
## Custom Hooks
|
## Custom Hooks
|
||||||
|
|
||||||
|
### Runtime & Preloading Hooks
|
||||||
|
|
||||||
| Hook | Purpose |
|
| 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 |
|
| `usePreloadOrchestrator` | Asset preloading with S3 presigned URLs and ready blob URLs |
|
||||||
| `usePageSwitch` | Page navigation using preloaded blob URLs (O(1) instant lookup) |
|
| `usePageSwitch` | Page navigation using preloaded blob URLs (O(1) instant lookup) |
|
||||||
| `useNeighborGraph` | Build page navigation graph (maxDepth: 1) |
|
| `useNeighborGraph` | Build page navigation graph (maxDepth: 1) |
|
||||||
| `useTransitionPlayback` | Video transition playback coordination |
|
| `useTransitionPlayback` | Video transition playback coordination |
|
||||||
| `usePageNavigation` | Runtime page history management |
|
| `usePageNavigation` | Runtime page history management |
|
||||||
| `useReversePlayback` | Reverse video playback |
|
| `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 |
|
| `useOfflineMode` | Detect offline/online status |
|
||||||
| `usePWAPreload` | Preload assets for offline |
|
| `usePWAPreload` | Preload assets for offline |
|
||||||
| `useStorageQuota` | Monitor IndexedDB usage |
|
| `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:**
|
**Component-specific hooks:**
|
||||||
- `useElementSettingsForm` (`components/ElementSettings/`) - Element settings form state (~60 fields)
|
- `useElementSettingsForm` (`components/ElementSettings/`) - Element settings form state
|
||||||
|
|
||||||
## Element Types
|
## 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>
|
</dd>
|
||||||
</div>
|
</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'>
|
<div className='flex justify-between gap-x-4 py-3'>
|
||||||
<dt className=' text-gray-500 dark:text-dark-600'>
|
<dt className=' text-gray-500 dark:text-dark-600'>
|
||||||
Isdeleted
|
Isdeleted
|
||||||
|
|||||||
@ -81,25 +81,6 @@ const ListProjects = ({
|
|||||||
<p className={'line-clamp-2'}>{item.og_image_url}</p>
|
<p className={'line-clamp-2'}>{item.og_image_url}</p>
|
||||||
</div>
|
</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'}>
|
<div className={'flex-1 px-3'}>
|
||||||
<p className={'text-xs text-gray-500 '}>Isdeleted</p>
|
<p className={'text-xs text-gray-500 '}>Isdeleted</p>
|
||||||
<p className={'line-clamp-2'}>
|
<p className={'line-clamp-2'}>
|
||||||
|
|||||||
@ -25,24 +25,6 @@ const PROJECTS_COLUMNS: ColumnMetadata[] = [
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
editable: true,
|
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',
|
field: 'is_deleted',
|
||||||
headerName: 'Isdeleted',
|
headerName: 'Isdeleted',
|
||||||
|
|||||||
@ -22,9 +22,9 @@ import { OfflineToggle } from './Offline/OfflineToggle';
|
|||||||
import RuntimeElement from './RuntimeElement';
|
import RuntimeElement from './RuntimeElement';
|
||||||
import GalleryCarouselOverlay from './UiElements/GalleryCarouselOverlay';
|
import GalleryCarouselOverlay from './UiElements/GalleryCarouselOverlay';
|
||||||
import LayoutGuest from '../layouts/Guest';
|
import LayoutGuest from '../layouts/Guest';
|
||||||
import { getPageTitle } from '../config';
|
|
||||||
import { usePreloadOrchestrator } from '../hooks/usePreloadOrchestrator';
|
import { usePreloadOrchestrator } from '../hooks/usePreloadOrchestrator';
|
||||||
import { usePageDataLoader } from '../hooks/usePageDataLoader';
|
import { usePageDataLoader } from '../hooks/usePageDataLoader';
|
||||||
|
import { useProjectAssets } from '../hooks/useProjectAssets';
|
||||||
import { extractPageLinksAndElements } from '../lib/extractPageLinks';
|
import { extractPageLinksAndElements } from '../lib/extractPageLinks';
|
||||||
import { usePageSwitch } from '../hooks/usePageSwitch';
|
import { usePageSwitch } from '../hooks/usePageSwitch';
|
||||||
import { useTransitionPlayback } from '../hooks/useTransitionPlayback';
|
import { useTransitionPlayback } from '../hooks/useTransitionPlayback';
|
||||||
@ -37,26 +37,6 @@ import {
|
|||||||
} from '../lib/navigationHelpers';
|
} from '../lib/navigationHelpers';
|
||||||
import type { TransitionPhase } from '../types/presentation';
|
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 {
|
interface RuntimePresentationProps {
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
environment: 'stage' | 'production';
|
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 [selectedPageId, setSelectedPageId] = useState<string | null>(null);
|
||||||
const [pageHistory, setPageHistory] = useState<string[]>([]);
|
const [pageHistory, setPageHistory] = useState<string[]>([]);
|
||||||
const [transitionPreview, setTransitionPreview] = useState<{
|
const [transitionPreview, setTransitionPreview] = useState<{
|
||||||
@ -408,7 +391,42 @@ export default function RuntimePresentation({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<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>
|
</Head>
|
||||||
|
|
||||||
<div
|
<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.
|
* Resolves an asset path to its full playback URL.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -282,19 +282,19 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
|||||||
<meta name='description' content={description} />
|
<meta name='description' content={description} />
|
||||||
<meta property='og:url' content={url} />
|
<meta property='og:url' content={url} />
|
||||||
<meta property='og:site_name' content='https://flatlogic.com/' />
|
<meta property='og:site_name' content='https://flatlogic.com/' />
|
||||||
<meta property='og:title' content={title} />
|
<meta key='og:title' property='og:title' content={title} />
|
||||||
<meta property='og:description' content={description} />
|
<meta key='og:description' property='og:description' content={description} />
|
||||||
<meta property='og:image' content={image} />
|
<meta key='og:image' property='og:image' content={image} />
|
||||||
<meta property='og:image:type' content='image/png' />
|
<meta property='og:image:type' content='image/png' />
|
||||||
<meta property='og:image:width' content={imageWidth} />
|
<meta property='og:image:width' content={imageWidth} />
|
||||||
<meta property='og:image:height' content={imageHeight} />
|
<meta property='og:image:height' content={imageHeight} />
|
||||||
<meta property='twitter:card' content='summary_large_image' />
|
<meta property='twitter:card' content='summary_large_image' />
|
||||||
<meta property='twitter:title' content={title} />
|
<meta key='twitter:title' property='twitter:title' content={title} />
|
||||||
<meta property='twitter:description' content={description} />
|
<meta key='twitter:description' property='twitter:description' content={description} />
|
||||||
<meta property='twitter:image:src' content={image} />
|
<meta key='twitter:image:src' property='twitter:image:src' content={image} />
|
||||||
<meta property='twitter:image:width' content={imageWidth} />
|
<meta property='twitter:image:width' content={imageWidth} />
|
||||||
<meta property='twitter:image:height' content={imageHeight} />
|
<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' />
|
<link rel='manifest' href='/manifest.json' />
|
||||||
<meta name='theme-color' content='#3B82F6' />
|
<meta name='theme-color' content='#3B82F6' />
|
||||||
<meta name='mobile-web-app-capable' content='yes' />
|
<meta name='mobile-web-app-capable' content='yes' />
|
||||||
|
|||||||
@ -26,7 +26,6 @@ import { useAppDispatch, useAppSelector } from '../../stores/hooks';
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import type { Project } from '../../types/entities';
|
import type { Project } from '../../types/entities';
|
||||||
import { logger } from '../../lib/logger';
|
import { logger } from '../../lib/logger';
|
||||||
import { FONT_OPTIONS, getFontByKey, getFontKeyFromValues } from '../../lib/fonts';
|
|
||||||
|
|
||||||
const initVals = {
|
const initVals = {
|
||||||
name: '',
|
name: '',
|
||||||
@ -35,107 +34,16 @@ const initVals = {
|
|||||||
logo_url: '',
|
logo_url: '',
|
||||||
favicon_url: '',
|
favicon_url: '',
|
||||||
og_image_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,
|
is_deleted: false,
|
||||||
deleted_at_time: new Date(),
|
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 EditProjectsPage = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [initialValues, setInitialValues] = useState(initVals);
|
const [initialValues, setInitialValues] = useState(initVals);
|
||||||
const [logoAssets, setLogoAssets] = useState<
|
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);
|
const [isLoadingLogoAssets, setIsLoadingLogoAssets] = useState(false);
|
||||||
|
|
||||||
@ -207,16 +115,6 @@ const EditProjectsPage = () => {
|
|||||||
if (typeof project === 'object' && project !== null) {
|
if (typeof project === 'object' && project !== null) {
|
||||||
const projectData = project as unknown as Record<string, unknown>;
|
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({
|
setInitialValues({
|
||||||
name: String(projectData.name || ''),
|
name: String(projectData.name || ''),
|
||||||
slug: String(projectData.slug || ''),
|
slug: String(projectData.slug || ''),
|
||||||
@ -224,12 +122,6 @@ const EditProjectsPage = () => {
|
|||||||
logo_url: String(projectData.logo_url || ''),
|
logo_url: String(projectData.logo_url || ''),
|
||||||
favicon_url: String(projectData.favicon_url || ''),
|
favicon_url: String(projectData.favicon_url || ''),
|
||||||
og_image_url: String(projectData.og_image_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),
|
is_deleted: Boolean(projectData.is_deleted),
|
||||||
deleted_at_time: projectData.deleted_at_time
|
deleted_at_time: projectData.deleted_at_time
|
||||||
? new Date(projectData.deleted_at_time as string)
|
? new Date(projectData.deleted_at_time as string)
|
||||||
@ -239,19 +131,6 @@ const EditProjectsPage = () => {
|
|||||||
}, [project]);
|
}, [project]);
|
||||||
|
|
||||||
const handleSubmit = async (data: typeof initVals) => {
|
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> = {
|
const apiData: Partial<Project> = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
slug: data.slug,
|
slug: data.slug,
|
||||||
@ -259,9 +138,6 @@ const EditProjectsPage = () => {
|
|||||||
logo_url: data.logo_url,
|
logo_url: data.logo_url,
|
||||||
favicon_url: data.favicon_url,
|
favicon_url: data.favicon_url,
|
||||||
og_image_url: data.og_image_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 }));
|
await dispatch(update({ id: id as string, data: apiData }));
|
||||||
@ -330,13 +206,13 @@ const EditProjectsPage = () => {
|
|||||||
: 'Select logo from Assets'}
|
: 'Select logo from Assets'}
|
||||||
</option>
|
</option>
|
||||||
{logoAssets.map((asset) => (
|
{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*/, '')}
|
{(asset.name || '').replace(/^\[[^\]]+\]\s*/, '')}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
{values.logo_url &&
|
{values.logo_url &&
|
||||||
!logoAssets.some(
|
!logoAssets.some(
|
||||||
(asset) => asset.cdn_url === values.logo_url,
|
(asset) => (asset.storage_key || asset.cdn_url) === values.logo_url,
|
||||||
) && (
|
) && (
|
||||||
<option value={values.logo_url}>
|
<option value={values.logo_url}>
|
||||||
{values.logo_url}
|
{values.logo_url}
|
||||||
@ -363,13 +239,13 @@ const EditProjectsPage = () => {
|
|||||||
: 'Select favicon from Assets logos'}
|
: 'Select favicon from Assets logos'}
|
||||||
</option>
|
</option>
|
||||||
{logoAssets.map((asset) => (
|
{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*/, '')}
|
{(asset.name || '').replace(/^\[[^\]]+\]\s*/, '')}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
{values.favicon_url &&
|
{values.favicon_url &&
|
||||||
!logoAssets.some(
|
!logoAssets.some(
|
||||||
(asset) => asset.cdn_url === values.favicon_url,
|
(asset) => (asset.storage_key || asset.cdn_url) === values.favicon_url,
|
||||||
) && (
|
) && (
|
||||||
<option value={values.favicon_url}>
|
<option value={values.favicon_url}>
|
||||||
{values.favicon_url}
|
{values.favicon_url}
|
||||||
@ -396,13 +272,13 @@ const EditProjectsPage = () => {
|
|||||||
: 'Select OG image from Assets logos'}
|
: 'Select OG image from Assets logos'}
|
||||||
</option>
|
</option>
|
||||||
{logoAssets.map((asset) => (
|
{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*/, '')}
|
{(asset.name || '').replace(/^\[[^\]]+\]\s*/, '')}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
{values.og_image_url &&
|
{values.og_image_url &&
|
||||||
!logoAssets.some(
|
!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}>
|
<option value={values.og_image_url}>
|
||||||
{values.og_image_url}
|
{values.og_image_url}
|
||||||
@ -421,56 +297,6 @@ const EditProjectsPage = () => {
|
|||||||
</a>
|
</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 />
|
<BaseDivider />
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton type='submit' color='info' label='Submit' />
|
<BaseButton type='submit' color='info' label='Submit' />
|
||||||
|
|||||||
@ -32,9 +32,6 @@ const initialValues = {
|
|||||||
logo_url: '',
|
logo_url: '',
|
||||||
favicon_url: '',
|
favicon_url: '',
|
||||||
og_image_url: '',
|
og_image_url: '',
|
||||||
theme_config_json: '',
|
|
||||||
custom_css_json: '',
|
|
||||||
cdn_base_url: '',
|
|
||||||
is_deleted: false,
|
is_deleted: false,
|
||||||
deleted_at_time: '',
|
deleted_at_time: '',
|
||||||
};
|
};
|
||||||
@ -97,26 +94,6 @@ const ProjectsNew = () => {
|
|||||||
<Field name='og_image_url' placeholder='OG Image URL' />
|
<Field name='og_image_url' placeholder='OG Image URL' />
|
||||||
</FormField>
|
</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'>
|
<FormField label='Is Deleted' labelFor='is_deleted'>
|
||||||
<Field
|
<Field
|
||||||
name='is_deleted'
|
name='is_deleted'
|
||||||
|
|||||||
@ -36,10 +36,6 @@ const ProjectsTablesPage = () => {
|
|||||||
{ label: 'LogoURL', title: 'logo_url' },
|
{ label: 'LogoURL', title: 'logo_url' },
|
||||||
{ label: 'FaviconURL', title: 'favicon_url' },
|
{ label: 'FaviconURL', title: 'favicon_url' },
|
||||||
{ label: 'OGImageURL', title: 'og_image_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 },
|
{ label: 'Deletedat', title: 'deleted_at_time', date: true },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@ -26,9 +26,6 @@ interface ProjectWithRelations extends Project {
|
|||||||
logo_url?: string;
|
logo_url?: string;
|
||||||
favicon_url?: string;
|
favicon_url?: string;
|
||||||
og_image_url?: string;
|
og_image_url?: string;
|
||||||
theme_config_json?: string;
|
|
||||||
custom_css_json?: string;
|
|
||||||
cdn_base_url?: string;
|
|
||||||
is_deleted?: boolean;
|
is_deleted?: boolean;
|
||||||
deleted_at_time?: string | Date;
|
deleted_at_time?: string | Date;
|
||||||
project_memberships_project?: Array<{
|
project_memberships_project?: Array<{
|
||||||
@ -189,27 +186,6 @@ const ProjectsView = () => {
|
|||||||
<p>{project?.og_image_url}</p>
|
<p>{project?.og_image_url}</p>
|
||||||
</div>
|
</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'>
|
<FormField label='Isdeleted'>
|
||||||
<SwitchField
|
<SwitchField
|
||||||
field={{ name: 'is_deleted', value: project?.is_deleted }}
|
field={{ name: 'is_deleted', value: project?.is_deleted }}
|
||||||
|
|||||||
@ -41,9 +41,6 @@ export interface Project extends BaseEntity {
|
|||||||
logo_url?: string;
|
logo_url?: string;
|
||||||
favicon_url?: string;
|
favicon_url?: string;
|
||||||
og_image_url?: string;
|
og_image_url?: string;
|
||||||
theme_config_json?: string;
|
|
||||||
custom_css_json?: string;
|
|
||||||
cdn_base_url?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Asset entity
|
// Asset entity
|
||||||
|
|||||||
@ -15,6 +15,9 @@ export interface RuntimeProject {
|
|||||||
name?: string;
|
name?: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
logo_url?: string;
|
||||||
|
favicon_url?: string;
|
||||||
|
og_image_url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user