Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f35d90dc51 |
@ -1,6 +1,4 @@
|
||||
|
||||
|
||||
|
||||
require('dotenv').config();
|
||||
const os = require('os');
|
||||
|
||||
const config = {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
const db = require('../models');
|
||||
const FileDBApi = require('./file');
|
||||
const crypto = require('crypto');
|
||||
@ -21,11 +20,6 @@ module.exports = class AssetsDBApi {
|
||||
{
|
||||
id: data.id || undefined,
|
||||
|
||||
id: data.id
|
||||
||
|
||||
null
|
||||
,
|
||||
|
||||
title: data.title
|
||||
||
|
||||
null
|
||||
@ -57,6 +51,11 @@ module.exports = class AssetsDBApi {
|
||||
|
||||
,
|
||||
|
||||
requiredGdpLevel: data.requiredGdpLevel
|
||||
||
|
||||
1
|
||||
,
|
||||
|
||||
importHash: data.importHash || null,
|
||||
createdById: currentUser.id,
|
||||
updatedById: currentUser.id,
|
||||
@ -114,11 +113,6 @@ module.exports = class AssetsDBApi {
|
||||
const assetsData = data.map((item, index) => ({
|
||||
id: item.id || undefined,
|
||||
|
||||
id: item.id
|
||||
||
|
||||
null
|
||||
,
|
||||
|
||||
title: item.title
|
||||
||
|
||||
null
|
||||
@ -148,6 +142,11 @@ module.exports = class AssetsDBApi {
|
||||
||
|
||||
false
|
||||
|
||||
,
|
||||
|
||||
requiredGdpLevel: item.requiredGdpLevel
|
||||
||
|
||||
1
|
||||
,
|
||||
|
||||
importHash: item.importHash || null,
|
||||
@ -221,6 +220,7 @@ module.exports = class AssetsDBApi {
|
||||
|
||||
if (data.allow_download !== undefined) updatePayload.allow_download = data.allow_download;
|
||||
|
||||
if (data.requiredGdpLevel !== undefined) updatePayload.requiredGdpLevel = data.requiredGdpLevel;
|
||||
|
||||
updatePayload.updatedById = currentUser.id;
|
||||
|
||||
@ -699,4 +699,3 @@ module.exports = class AssetsDBApi {
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
const db = require('../models');
|
||||
const FileDBApi = require('./file');
|
||||
const crypto = require('crypto');
|
||||
@ -85,6 +84,11 @@ module.exports = class UsersDBApi {
|
||||
null
|
||||
,
|
||||
|
||||
gdpLevel: data.data.gdpLevel
|
||||
||
|
||||
1
|
||||
,
|
||||
|
||||
importHash: data.data.importHash || null,
|
||||
createdById: currentUser.id,
|
||||
updatedById: currentUser.id,
|
||||
@ -203,6 +207,11 @@ module.exports = class UsersDBApi {
|
||||
provider: item.provider
|
||||
||
|
||||
null
|
||||
,
|
||||
|
||||
gdpLevel: item.gdpLevel
|
||||
||
|
||||
1
|
||||
,
|
||||
|
||||
importHash: item.importHash || null,
|
||||
@ -298,6 +307,7 @@ module.exports = class UsersDBApi {
|
||||
|
||||
if (data.provider !== undefined) updatePayload.provider = data.provider;
|
||||
|
||||
if (data.gdpLevel !== undefined) updatePayload.gdpLevel = data.gdpLevel;
|
||||
|
||||
updatePayload.updatedById = currentUser.id;
|
||||
|
||||
@ -936,4 +946,3 @@ module.exports = class UsersDBApi {
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
module.exports = {
|
||||
production: {
|
||||
@ -12,11 +12,12 @@ module.exports = {
|
||||
seederStorage: 'sequelize',
|
||||
},
|
||||
development: {
|
||||
username: 'postgres',
|
||||
username: process.env.DB_USER || 'postgres',
|
||||
dialect: 'postgres',
|
||||
password: '',
|
||||
database: 'db_app_draft',
|
||||
password: process.env.DB_PASS || '',
|
||||
database: process.env.DB_NAME || 'db_app_draft',
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: process.env.DB_PORT,
|
||||
logging: console.log,
|
||||
seederStorage: 'sequelize',
|
||||
},
|
||||
|
||||
@ -494,21 +494,6 @@ module.exports = {
|
||||
|
||||
|
||||
|
||||
await queryInterface.addColumn(
|
||||
'assets',
|
||||
'id',
|
||||
{
|
||||
type: Sequelize.DataTypes.INTEGER,
|
||||
|
||||
|
||||
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
await queryInterface.addColumn(
|
||||
'assets',
|
||||
'title',
|
||||
@ -634,21 +619,6 @@ module.exports = {
|
||||
|
||||
|
||||
|
||||
await queryInterface.addColumn(
|
||||
'product_categories',
|
||||
'id',
|
||||
{
|
||||
type: Sequelize.DataTypes.INTEGER,
|
||||
|
||||
|
||||
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
await queryInterface.addColumn(
|
||||
'product_categories',
|
||||
'name',
|
||||
@ -679,21 +649,6 @@ module.exports = {
|
||||
|
||||
|
||||
|
||||
await queryInterface.addColumn(
|
||||
'tags',
|
||||
'id',
|
||||
{
|
||||
type: Sequelize.DataTypes.INTEGER,
|
||||
|
||||
|
||||
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
await queryInterface.addColumn(
|
||||
'tags',
|
||||
'name',
|
||||
@ -724,21 +679,6 @@ module.exports = {
|
||||
|
||||
|
||||
|
||||
await queryInterface.addColumn(
|
||||
'download_logs',
|
||||
'id',
|
||||
{
|
||||
type: Sequelize.DataTypes.INTEGER,
|
||||
|
||||
|
||||
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
await queryInterface.addColumn(
|
||||
'download_logs',
|
||||
'userId',
|
||||
|
||||
49
backend/src/db/migrations/20260202120000-wellmax-setup.js
Normal file
49
backend/src/db/migrations/20260202120000-wellmax-setup.js
Normal file
@ -0,0 +1,49 @@
|
||||
'use strict';
|
||||
|
||||
const { v4: uuid } = require('uuid');
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
const createdAt = new Date();
|
||||
const updatedAt = new Date();
|
||||
|
||||
// 1. Seed Product Categories
|
||||
const categories = [
|
||||
{ id: uuid(), name: 'High Bay', description: 'Industrial High Bay lighting', createdAt, updatedAt },
|
||||
{ id: uuid(), name: 'Panel', description: 'LED Panels', createdAt, updatedAt },
|
||||
{ id: uuid(), name: 'Tube', description: 'LED Tubes', createdAt, updatedAt },
|
||||
{ id: uuid(), name: 'Downlight', description: 'LED Downlights', createdAt, updatedAt },
|
||||
{ id: uuid(), name: 'Others', description: 'Other lighting solutions', createdAt, updatedAt },
|
||||
];
|
||||
await queryInterface.bulkInsert('product_categories', categories);
|
||||
|
||||
// 2. Grant READ_ASSETS, READ_PRODUCT_CATEGORIES, READ_TAGS to Public role
|
||||
const [publicRole] = await queryInterface.sequelize.query(
|
||||
"SELECT id FROM roles WHERE name = 'Public' LIMIT 1;"
|
||||
);
|
||||
|
||||
if (publicRole && publicRole[0] && publicRole[0].length > 0) {
|
||||
const publicRoleId = publicRole[0][0].id;
|
||||
|
||||
const permissionsToGrant = ['READ_ASSETS', 'READ_PRODUCT_CATEGORIES', 'READ_TAGS'];
|
||||
const [perms] = await queryInterface.sequelize.query(
|
||||
`SELECT id FROM permissions WHERE name IN ('${permissionsToGrant.join("','")}')`
|
||||
);
|
||||
|
||||
if (perms && perms.length > 0) {
|
||||
const rolesPermissions = perms.map(p => ({
|
||||
createdAt,
|
||||
updatedAt,
|
||||
roles_permissionsId: publicRoleId,
|
||||
permissionId: p.id
|
||||
}));
|
||||
|
||||
await queryInterface.bulkInsert('rolesPermissionsPermissions', rolesPermissions);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
// Revert logic if needed
|
||||
}
|
||||
};
|
||||
19
backend/src/db/migrations/20260205100000-add-gdp-levels.js
Normal file
19
backend/src/db/migrations/20260205100000-add-gdp-levels.js
Normal file
@ -0,0 +1,19 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('users', 'gdpLevel', {
|
||||
type: Sequelize.INTEGER,
|
||||
defaultValue: 1, // Blue
|
||||
allowNull: false,
|
||||
});
|
||||
await queryInterface.addColumn('assets', 'requiredGdpLevel', {
|
||||
type: Sequelize.INTEGER,
|
||||
defaultValue: 1, // Blue
|
||||
allowNull: false,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn('users', 'gdpLevel');
|
||||
await queryInterface.removeColumn('assets', 'requiredGdpLevel');
|
||||
},
|
||||
};
|
||||
@ -14,86 +14,42 @@ module.exports = function(sequelize, DataTypes) {
|
||||
primaryKey: true,
|
||||
},
|
||||
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
title: {
|
||||
type: DataTypes.TEXT,
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
asset_type: {
|
||||
type: DataTypes.ENUM,
|
||||
|
||||
|
||||
|
||||
values: [
|
||||
|
||||
"image",
|
||||
|
||||
|
||||
"video",
|
||||
|
||||
|
||||
"artwork"
|
||||
|
||||
"image",
|
||||
"video",
|
||||
"artwork"
|
||||
],
|
||||
|
||||
},
|
||||
|
||||
asset_category: {
|
||||
type: DataTypes.ENUM,
|
||||
|
||||
|
||||
|
||||
values: [
|
||||
|
||||
"ProductPhotos",
|
||||
|
||||
|
||||
"ApplicationScenarios",
|
||||
|
||||
|
||||
"Videos",
|
||||
|
||||
|
||||
"Packaging&Artwork",
|
||||
|
||||
|
||||
"Logos&BrandAssets"
|
||||
|
||||
"ProductPhotos",
|
||||
"ApplicationScenarios",
|
||||
"Videos",
|
||||
"Packaging&Artwork",
|
||||
"Logos&BrandAssets"
|
||||
],
|
||||
|
||||
},
|
||||
|
||||
uploaded_at: {
|
||||
type: DataTypes.DATE,
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
allow_download: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
importHash: {
|
||||
@ -101,6 +57,12 @@ allow_download: {
|
||||
allowNull: true,
|
||||
unique: true,
|
||||
},
|
||||
|
||||
requiredGdpLevel: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 1, // Blue
|
||||
allowNull: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
@ -147,16 +109,6 @@ allow_download: {
|
||||
through: 'assetsTagsTags',
|
||||
});
|
||||
|
||||
|
||||
/// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
db.assets.hasMany(db.download_logs, {
|
||||
as: 'download_logs_asset',
|
||||
foreignKey: {
|
||||
@ -165,12 +117,6 @@ allow_download: {
|
||||
constraints: false,
|
||||
});
|
||||
|
||||
|
||||
|
||||
//end loop
|
||||
|
||||
|
||||
|
||||
db.assets.belongsTo(db.users, {
|
||||
as: 'uploaded_by',
|
||||
foreignKey: {
|
||||
@ -179,8 +125,6 @@ allow_download: {
|
||||
constraints: false,
|
||||
});
|
||||
|
||||
|
||||
|
||||
db.assets.hasMany(db.file, {
|
||||
as: 'thumbnail',
|
||||
foreignKey: 'belongsToId',
|
||||
@ -201,7 +145,6 @@ allow_download: {
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
db.assets.belongsTo(db.users, {
|
||||
as: 'createdBy',
|
||||
});
|
||||
@ -211,9 +154,5 @@ allow_download: {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
return assets;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -14,13 +14,6 @@ module.exports = function(sequelize, DataTypes) {
|
||||
primaryKey: true,
|
||||
},
|
||||
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
downloaded_at: {
|
||||
type: DataTypes.DATE,
|
||||
|
||||
@ -105,5 +98,3 @@ file_size_bytes: {
|
||||
|
||||
return download_logs;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -14,13 +14,6 @@ module.exports = function(sequelize, DataTypes) {
|
||||
primaryKey: true,
|
||||
},
|
||||
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
name: {
|
||||
type: DataTypes.TEXT,
|
||||
|
||||
@ -82,5 +75,3 @@ description: {
|
||||
|
||||
return product_categories;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -14,13 +14,6 @@ module.exports = function(sequelize, DataTypes) {
|
||||
primaryKey: true,
|
||||
},
|
||||
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
name: {
|
||||
type: DataTypes.TEXT,
|
||||
|
||||
@ -82,5 +75,3 @@ slug: {
|
||||
|
||||
return tags;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -109,6 +109,12 @@ provider: {
|
||||
allowNull: true,
|
||||
unique: true,
|
||||
},
|
||||
|
||||
gdpLevel: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 1, // Blue
|
||||
allowNull: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
@ -243,4 +249,3 @@ function trimStringFields(users) {
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
|
||||
@ -68,7 +68,7 @@ await queryInterface.bulkInsert("permissions", [{ id: getId(`READ_API_DOCS`), cr
|
||||
await queryInterface.bulkInsert("permissions", [{ id: getId(`CREATE_SEARCH`), createdAt, updatedAt, name: `CREATE_SEARCH`}]);
|
||||
|
||||
|
||||
await queryInterface.sequelize.query(`create table "rolesPermissionsPermissions"
|
||||
await queryInterface.sequelize.query(`create table if not exists "rolesPermissionsPermissions"
|
||||
(
|
||||
"createdAt" timestamp with time zone not null,
|
||||
"updatedAt" timestamp with time zone not null,
|
||||
|
||||
@ -46,16 +46,6 @@ const AssetsData = [
|
||||
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
"id": 1,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"title": "High Bay Warehouse - Front View",
|
||||
|
||||
|
||||
@ -134,16 +124,6 @@ const AssetsData = [
|
||||
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
"id": 2,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"title": "High Bay Installation Guide - Video",
|
||||
|
||||
|
||||
@ -222,16 +202,6 @@ const AssetsData = [
|
||||
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
"id": 3,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"title": "WELLMAX Primary Logo - EPS",
|
||||
|
||||
|
||||
@ -310,16 +280,6 @@ const AssetsData = [
|
||||
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
"id": 4,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"title": "Packaging Mockup - High Bay",
|
||||
|
||||
|
||||
@ -398,16 +358,6 @@ const AssetsData = [
|
||||
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
"id": 5,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"title": "Downlight Living Room Scenario",
|
||||
|
||||
|
||||
@ -492,16 +442,6 @@ const ProductCategoriesData = [
|
||||
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
"id": 1,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"name": "High Bay",
|
||||
|
||||
|
||||
@ -517,16 +457,6 @@ const ProductCategoriesData = [
|
||||
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
"id": 2,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"name": "Panel",
|
||||
|
||||
|
||||
@ -542,16 +472,6 @@ const ProductCategoriesData = [
|
||||
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
"id": 3,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"name": "Tube",
|
||||
|
||||
|
||||
@ -567,16 +487,6 @@ const ProductCategoriesData = [
|
||||
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
"id": 4,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"name": "Downlight",
|
||||
|
||||
|
||||
@ -592,16 +502,6 @@ const ProductCategoriesData = [
|
||||
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
"id": 5,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"name": "Others",
|
||||
|
||||
|
||||
@ -623,16 +523,6 @@ const TagsData = [
|
||||
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
"id": 1,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"name": "warehouse",
|
||||
|
||||
|
||||
@ -648,16 +538,6 @@ const TagsData = [
|
||||
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
"id": 2,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"name": "installation",
|
||||
|
||||
|
||||
@ -673,16 +553,6 @@ const TagsData = [
|
||||
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
"id": 3,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"name": "indoor",
|
||||
|
||||
|
||||
@ -698,16 +568,6 @@ const TagsData = [
|
||||
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
"id": 4,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"name": "logo",
|
||||
|
||||
|
||||
@ -723,16 +583,6 @@ const TagsData = [
|
||||
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
"id": 5,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"name": "packaging",
|
||||
|
||||
|
||||
@ -754,16 +604,6 @@ const DownloadLogsData = [
|
||||
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
"id": 1,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
|
||||
@ -800,16 +640,6 @@ const DownloadLogsData = [
|
||||
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
"id": 2,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
|
||||
@ -846,16 +676,6 @@ const DownloadLogsData = [
|
||||
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
"id": 3,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
|
||||
@ -892,16 +712,6 @@ const DownloadLogsData = [
|
||||
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
"id": 4,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
|
||||
@ -938,16 +748,6 @@ const DownloadLogsData = [
|
||||
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
"id": 5,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// type code here for "relation_one" field
|
||||
|
||||
|
||||
|
||||
@ -92,6 +92,14 @@ app.use('/api/file', fileRoutes);
|
||||
app.use('/api/pexels', pexelsRoutes);
|
||||
app.enable('trust proxy');
|
||||
|
||||
const optionalAuthenticate = (req, res, next) => {
|
||||
passport.authenticate('jwt', { session: false }, (err, user, info) => {
|
||||
if (err) return next(err);
|
||||
if (user) req.currentUser = user;
|
||||
next();
|
||||
})(req, res, next);
|
||||
};
|
||||
|
||||
|
||||
app.use('/api/users', passport.authenticate('jwt', {session: false}), usersRoutes);
|
||||
|
||||
@ -99,11 +107,10 @@ app.use('/api/roles', passport.authenticate('jwt', {session: false}), rolesRoute
|
||||
|
||||
app.use('/api/permissions', passport.authenticate('jwt', {session: false}), permissionsRoutes);
|
||||
|
||||
app.use('/api/assets', passport.authenticate('jwt', {session: false}), assetsRoutes);
|
||||
|
||||
app.use('/api/product_categories', passport.authenticate('jwt', {session: false}), product_categoriesRoutes);
|
||||
|
||||
app.use('/api/tags', passport.authenticate('jwt', {session: false}), tagsRoutes);
|
||||
// Publicly accessible routes with optional authentication
|
||||
app.use('/api/assets', optionalAuthenticate, assetsRoutes);
|
||||
app.use('/api/product_categories', optionalAuthenticate, product_categoriesRoutes);
|
||||
app.use('/api/tags', optionalAuthenticate, tagsRoutes);
|
||||
|
||||
app.use('/api/download_logs', passport.authenticate('jwt', {session: false}), download_logsRoutes);
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
const express = require('express');
|
||||
|
||||
const AssetsService = require('../services/assets');
|
||||
@ -15,6 +14,13 @@ const {
|
||||
checkCrudPermissions,
|
||||
} = require('../middlewares/check-permissions');
|
||||
|
||||
/**
|
||||
* Custom download route for assets with role verification and logging
|
||||
*/
|
||||
router.get('/:id/download', wrapAsync(async (req, res) => {
|
||||
await AssetsService.download(req.params.id, req.currentUser, req, res);
|
||||
}));
|
||||
|
||||
router.use(checkCrudPermissions('assets'));
|
||||
|
||||
|
||||
|
||||
@ -1,15 +1,13 @@
|
||||
const db = require('../db/models');
|
||||
const AssetsDBApi = require('../db/api/assets');
|
||||
const Download_logsDBApi = require('../db/api/download_logs');
|
||||
const processFile = require("../middlewares/upload");
|
||||
const ValidationError = require('./notifications/errors/validation');
|
||||
const csv = require('csv-parser');
|
||||
const axios = require('axios');
|
||||
const config = require('../config');
|
||||
const stream = require('stream');
|
||||
|
||||
|
||||
|
||||
|
||||
const fileService = require('./file');
|
||||
|
||||
module.exports = class AssetsService {
|
||||
static async create(data, currentUser) {
|
||||
@ -30,6 +28,62 @@ module.exports = class AssetsService {
|
||||
}
|
||||
};
|
||||
|
||||
static async download(id, currentUser, req, res) {
|
||||
const asset = await AssetsDBApi.findBy({ id });
|
||||
if (!asset) {
|
||||
throw new ValidationError('assetsNotFound');
|
||||
}
|
||||
|
||||
// Check if user has permission to download
|
||||
const role = currentUser.app_role.name;
|
||||
|
||||
if (role !== 'Administrator') {
|
||||
if (role !== 'GDP_Partner') {
|
||||
throw new ValidationError('Unauthorized to download original assets');
|
||||
}
|
||||
|
||||
// Check GDP Level
|
||||
const userLevel = currentUser.gdpLevel || 1; // Default to 1 (Blue)
|
||||
const requiredLevel = asset.requiredGdpLevel || 1; // Default to 1
|
||||
|
||||
if (userLevel < requiredLevel) {
|
||||
throw new ValidationError(`GDP Level insufficient. Required: ${requiredLevel}, Your Level: ${userLevel}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Log the download
|
||||
const transaction = await db.sequelize.transaction();
|
||||
try {
|
||||
await Download_logsDBApi.create({
|
||||
assetId: asset.id,
|
||||
userId: currentUser.id,
|
||||
ip_address: req.ip || req.connection.remoteAddress,
|
||||
downloaded_at: new Date(),
|
||||
}, { transaction, currentUser });
|
||||
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
console.error('Failed to log download:', error);
|
||||
// We continue even if logging fails, but we logged the error
|
||||
}
|
||||
|
||||
// Get the original file
|
||||
const originalFiles = asset.original_file || [];
|
||||
if (originalFiles.length === 0) {
|
||||
throw new ValidationError('Original file not found for this asset');
|
||||
}
|
||||
|
||||
const file = originalFiles[0];
|
||||
req.query.privateUrl = file.privateUrl;
|
||||
|
||||
if (process.env.NODE_ENV == "production" || process.env.NEXT_PUBLIC_BACK_API) {
|
||||
return fileService.downloadGCloud(req, res);
|
||||
} else {
|
||||
return fileService.downloadLocal(req, res);
|
||||
}
|
||||
}
|
||||
|
||||
static async bulkImport(req, res, sendInvitationEmails = true, host) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
|
||||
@ -51,7 +105,7 @@ module.exports = class AssetsService {
|
||||
.on('error', (error) => reject(error));
|
||||
})
|
||||
|
||||
await AssetsDBApi.bulkImport(results, {
|
||||
await Download_logsDBApi.bulkImport(results, {
|
||||
transaction,
|
||||
ignoreDuplicates: true,
|
||||
validate: true,
|
||||
@ -134,5 +188,3 @@ module.exports = class AssetsService {
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
21
fix_migration.py
Normal file
21
fix_migration.py
Normal file
@ -0,0 +1,21 @@
|
||||
import re
|
||||
|
||||
file_path = 'backend/src/db/migrations/1770016660721.js'
|
||||
|
||||
with open(file_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Pattern to match the addColumn block for 'id'
|
||||
# We use re.DOTALL so . matches newlines
|
||||
# We look for 'id' as the second argument
|
||||
pattern = r"\s*await queryInterface\.addColumn\(\s*'[^']+',\s*'id',\s*\{\s*type: Sequelize\.DataTypes\.INTEGER,.*?\},\s*\{ transaction \}\s*\);"
|
||||
|
||||
matches = re.findall(pattern, content, re.DOTALL)
|
||||
print(f"Found {len(matches)} matches.")
|
||||
for m in matches:
|
||||
print("Match snippet:", m.strip()[:100])
|
||||
|
||||
new_content = re.sub(pattern, "", content, flags=re.DOTALL)
|
||||
|
||||
with open(file_path, 'w') as f:
|
||||
f.write(new_content)
|
||||
26
fix_seeder.py
Normal file
26
fix_seeder.py
Normal file
@ -0,0 +1,26 @@
|
||||
import re
|
||||
|
||||
file_path = 'backend/src/db/seeders/20231127130745-sample-data.js'
|
||||
|
||||
with open(file_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Pattern to remove "id": <number>, lines
|
||||
# We handle potential indentation and trailing commas
|
||||
# regex:
|
||||
# ^ start of line
|
||||
# \s* whitespace
|
||||
# "id": "id" key
|
||||
# \s* whitespace
|
||||
# \d+ integer
|
||||
# \s* whitespace
|
||||
# ,? optional comma
|
||||
# \s* whitespace
|
||||
# $ end of line
|
||||
pattern = r'^\s*"id":\s*\d+\s*,?\s*$'
|
||||
|
||||
# We need re.MULTILINE to match ^ and $ for each line
|
||||
new_content = re.sub(pattern, '', content, flags=re.MULTILINE)
|
||||
|
||||
with open(file_path, 'w') as f:
|
||||
f.write(new_content)
|
||||
15
fix_seeder_table.py
Normal file
15
fix_seeder_table.py
Normal file
@ -0,0 +1,15 @@
|
||||
import re
|
||||
|
||||
file_path = 'backend/src/db/seeders/20200430130760-user-roles.js'
|
||||
|
||||
with open(file_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Replace create table with create table if not exists
|
||||
pattern = 'create table "rolesPermissionsPermissions"'
|
||||
replacement = 'create table if not exists "rolesPermissionsPermissions"'
|
||||
|
||||
new_content = content.replace(pattern, replacement)
|
||||
|
||||
with open(file_path, 'w') as f:
|
||||
f.write(new_content)
|
||||
263
frontend/src/components/Assets/GalleryGrid.tsx
Normal file
263
frontend/src/components/Assets/GalleryGrid.tsx
Normal file
@ -0,0 +1,263 @@
|
||||
import React, { useState } from 'react';
|
||||
import ImageField from '../ImageField';
|
||||
import { useAppSelector } from '../../stores/hooks';
|
||||
import dataFormatter from '../../helpers/dataFormatter';
|
||||
import { Pagination } from '../Pagination';
|
||||
import LoadingSpinner from "../LoadingSpinner";
|
||||
import { mdiDownload, mdiEye, mdiLock } from '@mdi/js';
|
||||
import BaseButton from '../BaseButton';
|
||||
import CardBoxModal from '../CardBoxModal';
|
||||
import axios from 'axios';
|
||||
import { saveAs } from 'file-saver';
|
||||
import BaseIcon from '../BaseIcon';
|
||||
|
||||
type Props = {
|
||||
assets: any[];
|
||||
loading: boolean;
|
||||
currentPage: number;
|
||||
numPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
};
|
||||
|
||||
const gdpLevels = {
|
||||
1: 'Blue',
|
||||
2: 'Silver',
|
||||
3: 'Gold',
|
||||
4: 'Platinum'
|
||||
};
|
||||
|
||||
const GalleryGrid = ({
|
||||
assets,
|
||||
loading,
|
||||
currentPage,
|
||||
numPages,
|
||||
onPageChange,
|
||||
}: Props) => {
|
||||
const corners = useAppSelector((state) => state.style.corners);
|
||||
const currentUser = useAppSelector((state) => state.auth.currentUser);
|
||||
const [selectedAsset, setSelectedAsset] = useState<any>(null);
|
||||
const [isPreviewActive, setIsPreviewActive] = useState(false);
|
||||
const [isDownloading, setIsDownloading] = useState<string | null>(null);
|
||||
|
||||
const isPartner = currentUser?.app_role?.name === 'GDP_Partner' || currentUser?.app_role?.name === 'Administrator';
|
||||
|
||||
const canDownload = (asset: any) => {
|
||||
if (!currentUser) return false;
|
||||
const role = currentUser.app_role?.name;
|
||||
if (role === 'Administrator') return true;
|
||||
if (role !== 'GDP_Partner') return false;
|
||||
|
||||
const userLevel = currentUser.gdpLevel || 1;
|
||||
const requiredLevel = asset.requiredGdpLevel || 1;
|
||||
return userLevel >= requiredLevel;
|
||||
};
|
||||
|
||||
const handlePreview = (asset: any) => {
|
||||
setSelectedAsset(asset);
|
||||
setIsPreviewActive(true);
|
||||
};
|
||||
|
||||
const handleDownload = async (e: React.MouseEvent, asset: any) => {
|
||||
e.stopPropagation();
|
||||
if (isDownloading) return;
|
||||
|
||||
setIsDownloading(asset.id);
|
||||
try {
|
||||
const response = await axios.get(`/assets/${asset.id}/download`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
// Try to get filename from content-disposition
|
||||
let fileName = asset.title || 'download';
|
||||
const contentDisposition = response.headers['content-disposition'];
|
||||
if (contentDisposition) {
|
||||
const fileNameMatch = contentDisposition.match(/filename="(.+)"/);
|
||||
if (fileNameMatch && fileNameMatch.length === 2) {
|
||||
fileName = fileNameMatch[1];
|
||||
}
|
||||
} else {
|
||||
// Fallback to asset title + original extension if possible
|
||||
const originalFile = dataFormatter.filesFormatter(asset.original_file)[0];
|
||||
if (originalFile) {
|
||||
const ext = originalFile.name.split('.').pop();
|
||||
if (ext && !fileName.endsWith(ext)) {
|
||||
fileName = `${fileName}.${ext}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveAs(response.data, fileName);
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error);
|
||||
alert('Download failed. Please try again or contact support.');
|
||||
} finally {
|
||||
setIsDownloading(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="py-8">
|
||||
{loading && <LoadingSpinner />}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">
|
||||
{!loading && assets.map((asset) => (
|
||||
<div
|
||||
key={asset.id}
|
||||
className={`bg-white border border-gray-200 ${corners} overflow-hidden shadow-sm hover:shadow-md transition-shadow group flex flex-col`}
|
||||
>
|
||||
{/* Thumbnail Container */}
|
||||
<div className="relative aspect-video overflow-hidden bg-gray-100">
|
||||
<ImageField
|
||||
name="asset-thumb"
|
||||
image={asset.thumbnail}
|
||||
className="w-full h-full"
|
||||
imageClassName="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
/>
|
||||
{asset.asset_type === 'video' && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-20">
|
||||
<div className="w-12 h-12 rounded-full bg-white bg-opacity-80 flex items-center justify-center">
|
||||
<div className="w-0 h-0 border-t-[8px] border-t-transparent border-l-[12px] border-l-[#003366] border-b-[8px] border-b-transparent ml-1" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Level Badge Overlay if locked */}
|
||||
{isPartner && !canDownload(asset) && (
|
||||
<div className="absolute top-2 right-2 bg-black bg-opacity-75 text-white text-xs px-2 py-1 rounded flex items-center">
|
||||
<BaseIcon path={mdiLock} size={14} className="mr-1" />
|
||||
{gdpLevels[asset.requiredGdpLevel || 1]}+
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-5 flex-grow flex flex-col">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<span className="text-[10px] uppercase tracking-wider font-bold text-blue-600 bg-blue-50 px-2 py-1 rounded">
|
||||
{asset.asset_category || asset.asset_type}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-2 line-clamp-1">{asset.title}</h3>
|
||||
<p className="text-sm text-gray-500 line-clamp-2 mb-4 flex-grow">
|
||||
{asset.description || 'No description provided.'}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<BaseButton
|
||||
icon={mdiEye}
|
||||
label="Preview"
|
||||
color="info"
|
||||
outline
|
||||
small
|
||||
className="flex-1"
|
||||
onClick={() => handlePreview(asset)}
|
||||
/>
|
||||
{isPartner && (
|
||||
<div className="flex-1">
|
||||
{canDownload(asset) ? (
|
||||
<BaseButton
|
||||
icon={mdiDownload}
|
||||
label={isDownloading === asset.id ? '...' : 'Download'}
|
||||
color="success"
|
||||
small
|
||||
className="w-full"
|
||||
disabled={!!isDownloading}
|
||||
onClick={(e) => handleDownload(e, asset)}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
className="w-full py-1 text-xs text-gray-500 bg-gray-100 rounded border border-gray-200 cursor-not-allowed flex flex-col items-center justify-center h-[38px]"
|
||||
disabled
|
||||
>
|
||||
<span className="font-semibold">Locked</span>
|
||||
<span className="text-[10px]">{gdpLevels[asset.requiredGdpLevel || 1]}+ Required</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!loading && assets.length === 0 && (
|
||||
<div className="text-center py-20 border-2 border-dashed border-gray-200 rounded-lg">
|
||||
<p className="text-gray-500 text-lg">No assets found in this category.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-center mt-12">
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
numPages={numPages}
|
||||
setCurrentPage={onPageChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Preview Modal */}
|
||||
<CardBoxModal
|
||||
title={selectedAsset?.title || 'Asset Preview'}
|
||||
isActive={isPreviewActive}
|
||||
onConfirm={() => setIsPreviewActive(false)}
|
||||
onCancel={() => setIsPreviewActive(false)}
|
||||
buttonLabel="Close"
|
||||
buttonColor="info"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg overflow-hidden bg-gray-100 max-h-[60vh] flex items-center justify-center">
|
||||
{selectedAsset?.asset_type === 'video' ? (
|
||||
<video controls className="max-w-full max-h-full">
|
||||
{dataFormatter.filesFormatter(selectedAsset.original_file).map((file, i) => (
|
||||
<source key={i} src={file.publicUrl} type="video/mp4" />
|
||||
))}
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
) : (
|
||||
<ImageField
|
||||
name="preview-img"
|
||||
image={selectedAsset?.thumbnail}
|
||||
className="max-w-full max-h-full"
|
||||
imageClassName="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold text-gray-900">Description</h4>
|
||||
<p className="text-gray-600">{selectedAsset?.description || 'N/A'}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="font-bold block">Category</span>
|
||||
<span className="text-gray-600">{asset_category(selectedAsset)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-bold block">Type</span>
|
||||
<span className="text-gray-600 text-capitalize">{selectedAsset?.asset_type || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-bold block">Required Level</span>
|
||||
<span className="text-gray-600">{gdpLevels[selectedAsset?.requiredGdpLevel || 1]}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-bold block">Product Categories</span>
|
||||
<span className="text-gray-600">
|
||||
{dataFormatter.product_categoriesManyListFormatter(selectedAsset?.product_categories).join(', ') || 'None'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<span className="font-bold block">Tags</span>
|
||||
<span className="text-gray-600">
|
||||
{dataFormatter.tagsManyListFormatter(selectedAsset?.tags).join(', ') || 'None'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardBoxModal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Helper for safe access inside modal
|
||||
const asset_category = (asset) => asset?.asset_category || 'N/A';
|
||||
|
||||
export default GalleryGrid;
|
||||
@ -1,6 +1,5 @@
|
||||
import React, {useEffect, useRef} from 'react'
|
||||
import React, {useEffect, useRef, useState} from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
||||
import BaseDivider from './BaseDivider'
|
||||
import BaseIcon from './BaseIcon'
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import React, { ReactNode, useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import React, { ReactNode, useEffect, useState } from 'react'
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
||||
import menuAside from '../menuAside'
|
||||
|
||||
@ -362,6 +362,8 @@ const EditAssetsPage = () => {
|
||||
|
||||
allow_download: false,
|
||||
|
||||
requiredGdpLevel: 1,
|
||||
|
||||
|
||||
|
||||
|
||||
@ -944,6 +946,15 @@ const EditAssetsPage = () => {
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Required GDP Level" labelFor="requiredGdpLevel">
|
||||
<Field name="requiredGdpLevel" id="requiredGdpLevel" component="select">
|
||||
<option value={1}>Blue</option>
|
||||
<option value={2}>Silver</option>
|
||||
<option value={3}>Gold</option>
|
||||
<option value={4}>Platinum</option>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -216,6 +216,7 @@ const initialValues = {
|
||||
|
||||
allow_download: false,
|
||||
|
||||
requiredGdpLevel: 1,
|
||||
|
||||
|
||||
|
||||
@ -690,6 +691,15 @@ const AssetsNew = () => {
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Required GDP Level" labelFor="requiredGdpLevel">
|
||||
<Field name="requiredGdpLevel" id="requiredGdpLevel" component="select">
|
||||
<option value={1}>Blue</option>
|
||||
<option value={2}>Silver</option>
|
||||
<option value={3}>Gold</option>
|
||||
<option value={4}>Platinum</option>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
173
frontend/src/pages/gallery.tsx
Normal file
173
frontend/src/pages/gallery.tsx
Normal file
@ -0,0 +1,173 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import Head from 'next/head';
|
||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||
import { fetch } from '../stores/assets/assetsSlice';
|
||||
import { fetch as fetchCategories } from '../stores/product_categories/product_categoriesSlice';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
import SectionMain from '../components/SectionMain';
|
||||
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
|
||||
import { mdiLibraryOutline, mdiFilterOutline } from '@mdi/js';
|
||||
import { getPageTitle } from '../config';
|
||||
import GalleryGrid from '../components/Assets/GalleryGrid';
|
||||
import CardBox from '../components/CardBox';
|
||||
import FormField from '../components/FormField';
|
||||
import SelectField from '../components/SelectField';
|
||||
import BaseButton from '../components/BaseButton';
|
||||
|
||||
export default function GalleryPage() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { assets, loading, count } = useAppSelector((state) => state.assets);
|
||||
const { product_categories } = useAppSelector((state) => state.product_categories);
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const perPage = 12;
|
||||
|
||||
const [filters, setFilters] = useState({
|
||||
asset_type: '',
|
||||
asset_category: '',
|
||||
product_category: '',
|
||||
});
|
||||
|
||||
const assetTypes = [
|
||||
{ id: 'image', name: 'Image' },
|
||||
{ id: 'video', name: 'Video' },
|
||||
{ id: 'artwork', name: 'Artwork' },
|
||||
];
|
||||
|
||||
const assetCategories = [
|
||||
{ id: 'ProductPhotos', name: 'Product Photos' },
|
||||
{ id: 'ApplicationScenarios', name: 'Application Scenarios' },
|
||||
{ id: 'Videos', name: 'Videos' },
|
||||
{ id: 'Packaging&Artwork', name: 'Packaging & Artwork' },
|
||||
{ id: 'Logos&BrandAssets', name: 'Logos & Brand Assets' },
|
||||
];
|
||||
|
||||
const loadData = () => {
|
||||
let query = `?page=${currentPage}&limit=${perPage}`;
|
||||
if (filters.asset_type) query += `&asset_type=${filters.asset_type}`;
|
||||
if (filters.asset_category) query += `&asset_category=${filters.asset_category}`;
|
||||
if (filters.product_category) query += `&product_categories=${filters.product_category}`;
|
||||
|
||||
dispatch(fetch({ limit: perPage, page: currentPage, query }));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCategories({ limit: 100, page: 0 }));
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [currentPage, filters]);
|
||||
|
||||
const handleFilterChange = (name: string, value: string) => {
|
||||
setFilters(prev => ({ ...prev, [name]: value }));
|
||||
setCurrentPage(0);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setFilters({ asset_type: '', asset_category: '', product_category: '' });
|
||||
setCurrentPage(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50 min-h-screen">
|
||||
<Head>
|
||||
<title>{getPageTitle('Asset Library')}</title>
|
||||
</Head>
|
||||
|
||||
{/* Header / Hero */}
|
||||
<div className="bg-[#003366] text-white py-12 px-6">
|
||||
<SectionMain>
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold mb-2">WELLMAX Digital Asset Library</h1>
|
||||
<p className="text-blue-100">Browse and download official brand materials.</p>
|
||||
</div>
|
||||
</div>
|
||||
</SectionMain>
|
||||
</div>
|
||||
|
||||
<SectionMain>
|
||||
<div className="flex flex-col lg:flex-row gap-8">
|
||||
{/* Filters Sidebar */}
|
||||
<div className="w-full lg:w-64 flex-shrink-0">
|
||||
<CardBox className="sticky top-4">
|
||||
<div className="flex items-center mb-6 text-[#003366]">
|
||||
<span className="font-bold uppercase tracking-widest text-sm flex items-center">
|
||||
<mdiFilterOutline className="w-4 h-4 mr-2" />
|
||||
Filters
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<FormField label="Asset Type">
|
||||
<select
|
||||
className="w-full bg-gray-50 border border-gray-200 rounded p-2 text-sm"
|
||||
value={filters.asset_type}
|
||||
onChange={(e) => handleFilterChange('asset_type', e.target.value)}
|
||||
>
|
||||
<option value="">All Types</option>
|
||||
{assetTypes.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Asset Category">
|
||||
<select
|
||||
className="w-full bg-gray-50 border border-gray-200 rounded p-2 text-sm"
|
||||
value={filters.asset_category}
|
||||
onChange={(e) => handleFilterChange('asset_category', e.target.value)}
|
||||
>
|
||||
<option value="">All Categories</option>
|
||||
{assetCategories.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Product Category">
|
||||
<select
|
||||
className="w-full bg-gray-50 border border-gray-200 rounded p-2 text-sm"
|
||||
value={filters.product_category}
|
||||
onChange={(e) => handleFilterChange('product_category', e.target.value)}
|
||||
>
|
||||
<option value="">All Products</option>
|
||||
{product_categories?.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
|
||||
<BaseButton
|
||||
label="Reset Filters"
|
||||
color="info"
|
||||
outline
|
||||
className="w-full mt-4"
|
||||
small
|
||||
onClick={handleReset}
|
||||
/>
|
||||
</div>
|
||||
</CardBox>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-grow">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl font-bold text-gray-800">
|
||||
{count} Assets Found
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<GalleryGrid
|
||||
assets={assets}
|
||||
loading={loading}
|
||||
currentPage={currentPage}
|
||||
numPages={Math.ceil(count / perPage)}
|
||||
onPageChange={(page) => setCurrentPage(page)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SectionMain>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
GalleryPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
@ -1,166 +1,121 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import BaseButton from '../components/BaseButton';
|
||||
import CardBox from '../components/CardBox';
|
||||
import SectionFullScreen from '../components/SectionFullScreen';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
import BaseDivider from '../components/BaseDivider';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
import { getPageTitle } from '../config';
|
||||
import { useAppSelector } from '../stores/hooks';
|
||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
||||
import SectionMain from '../components/SectionMain';
|
||||
import { mdiLibraryOutline, mdiShieldCheckOutline, mdiCloudDownloadOutline } from '@mdi/js';
|
||||
import IconRounded from '../components/IconRounded';
|
||||
|
||||
|
||||
export default function Starter() {
|
||||
const [illustrationImage, setIllustrationImage] = useState({
|
||||
src: undefined,
|
||||
photographer: undefined,
|
||||
photographer_url: undefined,
|
||||
})
|
||||
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
|
||||
const [contentType, setContentType] = useState('video');
|
||||
const [contentPosition, setContentPosition] = useState('left');
|
||||
export default function WELLMAXLanding() {
|
||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
|
||||
const title = 'App Draft'
|
||||
const title = 'WELLMAX Digital Asset Library'
|
||||
|
||||
// Fetch Pexels image/video
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const image = await getPexelsImage();
|
||||
const video = await getPexelsVideo();
|
||||
setIllustrationImage(image);
|
||||
setIllustrationVideo(video);
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
return (
|
||||
<div className="bg-white min-h-screen font-sans">
|
||||
<Head>
|
||||
<title>{getPageTitle('Welcome to WELLMAX')}</title>
|
||||
</Head>
|
||||
|
||||
const imageBlock = (image) => (
|
||||
<div
|
||||
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
||||
style={{
|
||||
backgroundImage: `${
|
||||
image
|
||||
? `url(${image?.src?.original})`
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||
}`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'left center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}}
|
||||
>
|
||||
<div className='flex justify-center w-full bg-blue-300/20'>
|
||||
<a
|
||||
className='text-[8px]'
|
||||
href={image?.photographer_url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Photo by {image?.photographer} on Pexels
|
||||
</a>
|
||||
{/* Hero Section */}
|
||||
<div className="bg-[#003366] text-white py-24 px-6 md:px-12 flex flex-col items-center text-center">
|
||||
<h1 className="text-4xl md:text-6xl font-bold mb-6 tracking-tight">
|
||||
WELLMAX <span className="text-blue-300">Asset Library</span>
|
||||
</h1>
|
||||
<p className="text-xl md:text-2xl max-w-3xl mb-10 text-gray-200">
|
||||
The secure hub for all official brand assets, product photos, and marketing materials.
|
||||
Exclusively for WELLMAX GDP Partners.
|
||||
</p>
|
||||
<div className="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4">
|
||||
<BaseButton
|
||||
href="/gallery"
|
||||
label="Browse Assets"
|
||||
color="white"
|
||||
className="px-8 py-3 text-lg font-semibold"
|
||||
/>
|
||||
{!currentUser && (
|
||||
<BaseButton
|
||||
href="/login"
|
||||
label="Partner Login"
|
||||
color="info"
|
||||
outline
|
||||
className="px-8 py-3 text-lg font-semibold"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features Section */}
|
||||
<SectionMain>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-12 py-12">
|
||||
<div className="flex flex-col items-center text-center p-6">
|
||||
<IconRounded icon={mdiLibraryOutline} color="info" className="mb-4" />
|
||||
<h3 className="text-2xl font-bold mb-2">Centralized Library</h3>
|
||||
<p className="text-gray-600">
|
||||
Access all Product Photos, Application Scenarios, and Branding Assets in one place.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-center text-center p-6">
|
||||
<IconRounded icon={mdiShieldCheckOutline} color="success" className="mb-4" />
|
||||
<h3 className="text-2xl font-bold mb-2">Secure Access</h3>
|
||||
<p className="text-gray-600">
|
||||
Role-based access control ensuring that original high-res files are available only to authorized partners.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-center text-center p-6">
|
||||
<IconRounded icon={mdiCloudDownloadOutline} color="warning" className="mb-4" />
|
||||
<h3 className="text-2xl font-bold mb-2">Bulk Downloads</h3>
|
||||
<p className="text-gray-600">
|
||||
GDP Partners can easily download the assets they need for marketing and sales.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SectionMain>
|
||||
|
||||
{/* Product Categories Preview (Conceptual) */}
|
||||
<div className="bg-gray-50 py-20 px-6">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<h2 className="text-3xl font-bold text-center mb-12">Our Core Categories</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{['High Bay', 'Panel', 'Tube', 'Downlight'].map((cat) => (
|
||||
<div key={cat} className="bg-white p-8 border border-gray-200 rounded-lg text-center hover:shadow-md transition-shadow">
|
||||
<span className="font-bold text-[#003366]">{cat}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-[#333333] text-white py-12 px-6">
|
||||
<div className="max-w-6xl mx-auto flex flex-col md:flex-row justify-between items-center">
|
||||
<div className="mb-6 md:mb-0 text-center md:text-left">
|
||||
<h2 className="text-2xl font-bold mb-2 text-white">WELLMAX</h2>
|
||||
<p className="text-gray-400 text-sm">Industrial Digital Asset Management System</p>
|
||||
</div>
|
||||
<div className="flex space-x-6">
|
||||
<Link href="/terms-of-use" className="text-gray-400 hover:text-white transition-colors">Terms</Link>
|
||||
<Link href="/privacy-policy" className="text-gray-400 hover:text-white transition-colors">Privacy</Link>
|
||||
{currentUser ? (
|
||||
<Link href="/dashboard" className="text-gray-400 hover:text-white transition-colors font-bold">Admin Panel</Link>
|
||||
) : (
|
||||
<Link href="/login" className="text-gray-400 hover:text-white transition-colors font-bold">Login</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 pt-8 border-t border-gray-700 text-center text-gray-500 text-xs">
|
||||
© 2026 WELLMAX. All rights reserved. Powered by Flatlogic.
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
|
||||
const videoBlock = (video) => {
|
||||
if (video?.video_files?.length > 0) {
|
||||
return (
|
||||
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
||||
<video
|
||||
className='absolute top-0 left-0 w-full h-full object-cover'
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
>
|
||||
<source src={video?.video_files[0]?.link} type='video/mp4'/>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
||||
<a
|
||||
className='text-[8px]'
|
||||
href={video?.user?.url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Video by {video.user.name} on Pexels
|
||||
</a>
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
contentPosition === 'background'
|
||||
? {
|
||||
backgroundImage: `${
|
||||
illustrationImage
|
||||
? `url(${illustrationImage.src?.original})`
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||
}`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'left center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<Head>
|
||||
<title>{getPageTitle('Starter Page')}</title>
|
||||
</Head>
|
||||
|
||||
<SectionFullScreen bg='violet'>
|
||||
<div
|
||||
className={`flex ${
|
||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
||||
} min-h-screen w-full`}
|
||||
>
|
||||
{contentType === 'image' && contentPosition !== 'background'
|
||||
? imageBlock(illustrationImage)
|
||||
: null}
|
||||
{contentType === 'video' && contentPosition !== 'background'
|
||||
? videoBlock(illustrationVideo)
|
||||
: null}
|
||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
||||
<CardBoxComponentTitle title="Welcome to your App Draft app!"/>
|
||||
|
||||
<div className="space-y-3">
|
||||
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
||||
<p className='text-center text-gray-500'>For guides and documentation please check
|
||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
||||
</div>
|
||||
|
||||
<BaseButtons>
|
||||
<BaseButton
|
||||
href='/login'
|
||||
label='Login'
|
||||
color='info'
|
||||
className='w-full'
|
||||
/>
|
||||
|
||||
</BaseButtons>
|
||||
</CardBox>
|
||||
</div>
|
||||
</div>
|
||||
</SectionFullScreen>
|
||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
WELLMAXLanding.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
|
||||
|
||||
@ -264,7 +264,9 @@ const EditUsersPage = () => {
|
||||
|
||||
|
||||
|
||||
password: ''
|
||||
password: '',
|
||||
|
||||
gdpLevel: 1,
|
||||
|
||||
}
|
||||
const [initialValues, setInitialValues] = useState(initVals)
|
||||
@ -590,6 +592,15 @@ const EditUsersPage = () => {
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
<FormField label="GDP Level" labelFor="gdpLevel">
|
||||
<Field name="gdpLevel" id="gdpLevel" component="select">
|
||||
<option value={1}>Blue</option>
|
||||
<option value={2}>Silver</option>
|
||||
<option value={3}>Gold</option>
|
||||
<option value={4}>Platinum</option>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -156,6 +156,8 @@ const initialValues = {
|
||||
|
||||
custom_permissions: [],
|
||||
|
||||
gdpLevel: 1,
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -431,6 +433,15 @@ const UsersNew = () => {
|
||||
<Field name="app_role" id="app_role" component={SelectField} options={[]} itemRef={'roles'}></Field>
|
||||
</FormField>
|
||||
|
||||
<FormField label="GDP Level" labelFor="gdpLevel">
|
||||
<Field name="gdpLevel" id="gdpLevel" component="select">
|
||||
<option value={1}>Blue</option>
|
||||
<option value={2}>Silver</option>
|
||||
<option value={3}>Gold</option>
|
||||
<option value={4}>Platinum</option>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user