Autosave: 20260318-063526
This commit is contained in:
parent
06dd524cd0
commit
b4fe0dde81
@ -28,6 +28,11 @@ module.exports = class AssetsDBApi {
|
||||
||
|
||||
null
|
||||
,
|
||||
|
||||
type: data.type
|
||||
||
|
||||
'general'
|
||||
,
|
||||
|
||||
cdn_url: data.cdn_url
|
||||
||
|
||||
@ -123,6 +128,11 @@ module.exports = class AssetsDBApi {
|
||||
asset_type: item.asset_type
|
||||
||
|
||||
null
|
||||
,
|
||||
|
||||
type: item.type
|
||||
||
|
||||
'general'
|
||||
,
|
||||
|
||||
cdn_url: item.cdn_url
|
||||
@ -215,6 +225,9 @@ module.exports = class AssetsDBApi {
|
||||
if (data.asset_type !== undefined) updatePayload.asset_type = data.asset_type;
|
||||
|
||||
|
||||
if (data.type !== undefined) updatePayload.type = data.type;
|
||||
|
||||
|
||||
if (data.cdn_url !== undefined) updatePayload.cdn_url = data.cdn_url;
|
||||
|
||||
|
||||
@ -608,6 +621,13 @@ module.exports = class AssetsDBApi {
|
||||
};
|
||||
}
|
||||
|
||||
if (filter.type) {
|
||||
where = {
|
||||
...where,
|
||||
type: filter.type,
|
||||
};
|
||||
}
|
||||
|
||||
if (filter.is_public) {
|
||||
where = {
|
||||
...where,
|
||||
|
||||
105
backend/src/db/migrations/20260318102000-add-type-to-assets.js
Normal file
105
backend/src/db/migrations/20260318102000-add-type-to-assets.js
Normal file
@ -0,0 +1,105 @@
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
|
||||
try {
|
||||
const tableRows = await queryInterface.sequelize.query(
|
||||
"SELECT to_regclass('public.assets') AS regclass_name;",
|
||||
{
|
||||
transaction,
|
||||
type: Sequelize.QueryTypes.SELECT,
|
||||
},
|
||||
);
|
||||
|
||||
if (!tableRows[0]?.regclass_name) {
|
||||
await transaction.commit();
|
||||
return;
|
||||
}
|
||||
|
||||
const tableDefinition = await queryInterface.describeTable('assets', { transaction });
|
||||
|
||||
if (!tableDefinition.type) {
|
||||
await queryInterface.addColumn(
|
||||
'assets',
|
||||
'type',
|
||||
{
|
||||
type: Sequelize.DataTypes.ENUM,
|
||||
values: [
|
||||
'icon',
|
||||
'background_image',
|
||||
'audio',
|
||||
'video',
|
||||
'transition',
|
||||
'logo',
|
||||
'favicon',
|
||||
'document',
|
||||
'general',
|
||||
],
|
||||
allowNull: false,
|
||||
defaultValue: 'general',
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await queryInterface.sequelize.query(
|
||||
`
|
||||
UPDATE assets
|
||||
SET type = (
|
||||
CASE
|
||||
WHEN name ILIKE '[ICON] %' OR name ILIKE '[IMAGE] %' THEN 'icon'
|
||||
WHEN name ILIKE '[BACKGROUND] %' THEN 'background_image'
|
||||
WHEN name ILIKE '[AUDIO] %' THEN 'audio'
|
||||
WHEN name ILIKE '[VIDEO] %' THEN 'video'
|
||||
WHEN name ILIKE '[TRANSITION] %' THEN 'transition'
|
||||
WHEN name ILIKE '[LOGO] %' THEN 'logo'
|
||||
WHEN asset_type = 'audio' THEN 'audio'
|
||||
WHEN asset_type = 'video' THEN 'video'
|
||||
ELSE 'general'
|
||||
END
|
||||
)::"enum_assets_type"
|
||||
WHERE type = 'general';
|
||||
`,
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await queryInterface.addIndex('assets', ['type'], { transaction });
|
||||
}
|
||||
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
|
||||
try {
|
||||
const tableRows = await queryInterface.sequelize.query(
|
||||
"SELECT to_regclass('public.assets') AS regclass_name;",
|
||||
{
|
||||
transaction,
|
||||
type: Sequelize.QueryTypes.SELECT,
|
||||
},
|
||||
);
|
||||
|
||||
if (!tableRows[0]?.regclass_name) {
|
||||
await transaction.commit();
|
||||
return;
|
||||
}
|
||||
|
||||
const tableDefinition = await queryInterface.describeTable('assets', { transaction });
|
||||
|
||||
if (tableDefinition.type) {
|
||||
await queryInterface.removeIndex('assets', ['type'], { transaction });
|
||||
await queryInterface.removeColumn('assets', 'type', { transaction });
|
||||
}
|
||||
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -36,6 +36,43 @@ asset_type: {
|
||||
|
||||
},
|
||||
|
||||
type: {
|
||||
type: DataTypes.ENUM,
|
||||
allowNull: false,
|
||||
defaultValue: "general",
|
||||
|
||||
values: [
|
||||
|
||||
"icon",
|
||||
|
||||
|
||||
"background_image",
|
||||
|
||||
|
||||
"audio",
|
||||
|
||||
|
||||
"video",
|
||||
|
||||
|
||||
"transition",
|
||||
|
||||
|
||||
"logo",
|
||||
|
||||
|
||||
"favicon",
|
||||
|
||||
|
||||
"document",
|
||||
|
||||
|
||||
"general"
|
||||
|
||||
],
|
||||
|
||||
},
|
||||
|
||||
cdn_url: {
|
||||
type: DataTypes.TEXT,
|
||||
|
||||
@ -132,6 +169,7 @@ deleted_at_time: {
|
||||
indexes: [
|
||||
{ fields: ['projectId'] },
|
||||
{ fields: ['asset_type'] },
|
||||
{ fields: ['type'] },
|
||||
{ fields: ['is_public'] },
|
||||
{ fields: ['is_deleted'] },
|
||||
{ fields: ['deletedAt'] },
|
||||
@ -198,4 +236,3 @@ deleted_at_time: {
|
||||
|
||||
return assets;
|
||||
};
|
||||
|
||||
|
||||
@ -120,6 +120,7 @@ module.exports = class ProjectsService {
|
||||
{
|
||||
name: sourceAsset.name,
|
||||
asset_type: sourceAsset.asset_type,
|
||||
type: sourceAsset.type || 'general',
|
||||
cdn_url: sourceAsset.cdn_url,
|
||||
storage_key: sourceAsset.storage_key,
|
||||
mime_type: sourceAsset.mime_type,
|
||||
|
||||
@ -102,13 +102,22 @@ const CardAssets = ({
|
||||
|
||||
|
||||
<div className='flex justify-between gap-x-4 py-3'>
|
||||
<dt className=' text-gray-500 dark:text-dark-600'>Assettype</dt>
|
||||
<dt className=' text-gray-500 dark:text-dark-600'>Asset format</dt>
|
||||
<dd className='flex items-start gap-x-2'>
|
||||
<div className='font-medium line-clamp-4'>
|
||||
{ item.asset_type }
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div className='flex justify-between gap-x-4 py-3'>
|
||||
<dt className=' text-gray-500 dark:text-dark-600'>Type</dt>
|
||||
<dd className='flex items-start gap-x-2'>
|
||||
<div className='font-medium line-clamp-4'>
|
||||
{ item.type || 'general' }
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
@ -64,9 +64,14 @@ const ListAssets = ({ assets, loading, onDelete, currentPage, numPages, onPageCh
|
||||
|
||||
|
||||
<div className={'flex-1 px-3'}>
|
||||
<p className={'text-xs text-gray-500 '}>Assettype</p>
|
||||
<p className={'text-xs text-gray-500 '}>Asset format</p>
|
||||
<p className={'line-clamp-2'}>{ item.asset_type }</p>
|
||||
</div>
|
||||
|
||||
<div className={'flex-1 px-3'}>
|
||||
<p className={'text-xs text-gray-500 '}>Type</p>
|
||||
<p className={'line-clamp-2'}>{ item.type || 'general' }</p>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@ -189,4 +194,4 @@ const ListAssets = ({ assets, loading, onDelete, currentPage, numPages, onPageCh
|
||||
)
|
||||
};
|
||||
|
||||
export default ListAssets
|
||||
export default ListAssets
|
||||
|
||||
@ -80,7 +80,22 @@ export const loadColumns = async (
|
||||
|
||||
{
|
||||
field: 'asset_type',
|
||||
headerName: 'Assettype',
|
||||
headerName: 'Asset format',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
|
||||
|
||||
editable: hasUpdatePermission,
|
||||
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
field: 'type',
|
||||
headerName: 'Type',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
filterable: false,
|
||||
|
||||
@ -16,6 +16,7 @@ import { appWithTranslation } from 'next-i18next';
|
||||
import '../i18n';
|
||||
import IntroGuide from '../components/IntroGuide';
|
||||
import { appSteps, loginSteps, usersSteps, rolesSteps } from '../stores/introSteps';
|
||||
import { logoutUser } from '../stores/authSlice';
|
||||
|
||||
// Initialize axios
|
||||
axios.defaults.baseURL = process.env.NEXT_PUBLIC_BACK_API
|
||||
@ -62,6 +63,31 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
const interceptorId = axios.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
const status = error?.response?.status;
|
||||
const requestUrl = `${error?.config?.url || ''}`;
|
||||
const isLoginRequest = requestUrl.includes('/auth/signin/local') || requestUrl.includes('auth/signin/local');
|
||||
|
||||
if (status === 401 && !isLoginRequest) {
|
||||
store.dispatch(logoutUser());
|
||||
|
||||
if (router.pathname !== '/login') {
|
||||
router.replace('/login');
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
axios.interceptors.response.eject(interceptorId);
|
||||
};
|
||||
}, [router]);
|
||||
|
||||
// TODO: Remove this code in future releases
|
||||
React.useEffect(() => {
|
||||
const trustedOrigins = new Set<string>([window.location.origin]);
|
||||
|
||||
@ -113,6 +113,7 @@ const EditAssets = () => {
|
||||
|
||||
|
||||
asset_type: '',
|
||||
type: 'general',
|
||||
|
||||
|
||||
|
||||
@ -612,7 +613,7 @@ const EditAssets = () => {
|
||||
|
||||
|
||||
|
||||
<FormField label="Assettype" labelFor="asset_type">
|
||||
<FormField label="Asset Format" labelFor="asset_type">
|
||||
<Field name="asset_type" id="asset_type" component="select">
|
||||
|
||||
<option value="image">image</option>
|
||||
@ -625,6 +626,20 @@ const EditAssets = () => {
|
||||
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Type" labelFor="type">
|
||||
<Field name="type" id="type" component="select">
|
||||
<option value="general">general</option>
|
||||
<option value="icon">icon</option>
|
||||
<option value="background_image">background_image</option>
|
||||
<option value="audio">audio</option>
|
||||
<option value="video">video</option>
|
||||
<option value="transition">transition</option>
|
||||
<option value="logo">logo</option>
|
||||
<option value="favicon">favicon</option>
|
||||
<option value="document">document</option>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
@ -113,6 +113,7 @@ const EditAssetsPage = () => {
|
||||
|
||||
|
||||
asset_type: '',
|
||||
type: 'general',
|
||||
|
||||
|
||||
|
||||
@ -609,7 +610,7 @@ const EditAssetsPage = () => {
|
||||
|
||||
|
||||
|
||||
<FormField label="Assettype" labelFor="asset_type">
|
||||
<FormField label="Asset Format" labelFor="asset_type">
|
||||
<Field name="asset_type" id="asset_type" component="select">
|
||||
|
||||
<option value="image">image</option>
|
||||
@ -622,6 +623,20 @@ const EditAssetsPage = () => {
|
||||
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Type" labelFor="type">
|
||||
<Field name="type" id="type" component="select">
|
||||
<option value="general">general</option>
|
||||
<option value="icon">icon</option>
|
||||
<option value="background_image">background_image</option>
|
||||
<option value="audio">audio</option>
|
||||
<option value="video">video</option>
|
||||
<option value="transition">transition</option>
|
||||
<option value="logo">logo</option>
|
||||
<option value="favicon">favicon</option>
|
||||
<option value="document">document</option>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
@ -23,6 +23,16 @@ type Asset = {
|
||||
id: string
|
||||
name: string
|
||||
asset_type: 'image' | 'video' | 'audio' | 'file'
|
||||
type?:
|
||||
| 'icon'
|
||||
| 'background_image'
|
||||
| 'audio'
|
||||
| 'video'
|
||||
| 'transition'
|
||||
| 'logo'
|
||||
| 'favicon'
|
||||
| 'document'
|
||||
| 'general'
|
||||
cdn_url?: string | null
|
||||
mime_type?: string | null
|
||||
}
|
||||
@ -31,8 +41,9 @@ type AssetSection = {
|
||||
key: 'images' | 'backgroundImages' | 'audio' | 'video' | 'transitions' | 'logo'
|
||||
label: string
|
||||
accept: string
|
||||
assetType: 'image' | 'video' | 'audio'
|
||||
tag: string
|
||||
assetFormat: 'image' | 'video' | 'audio'
|
||||
assetCategory: NonNullable<Asset['type']>
|
||||
legacyTag: string
|
||||
}
|
||||
|
||||
type UploadQueueItem = {
|
||||
@ -44,28 +55,28 @@ type UploadQueueItem = {
|
||||
}
|
||||
|
||||
const ASSET_SECTIONS: AssetSection[] = [
|
||||
{ key: 'images', label: 'Icons', accept: 'image/*', assetType: 'image', tag: 'IMAGE' },
|
||||
{ key: 'images', label: 'Icons', accept: 'image/*', assetFormat: 'image', assetCategory: 'icon', legacyTag: 'IMAGE' },
|
||||
{
|
||||
key: 'backgroundImages',
|
||||
label: 'Background Images',
|
||||
accept: 'image/*',
|
||||
assetType: 'image',
|
||||
tag: 'BACKGROUND',
|
||||
assetFormat: 'image',
|
||||
assetCategory: 'background_image',
|
||||
legacyTag: 'BACKGROUND',
|
||||
},
|
||||
{ key: 'audio', label: 'Audio', accept: 'audio/*', assetType: 'audio', tag: 'AUDIO' },
|
||||
{ key: 'video', label: 'Video', accept: 'video/*', assetType: 'video', tag: 'VIDEO' },
|
||||
{ key: 'audio', label: 'Audio', accept: 'audio/*', assetFormat: 'audio', assetCategory: 'audio', legacyTag: 'AUDIO' },
|
||||
{ key: 'video', label: 'Video', accept: 'video/*', assetFormat: 'video', assetCategory: 'video', legacyTag: 'VIDEO' },
|
||||
{
|
||||
key: 'transitions',
|
||||
label: 'Transitions',
|
||||
accept: 'video/*',
|
||||
assetType: 'video',
|
||||
tag: 'TRANSITION',
|
||||
assetFormat: 'video',
|
||||
assetCategory: 'transition',
|
||||
legacyTag: 'TRANSITION',
|
||||
},
|
||||
{ key: 'logo', label: 'Logo', accept: 'image/*', assetType: 'image', tag: 'LOGO' },
|
||||
{ key: 'logo', label: 'Logo', accept: 'image/*', assetFormat: 'image', assetCategory: 'logo', legacyTag: 'LOGO' },
|
||||
]
|
||||
|
||||
const getTaggedName = (tag: string, name: string) => `[${tag}] ${name}`
|
||||
|
||||
const AssetsTablesPage = () => {
|
||||
const router = useRouter()
|
||||
const routeProjectId = useMemo(() => {
|
||||
@ -218,8 +229,9 @@ const AssetsTablesPage = () => {
|
||||
await axios.post('/assets', {
|
||||
data: {
|
||||
project: projectId,
|
||||
name: getTaggedName(section.tag, file.name),
|
||||
asset_type: section.assetType,
|
||||
name: file.name,
|
||||
asset_type: section.assetFormat,
|
||||
type: section.assetCategory,
|
||||
cdn_url: remoteFile.publicUrl,
|
||||
storage_key: remoteFile.privateUrl,
|
||||
mime_type: file.type || null,
|
||||
@ -313,7 +325,10 @@ const AssetsTablesPage = () => {
|
||||
|
||||
const assetsBySection = useMemo(() => {
|
||||
return ASSET_SECTIONS.reduce<Record<string, Asset[]>>((acc, section) => {
|
||||
acc[section.key] = assets.filter((asset) => asset.name?.startsWith(`[${section.tag}] `))
|
||||
acc[section.key] = assets.filter((asset) => {
|
||||
if (asset.type) return asset.type === section.assetCategory
|
||||
return asset.name?.startsWith(`[${section.legacyTag}] `)
|
||||
})
|
||||
return acc
|
||||
}, {})
|
||||
}, [assets])
|
||||
|
||||
@ -72,6 +72,7 @@ const initialValues = {
|
||||
|
||||
|
||||
asset_type: 'image',
|
||||
type: 'general',
|
||||
|
||||
|
||||
|
||||
@ -369,7 +370,7 @@ const AssetsNew = () => {
|
||||
|
||||
|
||||
|
||||
<FormField label="Assettype" labelFor="asset_type">
|
||||
<FormField label="Asset Format" labelFor="asset_type">
|
||||
<Field name="asset_type" id="asset_type" component="select">
|
||||
|
||||
<option value="image">image</option>
|
||||
@ -383,6 +384,20 @@ const AssetsNew = () => {
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Type" labelFor="type">
|
||||
<Field name="type" id="type" component="select">
|
||||
<option value="general">general</option>
|
||||
<option value="icon">icon</option>
|
||||
<option value="background_image">background_image</option>
|
||||
<option value="audio">audio</option>
|
||||
<option value="video">video</option>
|
||||
<option value="transition">transition</option>
|
||||
<option value="logo">logo</option>
|
||||
<option value="favicon">favicon</option>
|
||||
<option value="document">document</option>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -44,7 +44,8 @@ const AssetsTablesPage = () => {
|
||||
|
||||
|
||||
|
||||
{label: 'Assettype', title: 'asset_type', type: 'enum', options: ['image','video','audio','file']},
|
||||
{label: 'Asset format', title: 'asset_type', type: 'enum', options: ['image','video','audio','file']},
|
||||
{label: 'Type', title: 'type', type: 'enum', options: ['icon','background_image','audio','video','transition','logo','favicon','document','general']},
|
||||
]);
|
||||
|
||||
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_ASSETS');
|
||||
|
||||
@ -170,9 +170,13 @@ const AssetsView = () => {
|
||||
|
||||
|
||||
<div className={'mb-4'}>
|
||||
<p className={'block font-bold mb-2'}>Assettype</p>
|
||||
<p className={'block font-bold mb-2'}>Asset format</p>
|
||||
<p>{assets?.asset_type ?? 'No data'}</p>
|
||||
</div>
|
||||
<div className={'mb-4'}>
|
||||
<p className={'block font-bold mb-2'}>Type</p>
|
||||
<p>{assets?.type ?? 'No data'}</p>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@ -676,4 +680,4 @@ AssetsView.getLayout = function getLayout(page: ReactElement) {
|
||||
)
|
||||
}
|
||||
|
||||
export default AssetsView;
|
||||
export default AssetsView;
|
||||
|
||||
@ -35,6 +35,7 @@ type ProjectAsset = {
|
||||
id: string;
|
||||
name?: string;
|
||||
asset_type?: 'image' | 'video' | 'audio' | 'file';
|
||||
type?: 'icon' | 'background_image' | 'audio' | 'video' | 'transition' | 'logo' | 'favicon' | 'document' | 'general';
|
||||
cdn_url?: string | null;
|
||||
storage_key?: string | null;
|
||||
};
|
||||
@ -109,7 +110,7 @@ type TransitionPreviewState = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
type EditorMenuItem = 'none' | 'background_image' | 'background_video' | 'background_audio';
|
||||
type EditorMenuItem = 'none' | 'background_image' | 'background_video' | 'background_audio' | 'create_transition';
|
||||
|
||||
const parseJsonObject = <T,>(value?: string, fallback?: T): T => {
|
||||
if (!value) return (fallback || ({} as T)) as T;
|
||||
@ -211,6 +212,7 @@ const resolveAssetPlaybackUrl = (value?: string) => {
|
||||
};
|
||||
|
||||
const isBackgroundImageAsset = (asset: ProjectAsset) => {
|
||||
if (asset.type) return asset.type === 'background_image';
|
||||
const normalizedName = String(asset.name || '').toLowerCase();
|
||||
if (!normalizedName) return false;
|
||||
const hasBackgroundKeyword = /\bbackground\b|\bbg\b|backdrop|wallpaper/.test(normalizedName);
|
||||
@ -411,13 +413,19 @@ const ConstructorPage = () => {
|
||||
[assets],
|
||||
);
|
||||
const transitionVideoAssetOptions = useMemo(() => {
|
||||
const tagged = assets
|
||||
const typedAssets = assets
|
||||
.filter((asset) => asset.type === 'transition' && asset.asset_type === 'video' && getAssetSourceValue(asset))
|
||||
.map((asset) => ({ value: getAssetSourceValue(asset), label: getAssetLabel(asset) }));
|
||||
|
||||
if (typedAssets.length > 0) return typedAssets;
|
||||
|
||||
const taggedAssets = assets
|
||||
.filter(
|
||||
(asset) => asset.asset_type === 'video' && getAssetSourceValue(asset) && /\[TRANSITION\]/i.test(String(asset.name || '')),
|
||||
)
|
||||
.map((asset) => ({ value: getAssetSourceValue(asset), label: getAssetLabel(asset) }));
|
||||
|
||||
if (tagged.length > 0) return tagged;
|
||||
if (taggedAssets.length > 0) return taggedAssets;
|
||||
|
||||
return videoAssetOptions;
|
||||
}, [assets, videoAssetOptions]);
|
||||
@ -747,7 +755,7 @@ const ConstructorPage = () => {
|
||||
environment: activePage?.environment || 'dev',
|
||||
source_key: '',
|
||||
name: sanitizedName,
|
||||
slug: `transition-${Date.now().toString().slice(-4)}`,
|
||||
slug: `transition-${createLocalId()}`,
|
||||
video_url: sanitizedVideoUrl,
|
||||
audio_url: '',
|
||||
supports_reverse: Boolean(newTransitionSupportsReverse),
|
||||
@ -1070,7 +1078,9 @@ const ConstructorPage = () => {
|
||||
? 'Background video'
|
||||
: selectedMenuItem === 'background_audio'
|
||||
? 'Background audio'
|
||||
: selectedElement?.label || 'Element editor';
|
||||
: selectedMenuItem === 'create_transition'
|
||||
? 'Create transition'
|
||||
: selectedElement?.label || 'Element editor';
|
||||
|
||||
if (backgroundImageSrc) {
|
||||
canvasBackgroundStyle.backgroundImage = `url("${backgroundImageSrc}")`;
|
||||
@ -1339,6 +1349,55 @@ const ConstructorPage = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedMenuItem === 'create_transition' && (
|
||||
<div className='rounded border border-gray-200 p-2 space-y-2'>
|
||||
<p className='text-[11px] font-semibold text-gray-600'>Create next page transition</p>
|
||||
<input
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
placeholder='Name'
|
||||
value={newTransitionName}
|
||||
onChange={(event) => setNewTransitionName(event.target.value)}
|
||||
/>
|
||||
<select
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={newTransitionVideoUrl}
|
||||
onChange={(event) => setNewTransitionVideoUrl(event.target.value)}
|
||||
>
|
||||
<option value=''>Transition video asset</option>
|
||||
{transitionVideoAssetOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<label className='flex items-center gap-2 text-[11px] text-gray-700'>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={newTransitionSupportsReverse}
|
||||
onChange={(event) => setNewTransitionSupportsReverse(event.target.checked)}
|
||||
/>
|
||||
Supports reverse playback
|
||||
</label>
|
||||
<input
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
type='number'
|
||||
min='0.2'
|
||||
step='0.1'
|
||||
value={newTransitionDurationSec}
|
||||
onChange={(event) => setNewTransitionDurationSec(Number(event.target.value || 0.7))}
|
||||
/>
|
||||
<button
|
||||
type='button'
|
||||
className='menu-action-btn'
|
||||
onClick={createTransition}
|
||||
disabled={isCreatingTransition}
|
||||
>
|
||||
<BaseIcon path={mdiSwapHorizontal} size={16} />
|
||||
<span>{isCreatingTransition ? 'Creating Transition...' : 'Create Transition'}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedElement && (
|
||||
<div className='mb-2'>
|
||||
<label className='mb-1 block text-[11px] font-semibold text-gray-600'>Label</label>
|
||||
@ -1447,52 +1506,6 @@ const ConstructorPage = () => {
|
||||
<BaseButton small color='lightDark' label='Preview Forward' onClick={() => openTransitionPreview('forward')} />
|
||||
<BaseButton small color='lightDark' label='Preview Back' onClick={() => openTransitionPreview('back')} />
|
||||
</div>
|
||||
<div className='rounded border border-gray-200 p-2 space-y-2'>
|
||||
<p className='text-[11px] font-semibold text-gray-600'>Create next page transition</p>
|
||||
<input
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
placeholder='Name'
|
||||
value={newTransitionName}
|
||||
onChange={(event) => setNewTransitionName(event.target.value)}
|
||||
/>
|
||||
<select
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
value={newTransitionVideoUrl}
|
||||
onChange={(event) => setNewTransitionVideoUrl(event.target.value)}
|
||||
>
|
||||
<option value=''>Transition video asset</option>
|
||||
{transitionVideoAssetOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<label className='flex items-center gap-2 text-[11px] text-gray-700'>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={newTransitionSupportsReverse}
|
||||
onChange={(event) => setNewTransitionSupportsReverse(event.target.checked)}
|
||||
/>
|
||||
Supports reverse playback
|
||||
</label>
|
||||
<input
|
||||
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
|
||||
type='number'
|
||||
min='0.2'
|
||||
step='0.1'
|
||||
value={newTransitionDurationSec}
|
||||
onChange={(event) => setNewTransitionDurationSec(Number(event.target.value || 0.7))}
|
||||
/>
|
||||
<button
|
||||
type='button'
|
||||
className='menu-action-btn'
|
||||
onClick={createTransition}
|
||||
disabled={isCreatingTransition}
|
||||
>
|
||||
<BaseIcon path={mdiSwapHorizontal} size={16} />
|
||||
<span>{isCreatingTransition ? 'Creating Transition...' : 'Create Transition'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1729,6 +1742,10 @@ const ConstructorPage = () => {
|
||||
<BaseIcon path={mdiSwapHorizontal} size={16} />
|
||||
<span>Add Navigation</span>
|
||||
</button>
|
||||
<button type='button' className='menu-action-btn' onClick={() => selectMenuItemForEdit('create_transition')}>
|
||||
<BaseIcon path={mdiSwapHorizontal} size={16} />
|
||||
<span>Add Transition</span>
|
||||
</button>
|
||||
<button type='button' className='menu-action-btn' onClick={() => addElement('gallery')}>
|
||||
<BaseIcon path={mdiImageMultiple} size={16} />
|
||||
<span>Add Gallery</span>
|
||||
|
||||
@ -432,8 +432,7 @@ const EditProjectsPage = () => {
|
||||
typeof asset?.cdn_url === 'string' &&
|
||||
asset.cdn_url &&
|
||||
asset?.asset_type === 'image' &&
|
||||
typeof asset?.name === 'string' &&
|
||||
asset.name.startsWith('[LOGO] '),
|
||||
(asset?.type === 'logo' || (typeof asset?.name === 'string' && asset.name.startsWith('[LOGO] '))),
|
||||
)
|
||||
setLogoAssets(taggedLogoAssets)
|
||||
} catch (error: any) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user