diff --git a/backend/src/auth/auth.js b/backend/src/auth/auth.js index 251c149..6e2175b 100644 --- a/backend/src/auth/auth.js +++ b/backend/src/auth/auth.js @@ -55,14 +55,46 @@ passport.use(new MicrosoftStrategy({ } )); -function socialStrategy(email, profile, provider, done) { - db.users.findOrCreate({where: {email, provider}}).then(([user, created]) => { - const body = { - id: user.id, - email: user.email, - name: profile.displayName, - }; - const token = helpers.jwtSign({user: body}); - return done(null, {token}); - }); -} +async function socialStrategy(email, profile, provider, done) { + try { + let user = await db.users.findOne({where: {email, provider}}); + if (!user) { + // If user exists with same email but different provider, we might want to link or reject + // For now, let's just find by email if provider is not set or handle accordingly + user = await db.users.findOne({where: {email}}); + if (user) { + // Update provider if not set + if (!user.provider) { + await user.update({provider}); + } + } else { + // Create new user + user = await db.users.create({ + email, + provider, + firstName: profile.given_name || profile.displayName?.split(' ')[0], + lastName: profile.family_name || profile.displayName?.split(' ')[1], + emailVerified: true + }); + + // Set default role + const role = await db.roles.findOne({ + where: { name: config.roles?.user || 'Viewer' }, + }); + if (role) { + await user.setApp_role(role); + } + } + } + + const body = { + id: user.id, + email: user.email, + name: profile.displayName, + }; + const token = helpers.jwtSign({user: body}); + return done(null, {token}); + } catch (error) { + return done(error); + } +} \ No newline at end of file diff --git a/backend/src/config.js b/backend/src/config.js index 36ebd2b..fb32f66 100644 --- a/backend/src/config.js +++ b/backend/src/config.js @@ -1,6 +1,3 @@ - - - const os = require('os'); const config = { @@ -73,7 +70,7 @@ config.pexelsQuery = 'paper airplane over sunrise horizon'; config.host = process.env.NODE_ENV === "production" ? config.remote : "http://localhost"; config.apiUrl = `${config.host}${config.port ? `:${config.port}` : ``}/api`; config.swaggerUrl = `${config.swaggerUI}${config.swaggerPort}`; -config.uiUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}/#`; +config.uiUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}`; config.backUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}`; -module.exports = config; +module.exports = config; \ No newline at end of file diff --git a/backend/src/db/db.config.js b/backend/src/db/db.config.js index 5a2f718..7e4ca1c 100644 --- a/backend/src/db/db.config.js +++ b/backend/src/db/db.config.js @@ -1,5 +1,3 @@ - - module.exports = { production: { dialect: 'postgres', @@ -12,11 +10,12 @@ module.exports = { seederStorage: 'sequelize', }, development: { - username: 'postgres', dialect: 'postgres', - password: '', - database: 'db_app_draft', - host: process.env.DB_HOST || 'localhost', + username: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_NAME, + host: process.env.DB_HOST, + port: process.env.DB_PORT, logging: console.log, seederStorage: 'sequelize', }, @@ -30,4 +29,4 @@ module.exports = { logging: console.log, seederStorage: 'sequelize', } -}; +}; \ No newline at end of file diff --git a/backend/src/db/migrations/1770063478989.js b/backend/src/db/migrations/1770063478989.js new file mode 100644 index 0000000..7d4c583 --- /dev/null +++ b/backend/src/db/migrations/1770063478989.js @@ -0,0 +1,103 @@ +module.exports = { + async up(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.createTable('services', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + title: { type: Sequelize.DataTypes.TEXT }, + description: { type: Sequelize.DataTypes.TEXT }, + price: { type: Sequelize.DataTypes.DECIMAL }, + category: { type: Sequelize.DataTypes.TEXT }, + image: { type: Sequelize.DataTypes.TEXT }, + createdByUserId: { + type: Sequelize.DataTypes.UUID, + references: { model: 'users', key: 'id' }, + }, + updatedByUserId: { + type: Sequelize.DataTypes.UUID, + references: { model: 'users', key: 'id' }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { type: Sequelize.DataTypes.STRING(255), allowNull: true, unique: true }, + }, { transaction }); + + await queryInterface.createTable('courses', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + title: { type: Sequelize.DataTypes.TEXT }, + description: { type: Sequelize.DataTypes.TEXT }, + price: { type: Sequelize.DataTypes.DECIMAL }, + level: { type: Sequelize.DataTypes.TEXT }, + duration: { type: Sequelize.DataTypes.TEXT }, + image: { type: Sequelize.DataTypes.TEXT }, + createdByUserId: { + type: Sequelize.DataTypes.UUID, + references: { model: 'users', key: 'id' }, + }, + updatedByUserId: { + type: Sequelize.DataTypes.UUID, + references: { model: 'users', key: 'id' }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { type: Sequelize.DataTypes.STRING(255), allowNull: true, unique: true }, + }, { transaction }); + + await queryInterface.createTable('orders', { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + customerName: { type: Sequelize.DataTypes.TEXT }, + customerEmail: { type: Sequelize.DataTypes.TEXT }, + status: { + type: Sequelize.DataTypes.ENUM, + values: ['pending', 'completed', 'cancelled'], + }, + totalAmount: { type: Sequelize.DataTypes.DECIMAL }, + itemType: { type: Sequelize.DataTypes.TEXT }, + itemId: { type: Sequelize.DataTypes.UUID }, + createdByUserId: { + type: Sequelize.DataTypes.UUID, + references: { model: 'users', key: 'id' }, + }, + updatedByUserId: { + type: Sequelize.DataTypes.UUID, + references: { model: 'users', key: 'id' }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { type: Sequelize.DataTypes.STRING(255), allowNull: true, unique: true }, + }, { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + async down(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.dropTable('orders', { transaction }); + await queryInterface.dropTable('courses', { transaction }); + await queryInterface.dropTable('services', { transaction }); + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + } +}; diff --git a/backend/src/db/models/courses.js b/backend/src/db/models/courses.js new file mode 100644 index 0000000..4313c1c --- /dev/null +++ b/backend/src/db/models/courses.js @@ -0,0 +1,51 @@ +module.exports = function(sequelize, DataTypes) { + const courses = sequelize.define( + 'courses', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + title: { + type: DataTypes.TEXT, + }, + description: { + type: DataTypes.TEXT, + }, + price: { + type: DataTypes.DECIMAL, + }, + level: { + type: DataTypes.TEXT, + }, + duration: { + type: DataTypes.TEXT, + }, + image: { + type: DataTypes.TEXT, + }, + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + courses.associate = (db) => { + db.courses.belongsTo(db.users, { + as: 'createdBy', + }); + db.courses.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return courses; +}; diff --git a/backend/src/db/models/orders.js b/backend/src/db/models/orders.js new file mode 100644 index 0000000..f3649fd --- /dev/null +++ b/backend/src/db/models/orders.js @@ -0,0 +1,56 @@ +module.exports = function(sequelize, DataTypes) { + const orders = sequelize.define( + 'orders', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + customerName: { + type: DataTypes.TEXT, + }, + customerEmail: { + type: DataTypes.TEXT, + }, + message: { + type: DataTypes.TEXT, + }, + status: { + type: DataTypes.ENUM, + values: ['pending', 'completed', 'cancelled'], + defaultValue: 'pending', + }, + totalAmount: { + type: DataTypes.DECIMAL, + }, + itemType: { + type: DataTypes.TEXT, // 'service' or 'course' + }, + itemId: { + type: DataTypes.UUID, + }, + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + orders.associate = (db) => { + db.orders.belongsTo(db.users, { + as: 'createdBy', + }); + db.orders.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return orders; +}; \ No newline at end of file diff --git a/backend/src/db/models/services.js b/backend/src/db/models/services.js new file mode 100644 index 0000000..54a1064 --- /dev/null +++ b/backend/src/db/models/services.js @@ -0,0 +1,48 @@ +module.exports = function(sequelize, DataTypes) { + const services = sequelize.define( + 'services', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + title: { + type: DataTypes.TEXT, + }, + description: { + type: DataTypes.TEXT, + }, + price: { + type: DataTypes.DECIMAL, + }, + category: { + type: DataTypes.TEXT, + }, + image: { + type: DataTypes.TEXT, + }, + importHash: { + type: DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { + timestamps: true, + paranoid: true, + freezeTableName: true, + }, + ); + + services.associate = (db) => { + db.services.belongsTo(db.users, { + as: 'createdBy', + }); + db.services.belongsTo(db.users, { + as: 'updatedBy', + }); + }; + + return services; +}; diff --git a/backend/src/db/seeders/20260202130745-services-courses.js b/backend/src/db/seeders/20260202130745-services-courses.js new file mode 100644 index 0000000..037ad70 --- /dev/null +++ b/backend/src/db/seeders/20260202130745-services-courses.js @@ -0,0 +1,69 @@ +module.exports = { + async up(queryInterface, Sequelize) { + const services = [ + { + id: '1e2e3e4e-5e6e-7e8e-9e0e-1a2b3c4d5e6f', + title: 'AI Video Ad - Professional', + description: 'High-quality 30-second video ad generated with Gen AI, including script and voiceover.', + price: 499.00, + category: 'Video Ads', + image: 'https://images.pexels.com/photos/373543/pexels-photo-373543.jpeg', + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: '2e2e3e4e-5e6e-7e8e-9e0e-1a2b3c4d5e6f', + title: 'Social Media Pack', + description: 'Set of 5 short video ads optimized for TikTok, Reels, and YouTube Shorts.', + price: 799.00, + category: 'Video Ads', + image: 'https://images.pexels.com/photos/5053740/pexels-photo-5053740.jpeg', + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: '3e2e3e4e-5e6e-7e8e-9e0e-1a2b3c4d5e6f', + title: 'Custom AI Avatar Video', + description: 'Personalized AI avatar speaking your script. Ideal for corporate training or welcome videos.', + price: 299.00, + category: 'Video Ads', + image: 'https://images.pexels.com/photos/6146978/pexels-photo-6146978.jpeg', + createdAt: new Date(), + updatedAt: new Date(), + }, + ]; + + const courses = [ + { + id: '4e2e3e4e-5e6e-7e8e-9e0e-1a2b3c4d5e6f', + title: 'Introduction to Generative AI', + description: 'Learn the fundamentals of LLMs, Image Generation, and how AI is changing industries.', + price: 99.00, + level: 'Beginner', + duration: '4 weeks', + image: 'https://images.pexels.com/photos/8386440/pexels-photo-8386440.jpeg', + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: '5e2e3e4e-5e6e-7e8e-9e0e-1a2b3c4d5e6f', + title: 'Mastering Stable Diffusion', + description: 'Deep dive into prompt engineering, LoRA training, and advanced image generation techniques.', + price: 199.00, + level: 'Intermediate', + duration: '6 weeks', + image: 'https://images.pexels.com/photos/17483874/pexels-photo-17483874.jpeg', + createdAt: new Date(), + updatedAt: new Date(), + }, + ]; + + await queryInterface.bulkInsert('services', services); + await queryInterface.bulkInsert('courses', courses); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.bulkDelete('services', null, {}); + await queryInterface.bulkDelete('courses', null, {}); + } +}; diff --git a/backend/src/index.js b/backend/src/index.js index 94442b7..df59eea 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -1,4 +1,3 @@ - const express = require('express'); const cors = require('cors'); const app = express(); @@ -19,6 +18,9 @@ const pexelsRoutes = require('./routes/pexels'); const openaiRoutes = require('./routes/openai'); +const servicesRoutes = require('./routes/services'); +const coursesRoutes = require('./routes/courses'); +const ordersRoutes = require('./routes/orders'); const usersRoutes = require('./routes/users'); @@ -94,6 +96,10 @@ app.use('/api/file', fileRoutes); app.use('/api/pexels', pexelsRoutes); app.enable('trust proxy'); +app.use('/api/services', servicesRoutes); +app.use('/api/courses', coursesRoutes); +app.use('/api/orders', ordersRoutes); + app.use('/api/users', passport.authenticate('jwt', {session: false}), usersRoutes); @@ -155,4 +161,4 @@ db.sequelize.sync().then(function () { }); }); -module.exports = app; +module.exports = app; \ No newline at end of file diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index d6f29e8..75b9a79 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -201,7 +201,7 @@ router.get('/signin/microsoft/callback', passport.authenticate("microsoft", { router.use('/', require('../helpers').commonErrorHandler); function socialRedirect(res, state, token, config) { - res.redirect(config.uiUrl + "/login?token=" + token); + res.redirect("/login?token=" + token); } -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/backend/src/routes/courses.js b/backend/src/routes/courses.js new file mode 100644 index 0000000..23388b6 --- /dev/null +++ b/backend/src/routes/courses.js @@ -0,0 +1,16 @@ +const express = require('express'); +const db = require('../db/models'); +const wrapAsync = require('../helpers').wrapAsync; +const router = express.Router(); + +router.get('/', wrapAsync(async (req, res) => { + const courses = await db.courses.findAll(); + res.status(200).send({ rows: courses, count: courses.length }); +})); + +router.get('/:id', wrapAsync(async (req, res) => { + const course = await db.courses.findByPk(req.params.id); + res.status(200).send(course); +})); + +module.exports = router; \ No newline at end of file diff --git a/backend/src/routes/orders.js b/backend/src/routes/orders.js new file mode 100644 index 0000000..a0cf596 --- /dev/null +++ b/backend/src/routes/orders.js @@ -0,0 +1,11 @@ +const express = require('express'); +const db = require('../db/models'); +const wrapAsync = require('../helpers').wrapAsync; +const router = express.Router(); + +router.post('/', wrapAsync(async (req, res) => { + const order = await db.orders.create(req.body.data); + res.status(200).send(order); +})); + +module.exports = router; \ No newline at end of file diff --git a/backend/src/routes/services.js b/backend/src/routes/services.js new file mode 100644 index 0000000..173889d --- /dev/null +++ b/backend/src/routes/services.js @@ -0,0 +1,16 @@ +const express = require('express'); +const db = require('../db/models'); +const wrapAsync = require('../helpers').wrapAsync; +const router = express.Router(); + +router.get('/', wrapAsync(async (req, res) => { + const services = await db.services.findAll(); + res.status(200).send({ rows: services, count: services.length }); +})); + +router.get('/:id', wrapAsync(async (req, res) => { + const service = await db.services.findByPk(req.params.id); + res.status(200).send(service); +})); + +module.exports = router; \ No newline at end of file diff --git a/frontend/src/components/Dashboard/DashboardCharts.tsx b/frontend/src/components/Dashboard/DashboardCharts.tsx new file mode 100644 index 0000000..1604b9a --- /dev/null +++ b/frontend/src/components/Dashboard/DashboardCharts.tsx @@ -0,0 +1,147 @@ +import React, { useEffect, useState } from 'react'; +import axios from 'axios'; +import CardBox from '../CardBox'; +import { Pie, Bar } from 'react-chartjs-2'; +import { + Chart as ChartJS, + ArcElement, + Tooltip, + Legend, + CategoryScale, + LinearScale, + BarElement, + Title, +} from 'chart.js'; + +ChartJS.register( + ArcElement, + Tooltip, + Legend, + CategoryScale, + LinearScale, + BarElement, + Title +); + +const DashboardCharts = () => { + const [statusData, setStatusData] = useState(null); + const [priorityData, setPriorityData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + try { + const [statusRes, priorityRes] = await Promise.all([ + axios.post('/sql', { + sql: 'SELECT status, COUNT(*) as count FROM tasks WHERE "deletedAt" IS NULL GROUP BY status', + }), + axios.post('/sql', { + sql: 'SELECT priority, COUNT(*) as count FROM tasks WHERE "deletedAt" IS NULL GROUP BY priority', + }), + ]); + + const statuses = statusRes.data.rows; + const priorities = priorityRes.data.rows; + + setStatusData({ + labels: statuses.map((s: any) => s.status || 'No Status'), + datasets: [ + { + label: 'Tasks by Status', + data: statuses.map((s: any) => parseInt(s.count)), + backgroundColor: [ + '#6366f1', // todo + '#f59e0b', // in_progress + '#ec4899', // review + '#10b981', // done + '#ef4444', // blocked + ], + borderWidth: 1, + }, + ], + }); + + setPriorityData({ + labels: priorities.map((p: any) => p.priority || 'No Priority'), + datasets: [ + { + label: 'Tasks by Priority', + data: priorities.map((p: any) => parseInt(p.count)), + backgroundColor: '#6366f1', + borderRadius: 8, + }, + ], + }); + } catch (error) { + console.error('Failed to fetch chart data:', error); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, []); + + if (loading) { + return ( +
+ + +
+ ); + } + + return ( +
+ +

Task Status Distribution

+
+ {statusData && ( + + )} +
+
+ + +

Tasks by Priority

+
+ {priorityData && ( + + )} +
+
+
+ ); +}; + +export default DashboardCharts; diff --git a/frontend/src/components/Dashboard/DashboardStats.tsx b/frontend/src/components/Dashboard/DashboardStats.tsx new file mode 100644 index 0000000..83d5db5 --- /dev/null +++ b/frontend/src/components/Dashboard/DashboardStats.tsx @@ -0,0 +1,107 @@ +import React, { useEffect, useState } from 'react'; +import * as icon from '@mdi/js'; +import axios from 'axios'; +import BaseIcon from '../BaseIcon'; +import CardBox from '../CardBox'; +import { useAppSelector } from '../../stores/hooks'; + +const DashboardStats = () => { + const [stats, setStats] = useState({ + projects_count: 0, + tasks_count: 0, + completed_tasks_count: 0, + active_tasks_count: 0, + users_count: 0, + }); + const [loading, setLoading] = useState(true); + + const iconsColor = useAppSelector((state) => state.style.iconsColor); + + useEffect(() => { + const fetchStats = async () => { + try { + const sql = ` + SELECT + (SELECT COUNT(*) FROM projects WHERE "deletedAt" IS NULL) as projects_count, + (SELECT COUNT(*) FROM tasks WHERE "deletedAt" IS NULL) as tasks_count, + (SELECT COUNT(*) FROM tasks WHERE status = 'done' AND "deletedAt" IS NULL) as completed_tasks_count, + (SELECT COUNT(*) FROM tasks WHERE status IN ('todo', 'in_progress') AND "deletedAt" IS NULL) as active_tasks_count, + (SELECT COUNT(*) FROM users WHERE "deletedAt" IS NULL) as users_count + `; + const { data } = await axios.post('/sql', { sql }); + if (data.rows && data.rows.length > 0) { + setStats(data.rows[0]); + } + } catch (error) { + console.error('Failed to fetch dashboard stats:', error); + } finally { + setLoading(false); + } + }; + + fetchStats(); + }, []); + + const statItems = [ + { + label: 'Projects', + value: stats.projects_count, + icon: icon.mdiFolder, + color: 'text-blue-500', + }, + { + label: 'Total Tasks', + value: stats.tasks_count, + icon: icon.mdiFormatListCheckbox, + color: 'text-indigo-500', + }, + { + label: 'Active Tasks', + value: stats.active_tasks_count, + icon: icon.mdiProgressClock, + color: 'text-orange-500', + }, + { + label: 'Completed', + value: stats.completed_tasks_count, + icon: icon.mdiCheckCircle, + color: 'text-emerald-500', + }, + ]; + + if (loading) { + return ( +
+ {[1, 2, 3, 4].map((i) => ( + +
+
+ ))} +
+ ); + } + + return ( +
+ {statItems.map((item, index) => ( + +
+
+

+ {item.label} +

+

+ {item.value} +

+
+
+ +
+
+
+ ))} +
+ ); +}; + +export default DashboardStats; diff --git a/frontend/src/components/Dashboard/RecentTasks.tsx b/frontend/src/components/Dashboard/RecentTasks.tsx new file mode 100644 index 0000000..5846b88 --- /dev/null +++ b/frontend/src/components/Dashboard/RecentTasks.tsx @@ -0,0 +1,114 @@ +import React, { useEffect, useState } from 'react'; +import axios from 'axios'; +import CardBox from '../CardBox'; +import Link from 'next/link'; +import BaseIcon from '../BaseIcon'; +import * as icon from '@mdi/js'; + +const RecentTasks = () => { + const [tasks, setTasks] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchRecentTasks = async () => { + try { + const sql = ` + SELECT t.id, t.title, t.status, t.priority, p.title as project_title + FROM tasks t + LEFT JOIN projects p ON t."projectId" = p.id + WHERE t."deletedAt" IS NULL + ORDER BY t."createdAt" DESC + LIMIT 5 + `; + const { data } = await axios.post('/sql', { sql }); + setTasks(data.rows || []); + } catch (error) { + console.error('Failed to fetch recent tasks:', error); + } finally { + setLoading(false); + } + }; + + fetchRecentTasks(); + }, []); + + const getStatusColor = (status: string) => { + switch (status) { + case 'done': + return 'bg-emerald-100 text-emerald-700'; + case 'in_progress': + return 'bg-amber-100 text-amber-700'; + case 'blocked': + return 'bg-rose-100 text-rose-700'; + case 'todo': + return 'bg-blue-100 text-blue-700'; + default: + return 'bg-gray-100 text-gray-700'; + } + }; + + if (loading) { + return ( + +
+
+ ); + } + + return ( + +
+

Recent Tasks

+ + View all tasks + +
+ +
+ + + + + + + + + + + {tasks.length > 0 ? ( + tasks.map((task) => ( + + + + + + + )) + ) : ( + + + + )} + +
TaskProjectStatusPriority
+ {task.title} + + {task.project_title || 'No Project'} + + + {task.status?.replace('_', ' ')} + + +
+ + {task.priority} +
+
+ No tasks found. Start by creating a project and adding tasks! +
+
+
+ ); +}; + +export default RecentTasks; diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index 380db19..41e259f 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -15,6 +15,10 @@ import { fetchWidgets } from '../stores/roles/rolesSlice'; import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator'; import { SmartWidget } from '../components/SmartWidget/SmartWidget'; +import DashboardStats from '../components/Dashboard/DashboardStats'; +import DashboardCharts from '../components/Dashboard/DashboardCharts'; +import RecentTasks from '../components/Dashboard/RecentTasks'; + import { useAppDispatch, useAppSelector } from '../stores/hooks'; const Dashboard = () => { const dispatch = useAppDispatch(); @@ -94,11 +98,23 @@ const Dashboard = () => { {''} + + {/* Professional Dashboard Sections */} + + + + + {''} + + {hasPermission(currentUser, 'CREATE_ROLES') && { {!!rolesWidgets.length &&
} + + {''} + +
@@ -378,4 +401,4 @@ Dashboard.getLayout = function getLayout(page: ReactElement) { return {page} } -export default Dashboard +export default Dashboard \ No newline at end of file diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 9a58af9..160225d 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,166 +1,195 @@ - import React, { useEffect, useState } 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 { mdiVideo, mdiSchool, mdiBrain, mdiArrowRight } from '@mdi/js'; +import BaseIcon from '../components/BaseIcon'; +import axios from 'axios'; +export default function LandingPage() { + const title = 'GenAI Ads & Academy'; + const [services, setServices] = useState([]); + const [courses, setCourses] = useState([]); -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'); - const textColor = useAppSelector((state) => state.style.linkColor); - - const title = 'App Draft' - - // Fetch Pexels image/video - useEffect(() => { - async function fetchData() { - const image = await getPexelsImage(); - const video = await getPexelsVideo(); - setIllustrationImage(image); - setIllustrationVideo(video); - } - fetchData(); - }, []); - - const imageBlock = (image) => ( - - ); - - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- - -
) - } - }; + useEffect(() => { + axios.get('/services').then((res) => setServices(res.data.rows || [])); + axios.get('/courses').then((res) => setCourses(res.data.rows || [])); + }, []); return ( -
+
- {getPageTitle('Starter Page')} + {getPageTitle('GenAI Services & Courses')} - -
- {contentType === 'image' && contentPosition !== 'background' - ? imageBlock(illustrationImage) - : null} - {contentType === 'video' && contentPosition !== 'background' - ? videoBlock(illustrationVideo) - : null} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

+ {/* Navigation */} +
- -
-

© 2026 {title}. All rights reserved

- - Privacy Policy - + + + {/* Hero Section */} +
+
+ + +
+
+ Next-Gen Content Creation +
+ +

+ Elevate your brand with
+ + Generative AI + +

+ +

+ We create high-converting video ads and provide professional training + to help you master the latest AI tools for business. +

+ +
+ + + + + + +
+
+
+ {/* Services Section */} +
+ +
+

Video Ad Services

+

Professional AI-generated video content that grabs attention and drives conversions.

+
+
+ {services.map((service: any) => ( +
+
+ {service.title} +
+ ${service.price} +
+
+
+

{service.title}

+

+ {service.description} +

+
+ + + +
+
+
+ ))} +
+
+
+ + {/* Courses Section */} +
+ +
+

Gen AI Academy

+

Master the future of work with our expert-led AI courses and workshops.

+
+
+ {courses.map((course: any) => ( +
+
+ {course.title} +
+
+
+ + {course.level} • {course.duration} +
+

{course.title}

+

+ {course.description} +

+
+ ${course.price} + + + +
+
+
+ ))} +
+
+
+ + {/* CTA Section */} +
+ +
+
+
+ +

Ready to Transform Your Workflow?

+

+ Whether you need high-end production or hands-on training, we have the AI expertise to help you scale. +

+
+
+
+ + {/* Footer */} +
+
+
+ {title} + © 2026 +
+
+ Terms + Privacy + Contact +
+
+
); } -Starter.getLayout = function getLayout(page: ReactElement) { +LandingPage.getLayout = function getLayout(page: ReactElement) { return {page}; -}; - +}; \ No newline at end of file diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx index 04e2af6..90cd9df 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/pages/login.tsx @@ -1,12 +1,10 @@ - - import React, { useEffect, useState } from 'react'; import type { ReactElement } from 'react'; import Head from 'next/head'; import BaseButton from '../components/BaseButton'; import CardBox from '../components/CardBox'; import BaseIcon from "../components/BaseIcon"; -import { mdiInformation, mdiEye, mdiEyeOff } from '@mdi/js'; +import { mdiEye, mdiEyeOff, mdiGoogle } from '@mdi/js'; import SectionFullScreen from '../components/SectionFullScreen'; import LayoutGuest from '../layouts/Guest'; import { Field, Form, Formik } from 'formik'; @@ -16,7 +14,7 @@ import BaseDivider from '../components/BaseDivider'; import BaseButtons from '../components/BaseButtons'; import { useRouter } from 'next/router'; import { getPageTitle } from '../config'; -import { findMe, loginUser, resetAction } from '../stores/authSlice'; +import { findMe, loginUser, resetAction, setToken } from '../stores/authSlice'; import { useAppDispatch, useAppSelector } from '../stores/hooks'; import Link from 'next/link'; import {toast, ToastContainer} from "react-toastify"; @@ -26,7 +24,6 @@ export default function Login() { const router = useRouter(); const dispatch = useAppDispatch(); const textColor = useAppSelector((state) => state.style.linkColor); - const iconsColor = useAppSelector((state) => state.style.iconsColor); const notify = (type, msg) => toast(msg, { type }); const [ illustrationImage, setIllustrationImage ] = useState({ src: undefined, @@ -40,8 +37,8 @@ export default function Login() { const { currentUser, isFetching, errorMessage, token, notify:notifyState } = useAppSelector( (state) => state.auth, ); - const [initialValues, setInitialValues] = React.useState({ email:'admin@flatlogic.com', - password: '2f79a9f5', + const [initialValues, setInitialValues] = React.useState({ email:'', + password: '', remember: true }) const title = 'App Draft' @@ -56,6 +53,15 @@ export default function Login() { } fetchData(); }, []); + + // Handle token from social login + useEffect(() => { + if (router.query.token) { + dispatch(setToken(router.query.token)); + router.push('/dashboard'); + } + }, [router.query.token, dispatch, router]); + // Fetch user data useEffect(() => { if (token) { @@ -92,14 +98,6 @@ export default function Login() { await dispatch(loginUser(rest)); }; - const setLogin = (target: HTMLElement) => { - setInitialValues(prev => ({ - ...prev, - email : target.innerText.trim(), - password: target.dataset.password ?? '', - })); - }; - const imageBlock = (image) => (
- - -

{title}

- -
-
- -

Use{' '} - setLogin(e.target)}>admin@flatlogic.com{' / '} - 2f79a9f5{' / '} - to login as Admin

-

Use setLogin(e.target)}>client@hello.com{' / '} - 4b6d7f9aa842{' / '} - to login as User

-
-
- -
-
-
- +

{title}

+ + + + + window.location.href = '/api/auth/signin/google'} + /> + +

Don’t have an account yet?{' '} diff --git a/frontend/src/pages/order.tsx b/frontend/src/pages/order.tsx new file mode 100644 index 0000000..763efbb --- /dev/null +++ b/frontend/src/pages/order.tsx @@ -0,0 +1,169 @@ +import React, { useEffect, useState } from 'react'; +import type { ReactElement } from 'react'; +import Head from 'next/head'; +import { useRouter } from 'next/router'; +import LayoutGuest from '../layouts/Guest'; +import SectionMain from '../components/SectionMain'; +import CardBox from '../components/CardBox'; +import FormField from '../components/FormField'; +import BaseButton from '../components/BaseButton'; +import { getPageTitle } from '../config'; +import axios from 'axios'; +import { mdiCheckCircleOutline, mdiArrowLeft } from '@mdi/js'; +import BaseIcon from '../components/BaseIcon'; +import Link from 'next/link'; + +export default function OrderPage() { + const router = useRouter(); + const { type, id } = router.query; + const [item, setItem] = useState(null); + const [formData, setFormData] = useState({ + customerName: '', + customerEmail: '', + message: '', + }); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + + useEffect(() => { + if (id && type) { + const endpoint = type === 'service' ? `/services/${id}` : `/courses/${id}`; + axios.get(endpoint).then((res) => setItem(res.data)); + } + }, [id, type]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + try { + await axios.post('/orders', { + data: { + customerName: formData.customerName, + customerEmail: formData.customerEmail, + message: formData.message, + totalAmount: item?.price || 0, + itemType: type, + itemId: id, + status: 'pending', + }, + }); + setIsSuccess(true); + } catch (error) { + console.error('Order failed', error); + alert('Something went wrong. Please try again.'); + } finally { + setIsSubmitting(false); + } + }; + + if (isSuccess) { + return ( +

+ + {getPageTitle('Thank You')} + + +
+ +
+

Request Sent!

+

+ Thank you for your interest in {item?.title}. + Our team will contact you at {formData.customerEmail} shortly. +

+ + + +
+
+ ); + } + + return ( +
+ + {getPageTitle(type === 'service' ? 'Inquire' : 'Enroll')} + + +
+ + + Back to listings + + +
+
+

+ {type === 'service' ? 'Service Inquiry' : 'Course Enrollment'} +

+

+ Please fill out the form and our specialist will get back to you with the next steps and payment details. +

+ + {item && ( + +
+ +
+

{item.title}

+

{item.category || item.level}

+ ${item.price} +
+
+
+ )} +
+ + + + setFormData({ ...formData, customerName: e.target.value })} + /> + + + + setFormData({ ...formData, customerEmail: e.target.value })} + /> + + + +