Compare commits
8 Commits
001e4b6d00
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b0d86465f2 | |||
| 743187b216 | |||
| 29e6eb82c7 | |||
| dc35cfcb3f | |||
| cbbb0ed4c4 | |||
| 9113ba4c74 | |||
| 7d9350c3df | |||
| 9c84737fed |
+92
-92
@@ -1,4 +1,4 @@
|
||||
'use strict'
|
||||
"use strict";
|
||||
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
||||
/**
|
||||
* App
|
||||
@@ -8,52 +8,52 @@
|
||||
* @author Ryan Wong
|
||||
*
|
||||
*/
|
||||
require('dotenv').config()
|
||||
const express = require('express')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const logger = require('morgan')
|
||||
const helmet = require('helmet')
|
||||
const cookieParser = require('cookie-parser')
|
||||
const cors = require('cors')
|
||||
const { ApolloServer } = require('apollo-server-express')
|
||||
const { graphqlUploadExpress } = require('graphql-upload')
|
||||
const body_parser = require('body-parser')
|
||||
require("dotenv").config();
|
||||
const express = require("express");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const logger = require("morgan");
|
||||
const helmet = require("helmet");
|
||||
const cookieParser = require("cookie-parser");
|
||||
const cors = require("cors");
|
||||
const { ApolloServer } = require("apollo-server-express");
|
||||
const { graphqlUploadExpress } = require("graphql-upload");
|
||||
const body_parser = require("body-parser");
|
||||
|
||||
const db = require('./models')
|
||||
const db = require("./models");
|
||||
const typeDefs = fs.readFileSync(
|
||||
path.join(__dirname, '/types/schema.graphql'),
|
||||
'utf8'
|
||||
)
|
||||
const jwtService = require('./services/JwtService')
|
||||
const resolvers = require('./resolvers')
|
||||
const schemaDirectives = require('./directives')
|
||||
const { AuthenticationError } = require('./services/ErrorService')
|
||||
const { errorCodes } = require('./core/strings')
|
||||
const { formatGraphqlError } = require('./utils/formatError')
|
||||
path.join(__dirname, "/types/schema.graphql"),
|
||||
"utf8"
|
||||
);
|
||||
const jwtService = require("./services/JwtService");
|
||||
const resolvers = require("./resolvers");
|
||||
const schemaDirectives = require("./directives");
|
||||
const { AuthenticationError } = require("./services/ErrorService");
|
||||
const { errorCodes } = require("./core/strings");
|
||||
const { formatGraphqlError } = require("./utils/formatError");
|
||||
|
||||
const GRAPHQL_PATH = '/graphql'
|
||||
const ALLOWED_ROLE_IDS = [2]
|
||||
const GRAPHQL_PATH = "/graphql";
|
||||
const ALLOWED_ROLE_IDS = [2];
|
||||
|
||||
let app = express()
|
||||
let app = express();
|
||||
|
||||
app.use(logger('dev'))
|
||||
app.use(logger("dev"));
|
||||
|
||||
if (process.env.MODE === 'development') {
|
||||
logger.token('graphql-query', (req) => {
|
||||
const disallowedLogs = ['IntrospectionQuery']
|
||||
if (process.env.MODE === "development") {
|
||||
logger.token("graphql-query", (req) => {
|
||||
const disallowedLogs = ["IntrospectionQuery"];
|
||||
|
||||
if (req.method === 'POST' && req.originalUrl === GRAPHQL_PATH) {
|
||||
const { query, variables, operationName } = req.body
|
||||
if (req.method === "POST" && req.originalUrl === GRAPHQL_PATH) {
|
||||
const { query, variables, operationName } = req.body;
|
||||
return !disallowedLogs.includes(operationName)
|
||||
? `GRAPHQL: \nOperation Name: ${operationName} \nQuery: ${query} \nVariables: ${JSON.stringify(
|
||||
variables
|
||||
)}`
|
||||
: ''
|
||||
: "";
|
||||
}
|
||||
return ''
|
||||
})
|
||||
app.use(logger(':graphql-query'))
|
||||
return "";
|
||||
});
|
||||
app.use(logger(":graphql-query"));
|
||||
}
|
||||
|
||||
const server = new ApolloServer({
|
||||
@@ -62,97 +62,97 @@ const server = new ApolloServer({
|
||||
resolvers,
|
||||
schemaDirectives,
|
||||
context: async ({ req }) => {
|
||||
const token = req.headers.authorization
|
||||
// const token = req.headers.authorization
|
||||
|
||||
if (!token) {
|
||||
throw new AuthenticationError(
|
||||
'Invalid token',
|
||||
errorCodes.token.INVALID_TOKEN
|
||||
)
|
||||
}
|
||||
const cleanToken = token.replace('Bearer ', '')
|
||||
const verify = jwtService.verifyAccessToken(cleanToken)
|
||||
// if (!token) {
|
||||
// throw new AuthenticationError(
|
||||
// 'Invalid token',
|
||||
// errorCodes.token.INVALID_TOKEN
|
||||
// )
|
||||
// }
|
||||
// const cleanToken = token.replace('Bearer ', '')
|
||||
// const verify = jwtService.verifyAccessToken(cleanToken)
|
||||
|
||||
const roleId = verify?.role_id
|
||||
const user = verify?.user
|
||||
const credentialId = verify?.credential_id
|
||||
// const roleId = verify?.role_id
|
||||
// const user = verify?.user
|
||||
// const credentialId = verify?.credential_id
|
||||
|
||||
if (!verify || !roleId || !user || !credentialId) {
|
||||
throw new AuthenticationError(
|
||||
'Invalid token',
|
||||
errorCodes.token.INVALID_TOKEN
|
||||
)
|
||||
}
|
||||
// if (!verify || !roleId || !user || !credentialId) {
|
||||
// throw new AuthenticationError(
|
||||
// 'Invalid token',
|
||||
// errorCodes.token.INVALID_TOKEN
|
||||
// )
|
||||
// }
|
||||
|
||||
if (!ALLOWED_ROLE_IDS.includes(+roleId)) {
|
||||
throw new AuthenticationError(
|
||||
'Access Denied',
|
||||
errorCodes.account.UNAUTHORIZED
|
||||
)
|
||||
}
|
||||
// if (!ALLOWED_ROLE_IDS.includes(+roleId)) {
|
||||
// throw new AuthenticationError(
|
||||
// 'Access Denied',
|
||||
// errorCodes.account.UNAUTHORIZED
|
||||
// )
|
||||
// }
|
||||
|
||||
return {
|
||||
credentialId,
|
||||
user,
|
||||
credentialId: 1,
|
||||
user: { id: 1, role_id: 1 },
|
||||
db,
|
||||
role: {
|
||||
roleId,
|
||||
allowedRoleIds: ALLOWED_ROLE_IDS,
|
||||
roleId: 1,
|
||||
allowedRoleIds: [1, 2, 3],
|
||||
// allowedRoleIds: ALLOWED_ROLE_IDS,
|
||||
},
|
||||
}
|
||||
};
|
||||
},
|
||||
formatError: formatGraphqlError,
|
||||
})
|
||||
});
|
||||
|
||||
if (process.NODE_ENV === 'maintenance') {
|
||||
app.all('*', (req, res) => {
|
||||
res.status(503).json({ message: 'website under maintenance' })
|
||||
})
|
||||
if (process.NODE_ENV === "maintenance") {
|
||||
app.all("*", (req, res) => {
|
||||
res.status(503).json({ message: "website under maintenance" });
|
||||
});
|
||||
}
|
||||
|
||||
app.set('iocContainer', process.env)
|
||||
app.set('db', db)
|
||||
app.use(body_parser.json({ limit: '50mb' }))
|
||||
app.set("iocContainer", process.env);
|
||||
app.set("db", db);
|
||||
app.use(body_parser.json({ limit: "50mb" }));
|
||||
|
||||
app.use(express.json())
|
||||
app.use(express.json());
|
||||
app.use(
|
||||
express.urlencoded({
|
||||
extended: false,
|
||||
})
|
||||
)
|
||||
app.use(cors())
|
||||
app.set('view engine', 'eta')
|
||||
app.set('views', path.join(__dirname, '/views'))
|
||||
app.use(cookieParser())
|
||||
app.use(helmet())
|
||||
);
|
||||
app.use(cors());
|
||||
app.set("view engine", "eta");
|
||||
app.set("views", path.join(__dirname, "/views"));
|
||||
app.use(cookieParser());
|
||||
app.use(helmet());
|
||||
|
||||
app.use(express.static(path.join(__dirname, '/public')))
|
||||
app.use(express.static(path.join(__dirname, '/uploads')))
|
||||
app.use(express.static(path.join(__dirname, "/public")));
|
||||
app.use(express.static(path.join(__dirname, "/uploads")));
|
||||
app.use(express.static(path.join(__dirname)));
|
||||
|
||||
app.use(graphqlUploadExpress({ maxFileSize: 1000000000, maxFiles: 10 }))
|
||||
|
||||
server.applyMiddleware({ app, path: GRAPHQL_PATH })
|
||||
app.use(graphqlUploadExpress({ maxFileSize: 1000000000, maxFiles: 10 }));
|
||||
|
||||
server.applyMiddleware({ app, path: GRAPHQL_PATH });
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
res.locals.message = err.message
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {}
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get("env") === "development" ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500)
|
||||
res.status(err.status || 500);
|
||||
res.json({
|
||||
message: err.message,
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
app.use((_, res, next) => {
|
||||
return res
|
||||
.status(400)
|
||||
.send("<h3 style='text-align:center';>404: Page Not Found!</h3>")
|
||||
})
|
||||
.send("<h3 style='text-align:center';>404: Page Not Found!</h3>");
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
app,
|
||||
apollo: server,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
const coreModel = require("./../core/models");
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const Actor = sequelize.define(
|
||||
"actor",
|
||||
{
|
||||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||||
name: DataTypes.STRING,
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
freezeTableName: true,
|
||||
tableName: "actor",
|
||||
}
|
||||
);
|
||||
|
||||
coreModel.call(this, Actor);
|
||||
|
||||
Actor.associate = function (models) {
|
||||
Actor.belongsToMany(models.movie, {
|
||||
through: models.movie_actor,
|
||||
foreignKey: "actor_id",
|
||||
otherKey: "movie_id",
|
||||
as: "movies",
|
||||
constraints: false,
|
||||
});
|
||||
};
|
||||
|
||||
Actor.allowFields = function () {
|
||||
return ["id", "name"];
|
||||
};
|
||||
|
||||
return Actor;
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
const coreModel = require("./../core/models");
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const Director = sequelize.define(
|
||||
"director",
|
||||
{
|
||||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||||
name: DataTypes.STRING,
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
freezeTableName: true,
|
||||
tableName: "director",
|
||||
}
|
||||
);
|
||||
|
||||
coreModel.call(this, Director);
|
||||
|
||||
Director.associate = function (models) {
|
||||
Director.hasMany(models.movie, {
|
||||
foreignKey: "director_id",
|
||||
as: "movies",
|
||||
constraints: false,
|
||||
});
|
||||
};
|
||||
|
||||
Director.allowFields = function () {
|
||||
return ["id", "name"];
|
||||
};
|
||||
|
||||
return Director;
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
const coreModel = require("./../core/models");
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const Genre = sequelize.define(
|
||||
"genre",
|
||||
{
|
||||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||||
name: DataTypes.STRING,
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
freezeTableName: true,
|
||||
tableName: "genre",
|
||||
}
|
||||
);
|
||||
|
||||
coreModel.call(this, Genre);
|
||||
|
||||
Genre.associate = function (models) {
|
||||
Genre.belongsToMany(models.movie, {
|
||||
through: models.genre_movie,
|
||||
foreignKey: "genre_id",
|
||||
otherKey: "movie_id",
|
||||
as: "movies",
|
||||
constraints: false,
|
||||
});
|
||||
};
|
||||
|
||||
Genre.allowFields = function () {
|
||||
return ["id", "name"];
|
||||
};
|
||||
|
||||
return Genre;
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
const coreModel = require("./../core/models");
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const GenreMovie = sequelize.define(
|
||||
"genre_movie",
|
||||
{
|
||||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||||
movie_id: DataTypes.INTEGER,
|
||||
genre_id: DataTypes.INTEGER,
|
||||
},
|
||||
{
|
||||
timestamps: false,
|
||||
freezeTableName: true,
|
||||
tableName: "genre_movie",
|
||||
}
|
||||
);
|
||||
|
||||
coreModel.call(this, GenreMovie);
|
||||
|
||||
GenreMovie.associate = function (models) {
|
||||
GenreMovie.belongsTo(models.movie, {
|
||||
foreignKey: "movie_id",
|
||||
as: "movie",
|
||||
constraints: false,
|
||||
});
|
||||
GenreMovie.belongsTo(models.genre, {
|
||||
foreignKey: "genre_id",
|
||||
as: "genre",
|
||||
constraints: false,
|
||||
});
|
||||
};
|
||||
|
||||
GenreMovie.allowFields = function () {
|
||||
return ["id", "movie_id", "genre_id"];
|
||||
};
|
||||
|
||||
return GenreMovie;
|
||||
};
|
||||
+26
-16
@@ -1,4 +1,4 @@
|
||||
'use strict';
|
||||
"use strict";
|
||||
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
||||
/**
|
||||
* Sequelize File
|
||||
@@ -8,24 +8,28 @@
|
||||
* @author Ryan Wong
|
||||
*
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let Sequelize = require('sequelize');
|
||||
const { DataTypes } = require('sequelize');
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
let Sequelize = require("sequelize");
|
||||
const { DataTypes } = require("sequelize");
|
||||
const basename = path.basename(__filename);
|
||||
const config = {
|
||||
DB_DATABASE: 'mysql',
|
||||
DB_USERNAME: 'root',
|
||||
DB_PASSWORD: 'root',
|
||||
DB_ADAPTER: 'mysql',
|
||||
DB_NAME: 'day_1',
|
||||
DB_HOSTNAME: 'localhost',
|
||||
DB_DATABASE: "mysql",
|
||||
DB_USERNAME: "root",
|
||||
DB_PASSWORD: process.env.DB_PASSWORD || "root",
|
||||
DB_ADAPTER: "mysql",
|
||||
DB_NAME: "day_11",
|
||||
DB_HOSTNAME: "localhost",
|
||||
DB_PORT: 3306,
|
||||
};
|
||||
|
||||
let db = {};
|
||||
|
||||
let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, {
|
||||
let sequelize = new Sequelize(
|
||||
config.DB_NAME,
|
||||
config.DB_USERNAME,
|
||||
config.DB_PASSWORD,
|
||||
{
|
||||
dialect: config.DB_ADAPTER,
|
||||
username: config.DB_USERNAME,
|
||||
password: config.DB_PASSWORD,
|
||||
@@ -33,7 +37,7 @@ let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_
|
||||
host: config.DB_HOSTNAME,
|
||||
port: config.DB_PORT,
|
||||
logging: console.log,
|
||||
timezone: '-04:00',
|
||||
timezone: "-04:00",
|
||||
pool: {
|
||||
maxConnections: 1,
|
||||
minConnections: 0,
|
||||
@@ -44,13 +48,19 @@ let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_
|
||||
underscoredAll: true,
|
||||
underscored: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// sequelize.sync({ force: true });
|
||||
sequelize
|
||||
.sync()
|
||||
.then(() => console.log("Tables synced successfully!"))
|
||||
.catch((err) => console.log(err));
|
||||
|
||||
fs.readdirSync(__dirname)
|
||||
.filter((file) => {
|
||||
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
|
||||
return (
|
||||
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
|
||||
);
|
||||
})
|
||||
.forEach((file) => {
|
||||
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
const coreModel = require("./../core/models");
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const Movie = sequelize.define(
|
||||
"movie",
|
||||
{
|
||||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||||
title: DataTypes.STRING,
|
||||
director_id: DataTypes.INTEGER,
|
||||
main_genre: DataTypes.STRING,
|
||||
status: DataTypes.INTEGER,
|
||||
review: DataTypes.STRING,
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
freezeTableName: true,
|
||||
tableName: "movie",
|
||||
}
|
||||
);
|
||||
|
||||
coreModel.call(this, Movie);
|
||||
|
||||
Movie.associate = function (models) {
|
||||
Movie.belongsTo(models.director, {
|
||||
foreignKey: "director_id",
|
||||
as: "director",
|
||||
constraints: false,
|
||||
});
|
||||
Movie.hasMany(models.review, {
|
||||
foreignKey: "movie_id",
|
||||
as: "reviews",
|
||||
constraints: false,
|
||||
});
|
||||
Movie.belongsToMany(models.actor, {
|
||||
through: models.movie_actor,
|
||||
foreignKey: "movie_id",
|
||||
otherKey: "actor_id",
|
||||
as: "actors",
|
||||
constraints: false,
|
||||
});
|
||||
Movie.belongsToMany(models.genre, {
|
||||
through: models.genre_movie,
|
||||
foreignKey: "movie_id",
|
||||
otherKey: "genre_id",
|
||||
as: "genres",
|
||||
constraints: false,
|
||||
});
|
||||
};
|
||||
|
||||
Movie.allowFields = function () {
|
||||
return ["id", "title", "director_id", "main_genre", "status", "review"];
|
||||
};
|
||||
|
||||
return Movie;
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
const coreModel = require("./../core/models");
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const MovieActor = sequelize.define(
|
||||
"movie_actor",
|
||||
{
|
||||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||||
actor_id: DataTypes.INTEGER,
|
||||
movie_id: DataTypes.INTEGER,
|
||||
},
|
||||
{
|
||||
timestamps: false,
|
||||
freezeTableName: true,
|
||||
tableName: "movie_actor",
|
||||
}
|
||||
);
|
||||
|
||||
coreModel.call(this, MovieActor);
|
||||
|
||||
MovieActor.associate = function (models) {
|
||||
MovieActor.belongsTo(models.actor, {
|
||||
foreignKey: "actor_id",
|
||||
as: "actor",
|
||||
constraints: false,
|
||||
});
|
||||
MovieActor.belongsTo(models.movie, {
|
||||
foreignKey: "movie_id",
|
||||
as: "movie",
|
||||
constraints: false,
|
||||
});
|
||||
};
|
||||
|
||||
MovieActor.allowFields = function () {
|
||||
return ["id", "actor_id", "movie_id"];
|
||||
};
|
||||
|
||||
return MovieActor;
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
const coreModel = require("./../core/models");
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const Review = sequelize.define(
|
||||
"review",
|
||||
{
|
||||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||||
notes: DataTypes.STRING,
|
||||
movie_id: DataTypes.INTEGER,
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
freezeTableName: true,
|
||||
tableName: "review",
|
||||
}
|
||||
);
|
||||
|
||||
coreModel.call(this, Review);
|
||||
|
||||
Review.associate = function (models) {
|
||||
Review.belongsTo(models.movie, {
|
||||
foreignKey: "movie_id",
|
||||
as: "movie",
|
||||
constraints: false,
|
||||
});
|
||||
};
|
||||
|
||||
Review.allowFields = function () {
|
||||
return ["id", "notes", "movie_id"];
|
||||
};
|
||||
|
||||
return Review;
|
||||
};
|
||||
+4
-1
@@ -2,7 +2,10 @@
|
||||
"name": "day11",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {},
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node --watch --env-file=.env server.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Ryan Wong",
|
||||
"private": true,
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
const db = require("../../models");
|
||||
|
||||
const ActorResolvers = {
|
||||
Query: {
|
||||
async getActor(_, { id }) {
|
||||
try {
|
||||
const actor = await db.actor.findByPk(id, {
|
||||
include: [
|
||||
{ model: db.movie, as: "movies", through: { attributes: [] } },
|
||||
],
|
||||
});
|
||||
if (!actor) return { success: false, error: "Actor not found" };
|
||||
return { success: true, data: actor };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
async getAllActors() {
|
||||
try {
|
||||
const actors = await db.actor.findAll({
|
||||
include: [
|
||||
{ model: db.movie, as: "movies", through: { attributes: [] } },
|
||||
],
|
||||
});
|
||||
return { success: true, data: actors };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
async createActor(_, args) {
|
||||
try {
|
||||
const actor = await db.actor.create(args);
|
||||
return { success: true, data: actor };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
async updateActor(_, { id, ...args }) {
|
||||
try {
|
||||
const actor = await db.actor.findByPk(id);
|
||||
if (!actor) return { success: false, error: "Actor not found" };
|
||||
await actor.update(args);
|
||||
return { success: true, data: actor };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
async deleteActor(_, { id }) {
|
||||
try {
|
||||
const actor = await db.actor.findByPk(id);
|
||||
if (!actor) return { success: false, error: "Actor not found" };
|
||||
await actor.destroy();
|
||||
return { success: true, data: actor };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = ActorResolvers;
|
||||
@@ -0,0 +1,59 @@
|
||||
const db = require("../../models");
|
||||
|
||||
const DirectorResolvers = {
|
||||
Query: {
|
||||
async getDirector(_, { id }) {
|
||||
try {
|
||||
const director = await db.director.findByPk(id, {
|
||||
include: [{ model: db.movie, as: "movies" }],
|
||||
});
|
||||
if (!director) return { success: false, error: "Director not found" };
|
||||
return { success: true, data: director };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
async getAllDirectors() {
|
||||
try {
|
||||
const directors = await db.director.findAll({
|
||||
include: [{ model: db.movie, as: "movies" }],
|
||||
});
|
||||
return { success: true, data: directors };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
async createDirector(_, args) {
|
||||
try {
|
||||
const director = await db.director.create(args);
|
||||
return { success: true, data: director };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
async updateDirector(_, { id, ...args }) {
|
||||
try {
|
||||
const director = await db.director.findByPk(id);
|
||||
if (!director) return { success: false, error: "Director not found" };
|
||||
await director.update(args);
|
||||
return { success: true, data: director };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
async deleteDirector(_, { id }) {
|
||||
try {
|
||||
const director = await db.director.findByPk(id);
|
||||
if (!director) return { success: false, error: "Director not found" };
|
||||
await director.destroy();
|
||||
return { success: true, data: director };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = DirectorResolvers;
|
||||
@@ -0,0 +1,121 @@
|
||||
const { fn, col } = require("sequelize");
|
||||
const db = require("../../models");
|
||||
|
||||
const MovieResolvers = {
|
||||
Query: {
|
||||
async getMovie(_, { id }) {
|
||||
try {
|
||||
const movie = await db.movie.findByPk(id, {
|
||||
include: [
|
||||
{ model: db.director, as: "director" },
|
||||
{ model: db.review, as: "reviews" },
|
||||
{ model: db.actor, as: "actors", through: { attributes: [] } },
|
||||
{ model: db.genre, as: "genres", through: { attributes: [] } },
|
||||
],
|
||||
});
|
||||
if (!movie) return { success: false, error: "Movie not found" };
|
||||
return { success: true, data: movie };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
async getAllMovies() {
|
||||
try {
|
||||
const movies = await db.movie.findAll({
|
||||
include: [
|
||||
{ model: db.director, as: "director" },
|
||||
{ model: db.review, as: "reviews" },
|
||||
{ model: db.actor, as: "actors", through: { attributes: [] } },
|
||||
{ model: db.genre, as: "genres", through: { attributes: [] } },
|
||||
],
|
||||
});
|
||||
return { success: true, data: movies };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
async getMoviesWithReviewCount(_, { minReviews }) {
|
||||
try {
|
||||
const movies = await db.movie.findAll({
|
||||
// attributes: {
|
||||
// include: [[fn("COUNT", col("reviews.id")), "reviewCount"]],
|
||||
// },
|
||||
include: [
|
||||
{ model: db.review, as: "reviews" },
|
||||
{ model: db.director, as: "director" },
|
||||
{ model: db.actor, as: "actors", through: { attributes: [] } },
|
||||
{ model: db.genre, as: "genres", through: { attributes: [] } },
|
||||
],
|
||||
// having: literal(`COUNT(reviews.id) > ${minReviews}`),
|
||||
});
|
||||
const filtered = movies.filter(
|
||||
(m) => (m.reviews ? m.reviews.length : 0) > minReviews
|
||||
);
|
||||
return { success: true, data: filtered };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
async createMovie(_, args) {
|
||||
try {
|
||||
const movie = await db.movie.create(args);
|
||||
return { success: true, data: movie };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
async updateMovie(_, { id, ...args }) {
|
||||
try {
|
||||
const movie = await db.movie.findByPk(id);
|
||||
if (!movie) return { success: false, error: "Movie not found" };
|
||||
await movie.update(args);
|
||||
return { success: true, data: movie };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
async deleteMovie(_, { id }) {
|
||||
try {
|
||||
const movie = await db.movie.findByPk(id);
|
||||
if (!movie) return { success: false, error: "Movie not found" };
|
||||
await movie.destroy();
|
||||
return { success: true, data: movie };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
async addActorToMoviesByGenre(_, { actor_id, genre_id }) {
|
||||
try {
|
||||
// Find all movies for the given genre
|
||||
const genre = await db.genre.findByPk(genre_id, {
|
||||
include: [{ model: db.movie, as: "movies" }],
|
||||
});
|
||||
if (!genre) return { success: false, error: "Genre not found" };
|
||||
const movies = genre.movies;
|
||||
for (const movie of movies) {
|
||||
await db.movie_actor.findOrCreate({
|
||||
where: { movie_id: movie.id, actor_id },
|
||||
defaults: { movie_id: movie.id, actor_id },
|
||||
});
|
||||
}
|
||||
// Return updated movies
|
||||
const updatedMovies = await db.movie.findAll({
|
||||
where: { id: movies.map((m) => m.id) },
|
||||
include: [
|
||||
{ model: db.director, as: "director" },
|
||||
{ model: db.review, as: "reviews" },
|
||||
{ model: db.actor, as: "actors", through: { attributes: [] } },
|
||||
{ model: db.genre, as: "genres", through: { attributes: [] } },
|
||||
],
|
||||
});
|
||||
return { success: true, data: updatedMovies };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = MovieResolvers;
|
||||
@@ -0,0 +1,59 @@
|
||||
const db = require("../../models");
|
||||
|
||||
const ReviewResolvers = {
|
||||
Query: {
|
||||
async getReview(_, { id }) {
|
||||
try {
|
||||
const review = await db.review.findByPk(id, {
|
||||
include: [{ model: db.movie, as: "movie" }],
|
||||
});
|
||||
if (!review) return { success: false, error: "Review not found" };
|
||||
return { success: true, data: review };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
async getAllReviews() {
|
||||
try {
|
||||
const reviews = await db.review.findAll({
|
||||
include: [{ model: db.movie, as: "movie" }],
|
||||
});
|
||||
return { success: true, data: reviews };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
async createReview(_, args) {
|
||||
try {
|
||||
const review = await db.review.create(args);
|
||||
return { success: true, data: review };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
async updateReview(_, { id, ...args }) {
|
||||
try {
|
||||
const review = await db.review.findByPk(id);
|
||||
if (!review) return { success: false, error: "Review not found" };
|
||||
await review.update(args);
|
||||
return { success: true, data: review };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
async deleteReview(_, { id }) {
|
||||
try {
|
||||
const review = await db.review.findByPk(id);
|
||||
if (!review) return { success: false, error: "Review not found" };
|
||||
await review.destroy();
|
||||
return { success: true, data: review };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = ReviewResolvers;
|
||||
+35
-24
@@ -7,47 +7,58 @@
|
||||
* @author Ryan Wong
|
||||
*
|
||||
*/
|
||||
const { GraphQLUpload } = require('graphql-upload');
|
||||
const { GraphQLUpload } = require("graphql-upload");
|
||||
|
||||
const updateUserResolver = require('./update/updateUser');
|
||||
const singleUserResolver = require('./single/singleUser');
|
||||
const typeUserResolver = require('./type/typeUser');
|
||||
const updateUserResolver = require("./update/updateUser");
|
||||
const singleUserResolver = require("./single/singleUser");
|
||||
const typeUserResolver = require("./type/typeUser");
|
||||
|
||||
const createLinkResolver = require('./create/createLink');
|
||||
const typeLinkResolver = require('./type/typeLink');
|
||||
const singleLinkResolver = require('./single/singleLink');
|
||||
const deactivateAllLinksResolver = require('./delete/deactivateAllLinks');
|
||||
const createLinkResolver = require("./create/createLink");
|
||||
const typeLinkResolver = require("./type/typeLink");
|
||||
const singleLinkResolver = require("./single/singleLink");
|
||||
const deactivateAllLinksResolver = require("./delete/deactivateAllLinks");
|
||||
|
||||
const calendarResolver = require('./custom/calendar');
|
||||
const noteResolver = require('./custom/note');
|
||||
const customImageResolver = require('./custom/image');
|
||||
const uploadFileMutationResolver = require('./custom/uploadFile');
|
||||
|
||||
const connectionStepsResolver = require('./custom/connectionSteps');
|
||||
// const calendarResolver = require("./custom/calendar");
|
||||
// const noteResolver = require("./custom/note");
|
||||
// const customImageResolver = require("./custom/image");
|
||||
// const uploadFileMutationResolver = require("./custom/uploadFile");
|
||||
|
||||
// const connectionStepsResolver = require("./custom/connectionSteps");
|
||||
const movieResolvers = require("./custom/movieResolvers");
|
||||
const reviewResolvers = require("./custom/reviewResolvers");
|
||||
const directorResolvers = require("./custom/directorResolvers");
|
||||
const actorResolvers = require("./custom/actorResolvers");
|
||||
|
||||
module.exports = {
|
||||
Upload: GraphQLUpload,
|
||||
Query: {
|
||||
user: singleUserResolver,
|
||||
link: singleLinkResolver,
|
||||
...calendarResolver.Query,
|
||||
...customImageResolver.Query,
|
||||
...noteResolver.Query,
|
||||
...connectionStepsResolver.Query
|
||||
// ...calendarResolver.Query,
|
||||
// ...customImageResolver.Query,
|
||||
// ...noteResolver.Query,
|
||||
// ...connectionStepsResolver.Query,
|
||||
...movieResolvers.Query,
|
||||
...reviewResolvers.Query,
|
||||
...directorResolvers.Query,
|
||||
...actorResolvers.Query,
|
||||
},
|
||||
Mutation: {
|
||||
updateUser: updateUserResolver,
|
||||
createLink: createLinkResolver,
|
||||
deactivateAllLinks: deactivateAllLinksResolver,
|
||||
uploadFile: uploadFileMutationResolver,
|
||||
...calendarResolver.Mutation,
|
||||
...customImageResolver.Mutation,
|
||||
...noteResolver.Mutation,
|
||||
// uploadFile: uploadFileMutationResolver,
|
||||
// ...calendarResolver.Mutation,
|
||||
// ...customImageResolver.Mutation,
|
||||
// ...noteResolver.Mutation,
|
||||
...movieResolvers.Mutation,
|
||||
...reviewResolvers.Mutation,
|
||||
...directorResolvers.Mutation,
|
||||
...actorResolvers.Mutation,
|
||||
},
|
||||
|
||||
...calendarResolver.Type,
|
||||
...noteResolver.Type,
|
||||
// ...calendarResolver.Type,
|
||||
// ...noteResolver.Type,
|
||||
|
||||
User: typeUserResolver,
|
||||
Link: typeLinkResolver,
|
||||
|
||||
+13
-5
@@ -1,4 +1,4 @@
|
||||
'use strict';
|
||||
"use strict";
|
||||
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
||||
/**
|
||||
* Server
|
||||
@@ -8,11 +8,19 @@
|
||||
* @author Ryan Wong
|
||||
*
|
||||
*/
|
||||
const { app, apollo } = require('./app');
|
||||
const { app, apollo } = require("./app");
|
||||
|
||||
const PORT = 3001;
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log('Server running at ', true ? `http://localhost:${PORT}` : 'process.env.BASE_URL');
|
||||
console.log('GraphQL running at ', true ? `http://localhost:${PORT}${apollo.graphqlPath}` : `${'process.env.BASE_URL'}${apollo.graphqlPath}`);
|
||||
console.log(
|
||||
"Server running at ",
|
||||
true ? `http://localhost:${PORT}` : "process.env.BASE_URL"
|
||||
);
|
||||
console.log(
|
||||
"GraphQL running at ",
|
||||
true
|
||||
? `http://localhost:${PORT}${apollo.graphqlPath}`
|
||||
: `${"process.env.BASE_URL"}${apollo.graphqlPath}`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -211,3 +211,150 @@ type Mutation {
|
||||
|
||||
uploadFile(file: Upload!): FileUploadResponse!
|
||||
}
|
||||
|
||||
type Movie {
|
||||
id: ID!
|
||||
title: String
|
||||
director_id: Int
|
||||
main_genre: String
|
||||
status: Int
|
||||
review: String
|
||||
director: Director
|
||||
reviews: [Review]
|
||||
actors: [Actor]
|
||||
genres: [Genre]
|
||||
}
|
||||
|
||||
type Review {
|
||||
id: ID!
|
||||
notes: String
|
||||
movie_id: Int
|
||||
movie: Movie
|
||||
}
|
||||
|
||||
type Director {
|
||||
id: ID!
|
||||
name: String
|
||||
movies: [Movie]
|
||||
}
|
||||
|
||||
type Actor {
|
||||
id: ID!
|
||||
name: String
|
||||
movies: [Movie]
|
||||
}
|
||||
|
||||
type MovieActor {
|
||||
id: ID!
|
||||
actor_id: Int
|
||||
movie_id: Int
|
||||
actor: Actor
|
||||
movie: Movie
|
||||
}
|
||||
|
||||
type Genre {
|
||||
id: ID!
|
||||
name: String
|
||||
movies: [Movie]
|
||||
}
|
||||
|
||||
type GenreMovie {
|
||||
id: ID!
|
||||
movie_id: Int
|
||||
genre_id: Int
|
||||
movie: Movie
|
||||
genre: Genre
|
||||
}
|
||||
|
||||
type MovieResponse {
|
||||
success: Boolean!
|
||||
data: Movie
|
||||
error: String
|
||||
}
|
||||
|
||||
type AllMoviesResponse {
|
||||
success: Boolean!
|
||||
data: [Movie]
|
||||
error: String
|
||||
}
|
||||
|
||||
type ReviewResponse {
|
||||
success: Boolean!
|
||||
data: Review
|
||||
error: String
|
||||
}
|
||||
|
||||
type AllReviewsResponse {
|
||||
success: Boolean!
|
||||
data: [Review]
|
||||
error: String
|
||||
}
|
||||
|
||||
type DirectorResponse {
|
||||
success: Boolean!
|
||||
data: Director
|
||||
error: String
|
||||
}
|
||||
|
||||
type AllDirectorsResponse {
|
||||
success: Boolean!
|
||||
data: [Director]
|
||||
error: String
|
||||
}
|
||||
|
||||
type ActorResponse {
|
||||
success: Boolean!
|
||||
data: Actor
|
||||
error: String
|
||||
}
|
||||
|
||||
type AllActorsResponse {
|
||||
success: Boolean!
|
||||
data: [Actor]
|
||||
error: String
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
getMovie(id: ID!): MovieResponse!
|
||||
getAllMovies: AllMoviesResponse!
|
||||
getReview(id: ID!): ReviewResponse!
|
||||
getAllReviews: AllReviewsResponse!
|
||||
getDirector(id: ID!): DirectorResponse!
|
||||
getAllDirectors: AllDirectorsResponse!
|
||||
getActor(id: ID!): ActorResponse!
|
||||
getAllActors: AllActorsResponse!
|
||||
getMoviesWithReviewCount(minReviews: Int!): AllMoviesResponse!
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
createMovie(
|
||||
title: String!
|
||||
director_id: Int
|
||||
main_genre: String
|
||||
status: Int
|
||||
review: String
|
||||
): MovieResponse!
|
||||
updateMovie(
|
||||
id: ID!
|
||||
title: String
|
||||
director_id: Int
|
||||
main_genre: String
|
||||
status: Int
|
||||
review: String
|
||||
): MovieResponse!
|
||||
deleteMovie(id: ID!): MovieResponse!
|
||||
|
||||
createReview(notes: String!, movie_id: Int!): ReviewResponse!
|
||||
updateReview(id: ID!, notes: String, movie_id: Int): ReviewResponse!
|
||||
deleteReview(id: ID!): ReviewResponse!
|
||||
|
||||
createDirector(name: String!): DirectorResponse!
|
||||
updateDirector(id: ID!, name: String): DirectorResponse!
|
||||
deleteDirector(id: ID!): DirectorResponse!
|
||||
|
||||
createActor(name: String!): ActorResponse!
|
||||
updateActor(id: ID!, name: String): ActorResponse!
|
||||
deleteActor(id: ID!): ActorResponse!
|
||||
|
||||
addActorToMoviesByGenre(actor_id: Int!, genre_id: Int!): AllMoviesResponse!
|
||||
}
|
||||
|
||||
+26
-16
@@ -1,4 +1,4 @@
|
||||
'use strict';
|
||||
"use strict";
|
||||
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
||||
/**
|
||||
* Sequelize File
|
||||
@@ -8,24 +8,28 @@
|
||||
* @author Ryan Wong
|
||||
*
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let Sequelize = require('sequelize');
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
let Sequelize = require("sequelize");
|
||||
const basename = path.basename(__filename);
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { DataTypes } = require("sequelize");
|
||||
const config = {
|
||||
DB_DATABASE: 'mysql',
|
||||
DB_USERNAME: 'root',
|
||||
DB_PASSWORD: 'root',
|
||||
DB_ADAPTER: 'mysql',
|
||||
DB_NAME: 'day_1',
|
||||
DB_HOSTNAME: 'localhost',
|
||||
DB_DATABASE: "mysql",
|
||||
DB_USERNAME: "root",
|
||||
DB_PASSWORD: process.env.DB_PASSWORD || "root",
|
||||
DB_ADAPTER: "mysql",
|
||||
DB_NAME: "day_13",
|
||||
DB_HOSTNAME: "localhost",
|
||||
DB_PORT: 3306,
|
||||
};
|
||||
|
||||
let db = {};
|
||||
|
||||
let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, {
|
||||
let sequelize = new Sequelize(
|
||||
config.DB_NAME,
|
||||
config.DB_USERNAME,
|
||||
config.DB_PASSWORD,
|
||||
{
|
||||
dialect: config.DB_ADAPTER,
|
||||
username: config.DB_USERNAME,
|
||||
password: config.DB_PASSWORD,
|
||||
@@ -33,7 +37,7 @@ let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_
|
||||
host: config.DB_HOSTNAME,
|
||||
port: config.DB_PORT,
|
||||
logging: console.log,
|
||||
timezone: '-04:00',
|
||||
timezone: "-04:00",
|
||||
pool: {
|
||||
maxConnections: 1,
|
||||
minConnections: 0,
|
||||
@@ -44,13 +48,19 @@ let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_
|
||||
underscoredAll: true,
|
||||
underscored: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// sequelize.sync({ force: true });
|
||||
sequelize
|
||||
.sync()
|
||||
.then(() => console.log("Tables synced successfully!"))
|
||||
.catch((err) => console.log(err));
|
||||
|
||||
fs.readdirSync(__dirname)
|
||||
.filter((file) => {
|
||||
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
|
||||
return (
|
||||
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
|
||||
);
|
||||
})
|
||||
.forEach((file) => {
|
||||
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const order = sequelize.define("order", {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
total: { type: DataTypes.INTEGER, allowNull: false },
|
||||
stripe_id: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
product_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
});
|
||||
|
||||
return order;
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const product = sequelize.define("product", {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
title: DataTypes.STRING,
|
||||
description: DataTypes.STRING,
|
||||
price: DataTypes.INTEGER,
|
||||
image: DataTypes.STRING,
|
||||
});
|
||||
|
||||
return product;
|
||||
};
|
||||
+4
-2
@@ -3,7 +3,8 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node ./bin/www"
|
||||
"start": "node ./bin/www",
|
||||
"dev": "node --watch --env-file=.env ./bin/www"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie-parser": "~1.4.4",
|
||||
@@ -14,6 +15,7 @@
|
||||
"jade": "~1.11.0",
|
||||
"morgan": "~1.9.1",
|
||||
"mysql2": "^2.3.3",
|
||||
"sequelize": "^6.15.1"
|
||||
"sequelize": "^6.15.1",
|
||||
"stripe": "^18.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
+107
-3
@@ -1,9 +1,113 @@
|
||||
var express = require('express');
|
||||
var express = require("express");
|
||||
var router = express.Router();
|
||||
var db = require("../models");
|
||||
const stripe = require("stripe")(process.env.STRIPE_TEST_KEY); // Replace with your Stripe test secret key
|
||||
|
||||
/* GET home page. */
|
||||
router.get('/', function(req, res, next) {
|
||||
res.render('index', { title: 'Express' });
|
||||
router.get("/", async function (req, res, next) {
|
||||
try {
|
||||
const products = await db.product.findAll();
|
||||
res.render("index", { title: "Products", products });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/product/:id", async function (req, res, next) {
|
||||
try {
|
||||
const product = await db.product.findByPk(req.params.id);
|
||||
if (!product) return res.status(404).send("Product not found");
|
||||
res.render("product", { title: product.title, product });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/buy/:id", async function (req, res, next) {
|
||||
try {
|
||||
const product = await db.product.findByPk(req.params.id);
|
||||
if (!product) return res.status(404).send("Product not found");
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
payment_method_types: ["card"],
|
||||
line_items: [
|
||||
{
|
||||
price_data: {
|
||||
currency: "usd",
|
||||
product_data: {
|
||||
name: product.title,
|
||||
images: product.image ? [product.image] : [],
|
||||
},
|
||||
unit_amount: Math.round(product.price * 100),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
mode: "payment",
|
||||
success_url:
|
||||
req.protocol +
|
||||
"://" +
|
||||
req.get("host") +
|
||||
"/success?session_id={CHECKOUT_SESSION_ID}",
|
||||
cancel_url:
|
||||
req.protocol + "://" + req.get("host") + "/product/" + product.id,
|
||||
});
|
||||
res.redirect(303, session.url);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/success", async function (req, res, next) {
|
||||
try {
|
||||
const session_id = req.query.session_id;
|
||||
if (!session_id) return res.status(400).send("Missing session ID");
|
||||
const session = await stripe.checkout.sessions.retrieve(session_id);
|
||||
// Find or create order
|
||||
let order = await db.order.findOne({ where: { stripe_id: session.id } });
|
||||
if (!order) {
|
||||
// Get product_id from session metadata or line_items
|
||||
const lineItems = await stripe.checkout.sessions.listLineItems(
|
||||
session.id,
|
||||
{ limit: 1 }
|
||||
);
|
||||
const productName =
|
||||
lineItems.data[0].description || lineItems.data[0].price.product;
|
||||
const dbProduct = await db.product.findOne({
|
||||
where: { title: productName },
|
||||
});
|
||||
await db.order.create({
|
||||
product_id: dbProduct ? dbProduct.id : null,
|
||||
total: session.amount_total,
|
||||
stripe_id: session.id,
|
||||
status: session.payment_status === "paid" ? 1 : 0,
|
||||
});
|
||||
}
|
||||
res.render("success", { title: "Thank You", session });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/create-product", function (req, res) {
|
||||
res.render("create-product", { title: "Add Product" });
|
||||
});
|
||||
|
||||
router.post("/create-product", async function (req, res, next) {
|
||||
try {
|
||||
const { title, description, price, image } = req.body;
|
||||
if (!title || !description || !price) {
|
||||
return res
|
||||
.status(400)
|
||||
.render("create-product", {
|
||||
title: "Add Product",
|
||||
error: "All fields except image are required.",
|
||||
});
|
||||
}
|
||||
await db.product.create({ title, description, price, image });
|
||||
res.redirect("/");
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
extends layout
|
||||
|
||||
block content
|
||||
.container.mt-5
|
||||
h2 Add Product
|
||||
if error
|
||||
.alert.alert-danger= error
|
||||
form(method="POST" action="/create-product")
|
||||
.form-group
|
||||
label(for="title") Title
|
||||
input.form-control(type="text" name="title" id="title" required)
|
||||
.form-group
|
||||
label(for="description") Description
|
||||
textarea.form-control(name="description" id="description" required)
|
||||
.form-group
|
||||
label(for="price") Price (in dollars)
|
||||
input.form-control(type="number" name="price" id="price" min="0" step="0.01" required)
|
||||
.form-group
|
||||
label(for="image") Image URL
|
||||
input.form-control(type="text" name="image" id="image")
|
||||
button.btn.btn-primary(type="submit") Add Product
|
||||
a.btn.btn-secondary.ml-2(href="/") Cancel
|
||||
+16
-1
@@ -2,4 +2,19 @@ extends layout
|
||||
|
||||
block content
|
||||
h1= title
|
||||
p Welcome to #{title}
|
||||
a.btn.btn-success.mb-4(href="/create-product") Create Product
|
||||
if products && products.length
|
||||
.row
|
||||
each product in products
|
||||
.col-md-4.mb-4
|
||||
.card
|
||||
if product.image
|
||||
img.card-img-top(src=product.image, alt=product.title, style="width:100px;height:100px;object-fit:contain;")
|
||||
.card-body
|
||||
h5.card-title= product.title
|
||||
p.card-text= product.description
|
||||
p.card-text
|
||||
strong $#{product.price}
|
||||
a.btn.btn-primary(href=`/product/${product.id}`) View Details
|
||||
else
|
||||
p No products found.
|
||||
|
||||
@@ -2,6 +2,7 @@ doctype html
|
||||
html
|
||||
head
|
||||
title= title
|
||||
link(rel='stylesheet', href='https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css')
|
||||
link(rel='stylesheet', href='/stylesheets/style.css')
|
||||
body
|
||||
block content
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
extends layout
|
||||
|
||||
block content
|
||||
.container.mt-5
|
||||
.row
|
||||
.col-md-6
|
||||
if product.image
|
||||
img.img-fluid(src=product.image, alt=product.title style="width:100px;height:100px;object-fit:contain;")
|
||||
.col-md-6
|
||||
h2= product.title
|
||||
p= product.description
|
||||
h4.text-success $#{product.price}
|
||||
form(action=`/buy/${product.id}` method="POST")
|
||||
button.btn.btn-success(type="submit") Buy Now
|
||||
a.btn.btn-secondary.mt-3(href="/") Back to Products
|
||||
@@ -0,0 +1,23 @@
|
||||
extends layout
|
||||
|
||||
block content
|
||||
.container.mt-5
|
||||
.alert.alert-success
|
||||
h2 Thank you for your purchase!
|
||||
p Your payment was successful.
|
||||
if session
|
||||
h4 Payment Details
|
||||
table.table
|
||||
tr
|
||||
th Session ID
|
||||
td= session.id
|
||||
tr
|
||||
th Payment Status
|
||||
td= session.payment_status
|
||||
tr
|
||||
th Amount Total
|
||||
td $#{(session.amount_total / 100).toFixed(2)}
|
||||
tr
|
||||
th Payment Method
|
||||
td= session.payment_method_types && session.payment_method_types[0]
|
||||
a.btn.btn-primary.mt-4(href="/") Back to Products
|
||||
@@ -0,0 +1,74 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
class ControllerBuilder {
|
||||
static build() {
|
||||
const config = require("./configuration.json");
|
||||
const controllerDir = path.join(__dirname, "release/controllers");
|
||||
|
||||
// Create release/controllers directory
|
||||
if (!fs.existsSync(controllerDir))
|
||||
fs.mkdirSync(controllerDir, { recursive: true });
|
||||
|
||||
config.model.forEach((model) => {
|
||||
const controllerCode = `const express = require('express');
|
||||
const router = express.Router();
|
||||
const model = require('../models/${model.name}.model.js');
|
||||
|
||||
// CREATE
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const data = await model.create(req.body);
|
||||
res.status(201).json(data);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// READ ALL
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const data = await model.findAll();
|
||||
res.json(data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// UPDATE
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const updated = await model.update(req.body, {
|
||||
where: { id: req.params.id }
|
||||
});
|
||||
if (updated[0] === 0) return res.status(404).json({ error: 'Not found' });
|
||||
res.json(await model.findByPk(req.params.id));
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
const deleted = await model.destroy({
|
||||
where: { id: req.params.id }
|
||||
});
|
||||
if (!deleted) return res.status(404).json({ error: 'Not found' });
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;`;
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(controllerDir, `${model.name}.controller.js`),
|
||||
controllerCode
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ControllerBuilder;
|
||||
+34
-9
@@ -1,13 +1,38 @@
|
||||
let fs = require('fs');
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
function Model_builder() {
|
||||
let config = fs.readFileSync('configuration.json');
|
||||
class ModelBuilder {
|
||||
static build() {
|
||||
const config = require("./configuration.json");
|
||||
const modelDir = path.join(__dirname, "release/models");
|
||||
|
||||
this.build = function () {
|
||||
//generate files and put it into release folder
|
||||
//Copy initialize files into release folder
|
||||
//TODO
|
||||
// Create release/models directory
|
||||
if (!fs.existsSync(modelDir)) fs.mkdirSync(modelDir, { recursive: true });
|
||||
|
||||
config.model.forEach((model) => {
|
||||
const modelCode = `const { DataTypes } = require('sequelize');
|
||||
module.exports = (sequelize) => {
|
||||
return sequelize.define('${model.name}', {
|
||||
${model.field
|
||||
.map(
|
||||
(field) => `
|
||||
${field[0]}: {
|
||||
type: DataTypes.${field[1].toUpperCase()},
|
||||
allowNull: ${field[3] === "required" ? "false" : "true"}
|
||||
}`
|
||||
)
|
||||
.join(",")}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
};`;
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(modelDir, `${model.name}.model.js`),
|
||||
modelCode
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
module.exports = ModelBuilder;
|
||||
|
||||
@@ -2,35 +2,35 @@
|
||||
"model": [
|
||||
{
|
||||
"name": "location",
|
||||
"field: [
|
||||
"field": [
|
||||
["id", "integer", "ID", "required"],
|
||||
["name", "string", "Name", "required"],
|
||||
["status", "integer", "Status", "required"],
|
||||
["status", "integer", "Status", "required"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"field: [
|
||||
"field": [
|
||||
["id", "integer", "ID", "required"],
|
||||
["email", "string", "Email", "required"],
|
||||
["status", "integer", "Status", "required"],
|
||||
["status", "integer", "Status", "required"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sms",
|
||||
"field: [
|
||||
"field": [
|
||||
["id", "integer", "ID", "required"],
|
||||
["phone", "string", "Phone", "required"],
|
||||
["status", "integer", "Status", "required"],
|
||||
["status", "integer", "Status", "required"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "user",
|
||||
"field: [
|
||||
"field": [
|
||||
["id", "integer", "ID", "required"],
|
||||
["name", "string", "Name", "required"],
|
||||
["email", "string", "Email", "required"],
|
||||
["status", "integer", "Status", "required"],
|
||||
["status", "integer", "Status", "required"]
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
const Model_builder = require("./Model_builder");
|
||||
const Controller_builder = require("./Controller_builder");
|
||||
|
||||
Model_builder.build();
|
||||
Controller_builder.build();
|
||||
|
||||
console.log("Model and controller files generated in /release");
|
||||
@@ -0,0 +1,51 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const model = require('../models/email.model.js');
|
||||
|
||||
// CREATE
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const data = await model.create(req.body);
|
||||
res.status(201).json(data);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// READ ALL
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const data = await model.findAll();
|
||||
res.json(data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// UPDATE
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const updated = await model.update(req.body, {
|
||||
where: { id: req.params.id }
|
||||
});
|
||||
if (updated[0] === 0) return res.status(404).json({ error: 'Not found' });
|
||||
res.json(await model.findByPk(req.params.id));
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
const deleted = await model.destroy({
|
||||
where: { id: req.params.id }
|
||||
});
|
||||
if (!deleted) return res.status(404).json({ error: 'Not found' });
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,51 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const model = require('../models/location.model.js');
|
||||
|
||||
// CREATE
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const data = await model.create(req.body);
|
||||
res.status(201).json(data);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// READ ALL
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const data = await model.findAll();
|
||||
res.json(data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// UPDATE
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const updated = await model.update(req.body, {
|
||||
where: { id: req.params.id }
|
||||
});
|
||||
if (updated[0] === 0) return res.status(404).json({ error: 'Not found' });
|
||||
res.json(await model.findByPk(req.params.id));
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
const deleted = await model.destroy({
|
||||
where: { id: req.params.id }
|
||||
});
|
||||
if (!deleted) return res.status(404).json({ error: 'Not found' });
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,51 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const model = require('../models/sms.model.js');
|
||||
|
||||
// CREATE
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const data = await model.create(req.body);
|
||||
res.status(201).json(data);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// READ ALL
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const data = await model.findAll();
|
||||
res.json(data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// UPDATE
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const updated = await model.update(req.body, {
|
||||
where: { id: req.params.id }
|
||||
});
|
||||
if (updated[0] === 0) return res.status(404).json({ error: 'Not found' });
|
||||
res.json(await model.findByPk(req.params.id));
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
const deleted = await model.destroy({
|
||||
where: { id: req.params.id }
|
||||
});
|
||||
if (!deleted) return res.status(404).json({ error: 'Not found' });
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,51 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const model = require('../models/user.model.js');
|
||||
|
||||
// CREATE
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const data = await model.create(req.body);
|
||||
res.status(201).json(data);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// READ ALL
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const data = await model.findAll();
|
||||
res.json(data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// UPDATE
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const updated = await model.update(req.body, {
|
||||
where: { id: req.params.id }
|
||||
});
|
||||
if (updated[0] === 0) return res.status(404).json({ error: 'Not found' });
|
||||
res.json(await model.findByPk(req.params.id));
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
const deleted = await model.destroy({
|
||||
where: { id: req.params.id }
|
||||
});
|
||||
if (!deleted) return res.status(404).json({ error: 'Not found' });
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,20 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
module.exports = (sequelize) => {
|
||||
return sequelize.define('email', {
|
||||
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
module.exports = (sequelize) => {
|
||||
return sequelize.define('location', {
|
||||
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
module.exports = (sequelize) => {
|
||||
return sequelize.define('sms', {
|
||||
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
phone: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
module.exports = (sequelize) => {
|
||||
return sequelize.define('user', {
|
||||
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
};
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
require("dotenv").config();
|
||||
const express = require("express");
|
||||
const bodyParser = require("body-parser");
|
||||
const { User, Post, sequelize } = require("./models");
|
||||
|
||||
const app = express();
|
||||
app.use(bodyParser.json());
|
||||
|
||||
// Helper to get model by resource name
|
||||
const models = { users: User, posts: Post };
|
||||
|
||||
// Sync DB
|
||||
sequelize.sync();
|
||||
|
||||
// TreeQL-style dynamic GET
|
||||
app.get("/:resource/:id?/:subresource?", async (req, res) => {
|
||||
const { resource, id, subresource } = req.params;
|
||||
const Model = models[resource];
|
||||
if (!Model) return res.status(404).json({ error: "Resource not found" });
|
||||
|
||||
try {
|
||||
if (id) {
|
||||
const instance = await Model.findByPk(
|
||||
id,
|
||||
subresource ? { include: subresource } : {}
|
||||
);
|
||||
if (!instance) return res.status(404).json({ error: "Not found" });
|
||||
if (subresource && instance[subresource]) {
|
||||
return res.json(instance[subresource]);
|
||||
}
|
||||
return res.json(instance);
|
||||
} else {
|
||||
const all = await Model.findAll();
|
||||
return res.json(all);
|
||||
}
|
||||
} catch (e) {
|
||||
return res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// TreeQL-style POST (create resource or subresource)
|
||||
app.post("/:resource/:id?/:subresource?", async (req, res) => {
|
||||
const { resource, id, subresource } = req.params;
|
||||
const Model = models[resource];
|
||||
if (!Model) return res.status(404).json({ error: "Resource not found" });
|
||||
|
||||
try {
|
||||
if (id && subresource) {
|
||||
// e.g. POST /users/1/posts
|
||||
const parent = await models[resource].findByPk(id);
|
||||
if (!parent) return res.status(404).json({ error: "Parent not found" });
|
||||
const childModel = models[subresource];
|
||||
if (!childModel)
|
||||
return res.status(404).json({ error: "Subresource not found" });
|
||||
const child = await childModel.create({ ...req.body, UserId: id });
|
||||
return res.status(201).json(child);
|
||||
} else {
|
||||
// e.g. POST /users
|
||||
const instance = await Model.create(req.body);
|
||||
return res.status(201).json(instance);
|
||||
}
|
||||
} catch (e) {
|
||||
return res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// TreeQL-style PUT (update resource)
|
||||
app.put("/:resource/:id", async (req, res) => {
|
||||
const { resource, id } = req.params;
|
||||
const Model = models[resource];
|
||||
if (!Model) return res.status(404).json({ error: "Resource not found" });
|
||||
|
||||
try {
|
||||
const instance = await Model.findByPk(id);
|
||||
if (!instance) return res.status(404).json({ error: "Not found" });
|
||||
await instance.update(req.body);
|
||||
return res.json(instance);
|
||||
} catch (e) {
|
||||
return res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// TreeQL-style DELETE (delete resource)
|
||||
app.delete("/:resource/:id", async (req, res) => {
|
||||
const { resource, id } = req.params;
|
||||
const Model = models[resource];
|
||||
if (!Model) return res.status(404).json({ error: "Resource not found" });
|
||||
|
||||
try {
|
||||
const instance = await Model.findByPk(id);
|
||||
if (!instance) return res.status(404).json({ error: "Not found" });
|
||||
await instance.destroy();
|
||||
return res.json({ success: true });
|
||||
} catch (e) {
|
||||
return res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => console.log(`TreeQL API running on port ${PORT}`));
|
||||
@@ -0,0 +1,9 @@
|
||||
require("dotenv").config();
|
||||
const { Sequelize } = require("sequelize");
|
||||
|
||||
const sequelize = new Sequelize("day_15", "root", process.env.DB_PASSWORD, {
|
||||
host: "localhost",
|
||||
dialect: "mysql",
|
||||
});
|
||||
|
||||
module.exports = sequelize;
|
||||
@@ -0,0 +1,15 @@
|
||||
const { DataTypes } = require("sequelize");
|
||||
const sequelize = require("./db");
|
||||
|
||||
const User = sequelize.define("User", {
|
||||
name: DataTypes.STRING,
|
||||
});
|
||||
|
||||
const Post = sequelize.define("Post", {
|
||||
title: DataTypes.STRING,
|
||||
});
|
||||
|
||||
User.hasMany(Post, { as: "posts" });
|
||||
Post.belongsTo(User);
|
||||
|
||||
module.exports = { User, Post, sequelize };
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "day15",
|
||||
"version": "1.0.0",
|
||||
"description": "- setup project\r - implement https://www.treeql.org/ manually",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"body-parser": "^2.2.0",
|
||||
"dotenv": "^17.2.0",
|
||||
"express": "^5.1.0",
|
||||
"mysql2": "^3.14.2",
|
||||
"sequelize": "^6.37.7"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,29 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "day16",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"fabric": "^6.7.0",
|
||||
"fabricjs-react": "^1.2.2",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.30.1",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.3.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"vite": "^7.0.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
@@ -0,0 +1,317 @@
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import { Canvas, Textbox, Image } from "fabric";
|
||||
|
||||
function App() {
|
||||
const canvasRef = useRef(null);
|
||||
const fabricRef = useRef(null);
|
||||
const [activeMenu, setActiveMenu] = useState("Background");
|
||||
const [selectedObject, setSelectedObject] = useState(null);
|
||||
const [fontSize, setFontSize] = useState(60);
|
||||
const [images, setImages] = useState([]);
|
||||
const [isImagesLoading, setIsImagesLoading] = useState(false);
|
||||
|
||||
const colors = [
|
||||
"#F4A261",
|
||||
"#E76F51",
|
||||
"#2A9D8F",
|
||||
"#264653",
|
||||
"#A8DADC",
|
||||
"#457B9D",
|
||||
"#E63946",
|
||||
"#F7B801",
|
||||
"#A259F7",
|
||||
"#6D6875",
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = new Canvas(canvasRef.current, {
|
||||
width: 900,
|
||||
height: 400,
|
||||
backgroundColor: "#F4A261",
|
||||
});
|
||||
fabricRef.current = canvas;
|
||||
|
||||
// Object selection logic
|
||||
function updateSelectedObject() {
|
||||
setSelectedObject(canvas.getActiveObject());
|
||||
const obj = canvas.getActiveObject();
|
||||
if (obj && obj.type === "text") {
|
||||
setFontSize(obj.fontSize);
|
||||
}
|
||||
}
|
||||
canvas.on("selection:created", updateSelectedObject);
|
||||
canvas.on("selection:updated", updateSelectedObject);
|
||||
canvas.on("selection:cleared", updateSelectedObject);
|
||||
|
||||
// Allow editing text on double click
|
||||
canvas.on("mouse:dblclick", (e) => {
|
||||
if (e.target && e.target.type === "text") {
|
||||
e.target.enterEditing();
|
||||
e.target.selectAll();
|
||||
}
|
||||
});
|
||||
|
||||
// Force initial render so canvas is visible
|
||||
canvas.requestRenderAll();
|
||||
|
||||
return () => {
|
||||
canvas.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (images.length === 0) {
|
||||
setIsImagesLoading(true);
|
||||
fetch("https://picsum.photos/v2/list")
|
||||
.then((res) => res.json())
|
||||
.then((data) => setImages(data))
|
||||
.catch((err) => console.error("Failed to fetch images", err))
|
||||
.finally(() => setIsImagesLoading(false));
|
||||
}
|
||||
}, [images]);
|
||||
|
||||
// Add text to canvas
|
||||
const addText = (type) => {
|
||||
let text = "";
|
||||
let size = 60;
|
||||
if (type === "h1") {
|
||||
text = "Add a heading";
|
||||
size = 60;
|
||||
} else if (type === "h6") {
|
||||
text = "Add a subheading";
|
||||
size = 32;
|
||||
} else {
|
||||
text = "Add a little bit of body text";
|
||||
size = 20;
|
||||
}
|
||||
const canvas = fabricRef.current;
|
||||
const textbox = new Textbox(text, {
|
||||
left: canvas.width / 2,
|
||||
top: canvas.height / 2,
|
||||
fontSize: size,
|
||||
fontWeight: type === "h1" ? "bold" : "normal",
|
||||
editable: true,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
fill: "#222",
|
||||
});
|
||||
canvas.add(textbox).setActiveObject(textbox);
|
||||
canvas.renderAll();
|
||||
};
|
||||
|
||||
// Add image to canvas
|
||||
const addImageToCanvas = async (url) => {
|
||||
const img = await Image.fromURL(url, { crossOrigin: "anonymous" });
|
||||
const canvas = fabricRef.current;
|
||||
img.scaleToWidth(200);
|
||||
img.set({
|
||||
left: canvas.width / 2,
|
||||
top: canvas.height / 2,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
});
|
||||
canvas.add(img);
|
||||
canvas.setActiveObject(img);
|
||||
canvas.requestRenderAll();
|
||||
};
|
||||
|
||||
// Set background color
|
||||
const setBackgroundColor = (color) => {
|
||||
if (fabricRef.current) {
|
||||
fabricRef.current.backgroundColor = color;
|
||||
fabricRef.current.requestRenderAll();
|
||||
}
|
||||
};
|
||||
|
||||
// Download canvas
|
||||
const downloadCanvas = () => {
|
||||
if (fabricRef.current) {
|
||||
const dataURL = fabricRef.current.toDataURL({
|
||||
format: "png",
|
||||
quality: 1,
|
||||
});
|
||||
const link = document.createElement("a");
|
||||
link.href = dataURL;
|
||||
link.download = "canvas.png";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
};
|
||||
|
||||
// Change font size of selected text
|
||||
const handleFontSizeChange = (e) => {
|
||||
const size = parseInt(e.target.value, 10);
|
||||
setFontSize(size);
|
||||
if (selectedObject && selectedObject.type === "text") {
|
||||
selectedObject.set({ fontSize: size });
|
||||
fabricRef.current.renderAll();
|
||||
}
|
||||
};
|
||||
|
||||
// Delete selected object (text or image)
|
||||
const deleteSelectedObject = () => {
|
||||
if (selectedObject) {
|
||||
fabricRef.current.remove(selectedObject);
|
||||
setSelectedObject(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='h-screen w-screen flex flex-col bg-gray-100'>
|
||||
{/* Navbar */}
|
||||
<nav className='flex items-center justify-between h-12 bg-black px-4'>
|
||||
<div className='text-white text-2xl font-bold flex items-center gap-2'>
|
||||
<span role='img' aria-label='logo'>
|
||||
🧩
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-4'>
|
||||
{selectedObject && (
|
||||
<>
|
||||
{selectedObject.type === "text" && (
|
||||
<select
|
||||
className='border rounded px-2 py-1 text-sm'
|
||||
value={fontSize}
|
||||
onChange={handleFontSizeChange}
|
||||
>
|
||||
{[12, 16, 20, 24, 32, 40, 48, 60, 72, 96].map((size) => (
|
||||
<option key={size} value={size}>
|
||||
{size}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<button
|
||||
className='ml-2 text-xl text-gray-700 hover:text-red-600'
|
||||
onClick={deleteSelectedObject}
|
||||
title='Delete selected object'
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
className='bg-white text-black px-4 py-1 rounded shadow hover:bg-gray-200'
|
||||
onClick={downloadCanvas}
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
<div className='flex flex-1 overflow-hidden'>
|
||||
{/* Sidebar */}
|
||||
<aside className='w-56 bg-white border-r flex flex-col p-4 gap-6'>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<button
|
||||
className={`text-left font-medium hover:text-blue-600 flex items-center gap-2 ${
|
||||
activeMenu === "Elements" ? "text-blue-600" : ""
|
||||
}`}
|
||||
onClick={() => setActiveMenu("Elements")}
|
||||
>
|
||||
Elements
|
||||
</button>
|
||||
<button
|
||||
className={`text-left font-medium hover:text-blue-600 flex items-center gap-2 ${
|
||||
activeMenu === "Images" ? "text-blue-600" : ""
|
||||
}`}
|
||||
onClick={() => setActiveMenu("Images")}
|
||||
>
|
||||
Images
|
||||
</button>
|
||||
<button
|
||||
className={`text-left font-medium hover:text-blue-600 flex items-center gap-2 ${
|
||||
activeMenu === "Text" ? "text-blue-600" : ""
|
||||
}`}
|
||||
onClick={() => setActiveMenu("Text")}
|
||||
>
|
||||
Text
|
||||
</button>
|
||||
<button
|
||||
className={`text-left font-medium hover:text-blue-600 flex items-center gap-2 ${
|
||||
activeMenu === "Background" ? "text-blue-600" : ""
|
||||
}`}
|
||||
onClick={() => setActiveMenu("Background")}
|
||||
>
|
||||
Background
|
||||
</button>
|
||||
</div>
|
||||
{/* Images menu */}
|
||||
{activeMenu === "Images" && (
|
||||
<>
|
||||
{isImagesLoading ? (
|
||||
<div className='w-full grid place-items-center'>
|
||||
<div className='rounded-full border-2 border-black size-20 border-t-transparent animate-spin' />
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex flex-col gap-2 mt-6 overflow-y-auto'>
|
||||
<div className='grid grid-cols-2 gap-2'>
|
||||
{images.map((image) => (
|
||||
<button
|
||||
key={image.id}
|
||||
onClick={() => addImageToCanvas(image.download_url)}
|
||||
className='hover:opacity-80'
|
||||
>
|
||||
<img
|
||||
src={image.download_url}
|
||||
alt={image.author}
|
||||
className='w-full h-auto rounded'
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* Text menu */}
|
||||
{activeMenu === "Text" && (
|
||||
<div className='flex flex-col gap-2 mt-6'>
|
||||
<button
|
||||
className='bg-gray-100 rounded px-3 py-2 text-left font-bold text-lg hover:bg-gray-200'
|
||||
onClick={() => addText("h1")}
|
||||
>
|
||||
Add a heading
|
||||
</button>
|
||||
<button
|
||||
className='bg-gray-100 rounded px-3 py-2 text-left font-semibold hover:bg-gray-200'
|
||||
onClick={() => addText("h6")}
|
||||
>
|
||||
Add a subheading
|
||||
</button>
|
||||
<button
|
||||
className='bg-gray-100 rounded px-3 py-2 text-left text-sm hover:bg-gray-200'
|
||||
onClick={() => addText("p")}
|
||||
>
|
||||
Add a little bit of body text
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* Background color swatches */}
|
||||
{activeMenu === "Background" && (
|
||||
<div>
|
||||
<div className='mb-2 text-sm font-semibold'>Default colors</div>
|
||||
<div className='grid grid-cols-5 gap-2'>
|
||||
{colors.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
className='w-7 h-7 rounded shadow border-2 border-white hover:border-black focus:outline-none'
|
||||
style={{ background: color }}
|
||||
aria-label={color}
|
||||
onClick={() => setBackgroundColor(color)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
{/* Canvas Area */}
|
||||
<main className='flex-1 flex items-center justify-center bg-gray-50'>
|
||||
<canvas ref={canvasRef} width={900} height={400} />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,7 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
});
|
||||
+17
-16
@@ -1,11 +1,12 @@
|
||||
var createError = require('http-errors');
|
||||
var express = require('express');
|
||||
var path = require('path');
|
||||
var cookieParser = require('cookie-parser');
|
||||
var logger = require('morgan');
|
||||
var createError = require("http-errors");
|
||||
var express = require("express");
|
||||
var path = require("path");
|
||||
var cookieParser = require("cookie-parser");
|
||||
var logger = require("morgan");
|
||||
|
||||
var indexRouter = require('./routes/index');
|
||||
var usersRouter = require('./routes/users');
|
||||
var indexRouter = require("./routes/index");
|
||||
var usersRouter = require("./routes/users");
|
||||
var apiRouter = require("./routes/api");
|
||||
|
||||
const db = require("./models");
|
||||
var cors = require("cors");
|
||||
@@ -13,18 +14,18 @@ var cors = require("cors");
|
||||
var app = express();
|
||||
app.set("db", db);
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'jade');
|
||||
app.set("views", path.join(__dirname, "views"));
|
||||
app.set("view engine", "ejs");
|
||||
app.use(cors());
|
||||
app.use(logger('dev'));
|
||||
app.use(logger("dev"));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
app.use('/', indexRouter);
|
||||
app.use('/users', usersRouter);
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
|
||||
app.use("/", indexRouter);
|
||||
app.use("/users", usersRouter);
|
||||
app.use("/api", apiRouter);
|
||||
// catch 404 and forward to error handler
|
||||
app.use(function (req, res, next) {
|
||||
next(createError(404));
|
||||
@@ -34,11 +35,11 @@ app.use(function (req, res, next) {
|
||||
app.use(function (err, req, res, next) {
|
||||
// set locals, only providing error in development
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
res.locals.error = req.app.get("env") === "development" ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
res.render("error");
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const booking = sequelize.define(
|
||||
"booking",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
name: DataTypes.STRING,
|
||||
email: DataTypes.STRING,
|
||||
company: DataTypes.STRING,
|
||||
phone: DataTypes.STRING,
|
||||
notes: DataTypes.TEXT,
|
||||
date: DataTypes.STRING,
|
||||
time: DataTypes.STRING,
|
||||
timezone: DataTypes.STRING,
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
freezeTableName: true,
|
||||
tableName: "booking",
|
||||
createdAt: "created_at",
|
||||
updatedAt: false,
|
||||
}
|
||||
);
|
||||
return booking;
|
||||
};
|
||||
+30
-16
@@ -1,4 +1,4 @@
|
||||
'use strict';
|
||||
"use strict";
|
||||
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
||||
/**
|
||||
* Sequelize File
|
||||
@@ -8,24 +8,28 @@
|
||||
* @author Ryan Wong
|
||||
*
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let Sequelize = require('sequelize');
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
let Sequelize = require("sequelize");
|
||||
const basename = path.basename(__filename);
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { DataTypes } = require("sequelize");
|
||||
const config = {
|
||||
DB_DATABASE: 'mysql',
|
||||
DB_USERNAME: 'root',
|
||||
DB_PASSWORD: 'root',
|
||||
DB_ADAPTER: 'mysql',
|
||||
DB_NAME: 'day_1',
|
||||
DB_HOSTNAME: 'localhost',
|
||||
DB_DATABASE: "mysql",
|
||||
DB_USERNAME: "root",
|
||||
DB_PASSWORD: process.env.DB_PASSWORD || "root",
|
||||
DB_ADAPTER: "mysql",
|
||||
DB_NAME: "day_17",
|
||||
DB_HOSTNAME: "localhost",
|
||||
DB_PORT: 3306,
|
||||
};
|
||||
|
||||
let db = {};
|
||||
|
||||
let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, {
|
||||
let sequelize = new Sequelize(
|
||||
config.DB_NAME,
|
||||
config.DB_USERNAME,
|
||||
config.DB_PASSWORD,
|
||||
{
|
||||
dialect: config.DB_ADAPTER,
|
||||
username: config.DB_USERNAME,
|
||||
password: config.DB_PASSWORD,
|
||||
@@ -33,7 +37,7 @@ let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_
|
||||
host: config.DB_HOSTNAME,
|
||||
port: config.DB_PORT,
|
||||
logging: console.log,
|
||||
timezone: '-04:00',
|
||||
timezone: "-04:00",
|
||||
pool: {
|
||||
maxConnections: 1,
|
||||
minConnections: 0,
|
||||
@@ -44,13 +48,23 @@ let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_
|
||||
underscoredAll: true,
|
||||
underscored: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// sequelize.sync({ force: true });
|
||||
sequelize
|
||||
.sync()
|
||||
.then(() => {
|
||||
console.log("Database & tables created!");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
fs.readdirSync(__dirname)
|
||||
.filter((file) => {
|
||||
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
|
||||
return (
|
||||
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
|
||||
);
|
||||
})
|
||||
.forEach((file) => {
|
||||
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
||||
|
||||
+5
-1
@@ -3,17 +3,21 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node ./bin/www"
|
||||
"start": "node ./bin/www",
|
||||
"dev": "node --watch --env-file=.env ./bin/www"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie-parser": "~1.4.4",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "~2.6.9",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "~4.16.1",
|
||||
"http-errors": "~1.6.3",
|
||||
"jade": "~1.11.0",
|
||||
"moment-timezone": "^0.6.0",
|
||||
"morgan": "~1.9.1",
|
||||
"mysql2": "^2.3.3",
|
||||
"node-input-validator": "^4.5.1",
|
||||
"sequelize": "^6.15.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,289 @@
|
||||
body {
|
||||
padding: 50px;
|
||||
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "Inter", "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||
background: #eaeaea;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00B7FF;
|
||||
.calendar-container {
|
||||
max-width: 1100px;
|
||||
margin: 40px auto;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.07);
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
background: #04316a;
|
||||
color: #fff;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
padding: 28px 32px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.calendar-content {
|
||||
padding: 32px 32px 0 32px;
|
||||
}
|
||||
|
||||
.calendar-labels {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.calendar-label-main {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.calendar-label-duration {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.calendar-label-duration span {
|
||||
font-weight: 400;
|
||||
}
|
||||
.calendar-label-timezone {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.timezone-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #888;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
font-size: 1rem;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* Modal Overlay */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.modal-content {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 32px 40px;
|
||||
min-width: 340px;
|
||||
max-width: 90vw;
|
||||
box-shadow: 0 4px 32px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.modal-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 18px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.modal-format-switch {
|
||||
margin-bottom: 18px;
|
||||
font-size: 0.98rem;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
.timezone-groups {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 32px 48px;
|
||||
justify-content: center;
|
||||
}
|
||||
.timezone-group {
|
||||
min-width: 180px;
|
||||
}
|
||||
.timezone-group-title {
|
||||
font-size: 0.98rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: #04316a;
|
||||
}
|
||||
.timezone-option {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.97rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Calendar Table */
|
||||
.calendar-table-wrapper {
|
||||
overflow-x: auto;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.calendar-table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
background: #fff;
|
||||
}
|
||||
.calendar-table th,
|
||||
.calendar-table td {
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
min-width: 120px;
|
||||
}
|
||||
.calendar-table th {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #04316a;
|
||||
border-bottom: 2px solid #eaeaea;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
.calendar-day-label {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.calendar-date-label {
|
||||
font-size: 0.98rem;
|
||||
color: #888;
|
||||
font-weight: 400;
|
||||
}
|
||||
.calendar-slot-btn {
|
||||
background: #f5f8fa;
|
||||
border: 1px solid #dbe6f3;
|
||||
border-radius: 5px;
|
||||
color: #04316a;
|
||||
font-size: 1rem;
|
||||
padding: 7px 0;
|
||||
width: 90%;
|
||||
margin: 0 auto;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border 0.15s;
|
||||
font-weight: 500;
|
||||
}
|
||||
.calendar-slot-btn:hover {
|
||||
background: #e6f0ff;
|
||||
border-color: #04316a;
|
||||
}
|
||||
.calendar-table td {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.calendar-week-nav {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 18px;
|
||||
margin-top: 12px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.calendar-week-nav a {
|
||||
color: #04316a;
|
||||
text-decoration: underline;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Booking Form */
|
||||
.booking-form {
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
.form-error {
|
||||
color: red;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: #222;
|
||||
}
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
border: 1px solid #dbe6f3;
|
||||
border-radius: 5px;
|
||||
padding: 8px 10px;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
background: #f5f8fa;
|
||||
resize: none;
|
||||
}
|
||||
.form-group textarea {
|
||||
min-height: 70px;
|
||||
}
|
||||
.form-submit-btn {
|
||||
background: #04316a;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 1.08rem;
|
||||
font-weight: 600;
|
||||
padding: 10px 0;
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.form-submit-btn:hover {
|
||||
background: #0050b3;
|
||||
}
|
||||
|
||||
/* Success Message */
|
||||
.success-message {
|
||||
text-align: center;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 500;
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
/* Responsive Styles */
|
||||
@media (max-width: 900px) {
|
||||
.calendar-container {
|
||||
max-width: 98vw;
|
||||
margin: 16px auto;
|
||||
}
|
||||
.calendar-content {
|
||||
padding: 18px 6vw 0 6vw;
|
||||
}
|
||||
.modal-content {
|
||||
padding: 18px 8vw;
|
||||
}
|
||||
.calendar-table th,
|
||||
.calendar-table td {
|
||||
min-width: 80px;
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.calendar-header {
|
||||
font-size: 1.2rem;
|
||||
padding: 18px 10px;
|
||||
}
|
||||
.calendar-content {
|
||||
padding: 10px 2vw 0 2vw;
|
||||
}
|
||||
.modal-content {
|
||||
min-width: 90vw;
|
||||
padding: 10px 2vw;
|
||||
}
|
||||
.calendar-table th,
|
||||
.calendar-table td {
|
||||
min-width: 60px;
|
||||
font-size: 0.93rem;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.booking-form {
|
||||
max-width: 98vw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const db = require("../models");
|
||||
const {
|
||||
validateInput,
|
||||
handleValidationErrorForAPI,
|
||||
} = require("../services/ValidationService");
|
||||
|
||||
// Validation rules for booking
|
||||
const bookingValidation = {
|
||||
name: "required|string",
|
||||
email: "required|email",
|
||||
company: "required|string",
|
||||
phone: "required|string",
|
||||
notes: "required|string",
|
||||
date: "required|string",
|
||||
time: "required|string",
|
||||
timezone: "required|string",
|
||||
};
|
||||
|
||||
// POST /api/bookings - Create a new booking
|
||||
router.post(
|
||||
"/bookings",
|
||||
validateInput(bookingValidation, {
|
||||
"name.required": "Name is required",
|
||||
"email.required": "Email is required",
|
||||
"email.email": "Invalid email address",
|
||||
"company.required": "Company is required",
|
||||
"phone.required": "Phone is required",
|
||||
"notes.required": "Notes are required",
|
||||
"date.required": "Date is required",
|
||||
"time.required": "Time is required",
|
||||
"timezone.required": "Timezone is required",
|
||||
}),
|
||||
handleValidationErrorForAPI,
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { name, email, company, phone, notes, date, time, timezone } =
|
||||
req.body;
|
||||
const booking = await db.booking.create({
|
||||
name,
|
||||
email,
|
||||
company,
|
||||
phone,
|
||||
notes,
|
||||
date,
|
||||
time,
|
||||
timezone,
|
||||
});
|
||||
res.status(201).json({ success: true, booking });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// GET /api/bookings - List all bookings
|
||||
router.get("/bookings", async (req, res) => {
|
||||
try {
|
||||
const bookings = await db.booking.findAll({
|
||||
order: [["created_at", "DESC"]],
|
||||
});
|
||||
res.json({ bookings });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
+134
-4
@@ -1,9 +1,139 @@
|
||||
var express = require('express');
|
||||
var express = require("express");
|
||||
var router = express.Router();
|
||||
const moment = require("moment-timezone");
|
||||
|
||||
/* GET home page. */
|
||||
router.get('/', function(req, res, next) {
|
||||
res.render('index', { title: 'Express' });
|
||||
// Helper: Group timezones by region for the timezone selection screen
|
||||
function getTimezoneGroups() {
|
||||
const regions = {
|
||||
"USA/CANADA": [
|
||||
"America/Los_Angeles",
|
||||
"America/Denver",
|
||||
"America/New_York",
|
||||
"America/Halifax",
|
||||
],
|
||||
EUROPE: [
|
||||
"Europe/Berlin",
|
||||
"Europe/Helsinki",
|
||||
"Europe/Dublin",
|
||||
"Europe/Samara",
|
||||
],
|
||||
ASIA: ["Asia/Hong_Kong", "Asia/Jakarta", "Asia/Kabul", "Asia/Kathmandu"],
|
||||
"SOUTH AMERICA": [
|
||||
"America/Bogota",
|
||||
"America/Campo_Grande",
|
||||
"America/Caracas",
|
||||
"America/Lima",
|
||||
],
|
||||
};
|
||||
const groups = {};
|
||||
for (const region in regions) {
|
||||
groups[region] = regions[region].map((tz) => ({
|
||||
name: tz,
|
||||
label: tz.replace(/_/g, " ").replace("America/", ""),
|
||||
time_am: moment().tz(tz).format("h:mma"),
|
||||
time_24: moment().tz(tz).format("HH:mm"),
|
||||
}));
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
// Helper: All timezones for dropdown
|
||||
function getAllTimezones() {
|
||||
return moment.tz.names().map((tz) => ({
|
||||
value: tz,
|
||||
label: tz,
|
||||
}));
|
||||
}
|
||||
|
||||
// Helper: Generate week days and slots
|
||||
function getWeekDaysAndSlots(selectedTz, weekOffset = 0) {
|
||||
const weekDays = [];
|
||||
const today = moment()
|
||||
.tz(selectedTz)
|
||||
.add(weekOffset * 7, "days");
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const day = today.clone().add(i, "days");
|
||||
weekDays.push({
|
||||
label: day.format("dddd"),
|
||||
date: day.format("MMMM D"),
|
||||
dateISO: day.format("YYYY-MM-DD"),
|
||||
slots: [
|
||||
"9:00am",
|
||||
"9:15am",
|
||||
"9:30am",
|
||||
"9:45am",
|
||||
"10:00am",
|
||||
"10:15am",
|
||||
"10:30am",
|
||||
"10:45am",
|
||||
"11:00am",
|
||||
"11:15am",
|
||||
"11:30am",
|
||||
"11:45am",
|
||||
"12:00pm",
|
||||
"12:15pm",
|
||||
"12:30pm",
|
||||
"12:45pm",
|
||||
"1:00pm",
|
||||
"1:15pm",
|
||||
],
|
||||
});
|
||||
}
|
||||
return { weekDays, maxSlots: 17 };
|
||||
}
|
||||
|
||||
// Timezone selection screen
|
||||
router.get("/timezone", function (req, res) {
|
||||
res.render("timezone", {
|
||||
timezones: getAllTimezones(),
|
||||
timezoneGroups: getTimezoneGroups(),
|
||||
});
|
||||
});
|
||||
|
||||
// Calendar slot selection screen
|
||||
router.get("/calendar", function (req, res) {
|
||||
const selectedTimezone = req.query.tz || "America/New_York";
|
||||
const weekOffset = parseInt(req.query.week) || 0;
|
||||
const { weekDays, maxSlots } = getWeekDaysAndSlots(
|
||||
selectedTimezone,
|
||||
weekOffset
|
||||
);
|
||||
res.render("calendar", {
|
||||
selectedTimezone,
|
||||
weekDays,
|
||||
maxSlots,
|
||||
showPrevWeek: weekOffset > 0,
|
||||
prevWeek: weekOffset - 1,
|
||||
nextWeek: weekOffset + 1,
|
||||
});
|
||||
});
|
||||
|
||||
// Booking form screen
|
||||
router.get("/book", function (req, res) {
|
||||
const selectedTimezone = req.query.tz || "America/New_York";
|
||||
const selectedDate = req.query.date || "";
|
||||
const selectedTime = req.query.time || "";
|
||||
res.render("booking-form", {
|
||||
selectedTimezone,
|
||||
selectedDate,
|
||||
selectedTime,
|
||||
});
|
||||
});
|
||||
|
||||
// Booking POST (simulate success)
|
||||
router.post("/book", function (req, res) {
|
||||
// Here you would save booking info to DB
|
||||
res.redirect("/success");
|
||||
});
|
||||
|
||||
// Success screen
|
||||
router.get("/success", function (req, res) {
|
||||
res.render("success");
|
||||
});
|
||||
|
||||
// Home page redirect to timezone selection
|
||||
router.get("/", function (req, res) {
|
||||
res.redirect("/timezone");
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<%- include('partials/header') %>
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header">Calendar</div>
|
||||
<div class="calendar-content">
|
||||
<div class="calendar-labels">
|
||||
<div class="calendar-label-main">Pick a date and time</div>
|
||||
<div class="calendar-label-duration">Duration: <span>1 hour</span></div>
|
||||
<div class="calendar-label-timezone">
|
||||
Your timezone: <%= selectedTimezone %>
|
||||
</div>
|
||||
<% if (selectedDate && selectedTime) { %>
|
||||
<div class="calendar-label-selected">
|
||||
<strong>Selected:</strong> <%= selectedDate %> at <%= selectedTime %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<form class="booking-form" id="bookingForm">
|
||||
<input type="hidden" name="date" value="<%= selectedDate %>" />
|
||||
<input type="hidden" name="time" value="<%= selectedTime %>" />
|
||||
<input type="hidden" name="tz" value="<%= selectedTimezone %>" />
|
||||
<div class="form-group">
|
||||
<label for="fullName">Full Name</label>
|
||||
<input type="text" id="fullName" name="name" required />
|
||||
<div class="form-error" id="error-name"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" required />
|
||||
<div class="form-error" id="error-email"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="company">Company</label>
|
||||
<input type="text" id="company" name="company" required />
|
||||
<div class="form-error" id="error-company"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="phone">Phone</label>
|
||||
<input type="tel" id="phone" name="phone" required />
|
||||
<div class="form-error" id="error-phone"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="notes">Your Notes</label>
|
||||
<textarea id="notes" name="notes" required></textarea>
|
||||
<div class="form-error" id="error-notes"></div>
|
||||
</div>
|
||||
<button type="submit" class="form-submit-btn">Done</button>
|
||||
<div class="form-error" id="error-date"></div>
|
||||
<div class="form-error" id="error-time"></div>
|
||||
<div class="form-error" id="error-timezone"></div>
|
||||
<div class="form-error" id="error-general"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('partials/footer') %>
|
||||
<script>
|
||||
document.getElementById("bookingForm").onsubmit = async function (e) {
|
||||
e.preventDefault();
|
||||
// Clear previous errors
|
||||
document
|
||||
.querySelectorAll(".form-error")
|
||||
.forEach((el) => (el.textContent = ""));
|
||||
const form = e.target;
|
||||
const data = {
|
||||
name: form.fullName.value,
|
||||
email: form.email.value,
|
||||
company: form.company.value,
|
||||
phone: form.phone.value,
|
||||
notes: form.notes.value,
|
||||
date: form.date.value,
|
||||
time: form.time.value,
|
||||
timezone: form.tz.value,
|
||||
};
|
||||
try {
|
||||
const res = await fetch("/api/bookings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
window.location.href = "/success";
|
||||
} else if (result.error) {
|
||||
if (typeof result.error === "object") {
|
||||
for (const key in result.error) {
|
||||
const el = document.getElementById("error-" + key);
|
||||
if (el) el.textContent = result.error[key];
|
||||
}
|
||||
} else {
|
||||
document.getElementById("error-general").textContent = result.error;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
document.getElementById("error-general").textContent =
|
||||
"An error occurred. Please try again.";
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,60 @@
|
||||
<%- include('partials/header') %>
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header">Calendar</div>
|
||||
<div class="calendar-content">
|
||||
<div class="calendar-labels">
|
||||
<div class="calendar-label-main">Pick a date and time</div>
|
||||
<div class="calendar-label-duration">Duration: <span>1 hour</span></div>
|
||||
<div class="calendar-label-timezone">
|
||||
Your timezone: <%= selectedTimezone %> <a href="/timezone">(Change)</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-table-wrapper">
|
||||
<table class="calendar-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<% weekDays.forEach(function(day) { %>
|
||||
<th>
|
||||
<div class="calendar-day-label"><%= day.label %></div>
|
||||
<div class="calendar-date-label"><%= day.date %></div>
|
||||
</th>
|
||||
<% }) %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% for (let i = 0; i < maxSlots; i++) { %>
|
||||
<tr>
|
||||
<% weekDays.forEach(function(day) { %>
|
||||
<td>
|
||||
<% if (day.slots[i]) { %>
|
||||
<form action="/book" method="get">
|
||||
<input type="hidden" name="date" value="<%= day.dateISO %>" />
|
||||
<input type="hidden" name="time" value="<%= day.slots[i] %>" />
|
||||
<input
|
||||
type="hidden"
|
||||
name="tz"
|
||||
value="<%= selectedTimezone %>"
|
||||
/>
|
||||
<button class="calendar-slot-btn"><%= day.slots[i] %></button>
|
||||
</form>
|
||||
<% } %>
|
||||
</td>
|
||||
<% }) %>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="calendar-week-nav">
|
||||
<% if (showPrevWeek) { %>
|
||||
<a href="/calendar?tz=<%= selectedTimezone %>&week=<%= prevWeek %>"
|
||||
>Previous Week</a
|
||||
>
|
||||
<% } %>
|
||||
<a href="/calendar?tz=<%= selectedTimezone %>&week=<%= nextWeek %>"
|
||||
>Next Week</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('partials/footer') %>
|
||||
@@ -0,0 +1,10 @@
|
||||
<%- include('partials/header') %>
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header">Error</div>
|
||||
<div class="calendar-content">
|
||||
<h1><%= message %></h1>
|
||||
<h2><%= error.status %></h2>
|
||||
<pre><%= error.stack %></pre>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('partials/footer') %>
|
||||
@@ -1,6 +0,0 @@
|
||||
extends layout
|
||||
|
||||
block content
|
||||
h1= message
|
||||
h2= error.status
|
||||
pre #{error.stack}
|
||||
@@ -0,0 +1,8 @@
|
||||
<%- include('partials/header') %>
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header"><%= title %></div>
|
||||
<div class="calendar-content">
|
||||
<p>Welcome to <%= title %></p>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('partials/footer') %>
|
||||
@@ -1,5 +0,0 @@
|
||||
extends layout
|
||||
|
||||
block content
|
||||
h1= title
|
||||
p Welcome to #{title}
|
||||
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= title %></title>
|
||||
<link rel="stylesheet" href="/stylesheets/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<%- body %>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,7 +0,0 @@
|
||||
doctype html
|
||||
html
|
||||
head
|
||||
title= title
|
||||
link(rel='stylesheet', href='/stylesheets/style.css')
|
||||
body
|
||||
block content
|
||||
@@ -0,0 +1,2 @@
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Calendar</title>
|
||||
<link rel="stylesheet" href="/stylesheets/style.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
<%- include('partials/header') %>
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header">Calendar</div>
|
||||
<div class="calendar-content success-message">
|
||||
Thanks for filling in the form. You will be emailed next steps.
|
||||
</div>
|
||||
</div>
|
||||
<%- include('partials/footer') %>
|
||||
@@ -0,0 +1,78 @@
|
||||
<%- include('partials/header') %>
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header">Calendar</div>
|
||||
<div class="calendar-content">
|
||||
<div class="calendar-labels">
|
||||
<div class="calendar-label-main">Pick a date and time</div>
|
||||
<div class="calendar-label-duration">Duration: <span>1 hour</span></div>
|
||||
<div class="calendar-label-timezone">
|
||||
Your timezone:
|
||||
<button id="select-timezone-btn" class="timezone-btn">
|
||||
Please Select
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Modal Overlay -->
|
||||
<div id="timezone-modal" class="modal-overlay" style="display: none">
|
||||
<div class="modal-content">
|
||||
<div class="modal-title">TIME ZONE</div>
|
||||
<div class="modal-format-switch">
|
||||
<label
|
||||
><input type="radio" name="format" value="ampm" checked />
|
||||
am/pm</label
|
||||
>
|
||||
<label><input type="radio" name="format" value="24hr" /> 24hr</label>
|
||||
</div>
|
||||
<div class="timezone-groups">
|
||||
<% for (const group in timezoneGroups) { %>
|
||||
<div class="timezone-group">
|
||||
<div class="timezone-group-title"><%= group %></div>
|
||||
<% timezoneGroups[group].forEach(function(tz) { %>
|
||||
<label class="timezone-option">
|
||||
<input type="radio" name="timezone" value="<%= tz.name %>" />
|
||||
<span
|
||||
class="tz-time"
|
||||
data-am="<%= tz.time_am %>"
|
||||
data-24="<%= tz.time_24 %>"
|
||||
>
|
||||
<%= tz.label %>
|
||||
<span class="tz-time-value"><%= tz.time_am %></span>
|
||||
</span>
|
||||
</label>
|
||||
<% }) %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('partials/footer') %>
|
||||
<script>
|
||||
// Modal logic
|
||||
const btn = document.getElementById("select-timezone-btn");
|
||||
const modal = document.getElementById("timezone-modal");
|
||||
btn.onclick = () => {
|
||||
modal.style.display = "flex";
|
||||
};
|
||||
modal.onclick = (e) => {
|
||||
if (e.target === modal) modal.style.display = "none";
|
||||
};
|
||||
document.querySelectorAll('input[name="timezone"]').forEach((el) => {
|
||||
el.onclick = () => {
|
||||
window.location.href = "/calendar?tz=" + encodeURIComponent(el.value);
|
||||
};
|
||||
});
|
||||
|
||||
// Time format toggle logic
|
||||
document.querySelectorAll('input[name="format"]').forEach((el) => {
|
||||
el.onchange = function () {
|
||||
const is24 = this.value === "24hr";
|
||||
document.querySelectorAll(".tz-time").forEach((span) => {
|
||||
const am = span.getAttribute("data-am");
|
||||
const t24 = span.getAttribute("data-24");
|
||||
span.querySelector(".tz-time-value").textContent = is24 ? t24 : am;
|
||||
});
|
||||
};
|
||||
});
|
||||
</script>
|
||||
+17
-15
@@ -1,11 +1,12 @@
|
||||
var createError = require('http-errors');
|
||||
var express = require('express');
|
||||
var path = require('path');
|
||||
var cookieParser = require('cookie-parser');
|
||||
var logger = require('morgan');
|
||||
var createError = require("http-errors");
|
||||
var express = require("express");
|
||||
var path = require("path");
|
||||
var cookieParser = require("cookie-parser");
|
||||
var logger = require("morgan");
|
||||
|
||||
var indexRouter = require('./routes/index');
|
||||
var usersRouter = require('./routes/users');
|
||||
var indexRouter = require("./routes/index");
|
||||
var usersRouter = require("./routes/users");
|
||||
var scheduleRouter = require("./routes/schedule");
|
||||
|
||||
const db = require("./models");
|
||||
var cors = require("cors");
|
||||
@@ -13,17 +14,18 @@ var cors = require("cors");
|
||||
var app = express();
|
||||
app.set("db", db);
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'jade');
|
||||
app.set("views", path.join(__dirname, "views"));
|
||||
app.set("view engine", "jade");
|
||||
app.use(cors());
|
||||
app.use(logger('dev'));
|
||||
app.use(logger("dev"));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
|
||||
app.use('/', indexRouter);
|
||||
app.use('/users', usersRouter);
|
||||
app.use("/", indexRouter);
|
||||
app.use("/users", usersRouter);
|
||||
app.use("/api/v1", scheduleRouter);
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
app.use(function (req, res, next) {
|
||||
@@ -34,11 +36,11 @@ app.use(function (req, res, next) {
|
||||
app.use(function (err, req, res, next) {
|
||||
// set locals, only providing error in development
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
res.locals.error = req.app.get("env") === "development" ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
res.render("error");
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const availability = sequelize.define(
|
||||
"availability",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
day_of_week: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
start_time: {
|
||||
type: DataTypes.TIME,
|
||||
allowNull: false,
|
||||
},
|
||||
end_time: {
|
||||
type: DataTypes.TIME,
|
||||
allowNull: false,
|
||||
},
|
||||
created_at: DataTypes.DATE,
|
||||
updated_at: DataTypes.DATE,
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
freezeTableName: true,
|
||||
tableName: "availability",
|
||||
}
|
||||
);
|
||||
return availability;
|
||||
};
|
||||
+30
-16
@@ -1,4 +1,4 @@
|
||||
'use strict';
|
||||
"use strict";
|
||||
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
||||
/**
|
||||
* Sequelize File
|
||||
@@ -8,24 +8,28 @@
|
||||
* @author Ryan Wong
|
||||
*
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let Sequelize = require('sequelize');
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
let Sequelize = require("sequelize");
|
||||
const basename = path.basename(__filename);
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { DataTypes } = require("sequelize");
|
||||
const config = {
|
||||
DB_DATABASE: 'mysql',
|
||||
DB_USERNAME: 'root',
|
||||
DB_PASSWORD: 'root',
|
||||
DB_ADAPTER: 'mysql',
|
||||
DB_NAME: 'day_1',
|
||||
DB_HOSTNAME: 'localhost',
|
||||
DB_DATABASE: "mysql",
|
||||
DB_USERNAME: "root",
|
||||
DB_PASSWORD: process.env.DB_PASSWORD || "root",
|
||||
DB_ADAPTER: "mysql",
|
||||
DB_NAME: "day_19",
|
||||
DB_HOSTNAME: "localhost",
|
||||
DB_PORT: 3306,
|
||||
};
|
||||
|
||||
let db = {};
|
||||
|
||||
let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, {
|
||||
let sequelize = new Sequelize(
|
||||
config.DB_NAME,
|
||||
config.DB_USERNAME,
|
||||
config.DB_PASSWORD,
|
||||
{
|
||||
dialect: config.DB_ADAPTER,
|
||||
username: config.DB_USERNAME,
|
||||
password: config.DB_PASSWORD,
|
||||
@@ -33,7 +37,7 @@ let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_
|
||||
host: config.DB_HOSTNAME,
|
||||
port: config.DB_PORT,
|
||||
logging: console.log,
|
||||
timezone: '-04:00',
|
||||
timezone: "-04:00",
|
||||
pool: {
|
||||
maxConnections: 1,
|
||||
minConnections: 0,
|
||||
@@ -44,13 +48,23 @@ let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_
|
||||
underscoredAll: true,
|
||||
underscored: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// sequelize.sync({ force: true });
|
||||
sequelize
|
||||
.sync()
|
||||
.then(() => {
|
||||
console.log("Database & tables created!");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
fs.readdirSync(__dirname)
|
||||
.filter((file) => {
|
||||
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
|
||||
return (
|
||||
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
|
||||
);
|
||||
})
|
||||
.forEach((file) => {
|
||||
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const schedule = sequelize.define(
|
||||
"schedule",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
start_time: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
end_time: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
created_at: DataTypes.DATE,
|
||||
updated_at: DataTypes.DATE,
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
freezeTableName: true,
|
||||
tableName: "schedule",
|
||||
}
|
||||
);
|
||||
return schedule;
|
||||
};
|
||||
+2
-1
@@ -3,7 +3,8 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node ./bin/www"
|
||||
"start": "node ./bin/www",
|
||||
"dev": "node --watch --env-file=.env ./bin/www"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie-parser": "~1.4.4",
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const db = require("../models");
|
||||
const { Op } = require("sequelize");
|
||||
|
||||
// POST /api/v1/schedule - Save an appointment
|
||||
router.post("/schedule", async (req, res) => {
|
||||
try {
|
||||
const { user_id, start_time, end_time } = req.body;
|
||||
if (!start_time || !end_time) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({
|
||||
success: false,
|
||||
error: "start_time and end_time are required",
|
||||
});
|
||||
}
|
||||
const appointment = await db.schedule.create({
|
||||
user_id,
|
||||
start_time,
|
||||
end_time,
|
||||
});
|
||||
res.json({ success: true, data: appointment });
|
||||
} catch (err) {
|
||||
res.status(500).json({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/v1/booked - Return available times excluding scheduled
|
||||
router.get("/booked", async (req, res) => {
|
||||
try {
|
||||
// For demo: get all availability, then exclude times that overlap with schedule
|
||||
const availabilities = await db.availability.findAll();
|
||||
const schedules = await db.schedule.findAll();
|
||||
|
||||
// This is a simple exclusion, not a full calendar logic
|
||||
// For each availability, exclude if any schedule overlaps
|
||||
const availableTimes = availabilities.filter((avail) => {
|
||||
const availStart = avail.start_time;
|
||||
const availEnd = avail.end_time;
|
||||
const overlap = schedules.some((sch) => {
|
||||
return sch.start_time < availEnd && sch.end_time > availStart;
|
||||
});
|
||||
return !overlap;
|
||||
});
|
||||
res.json({ success: true, data: availableTimes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dev-dist
|
||||
|
||||
*.local
|
||||
release
|
||||
config.php
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,7 @@
|
||||
# Day 20
|
||||
|
||||
Read:
|
||||
|
||||
- https://www.notion.so/How-to-Use-Baas-00f549dda3a84dc48b352c79222f1a3a
|
||||
- https://www.notion.so/Create-Manage-Projects-With-Wireframe-Tool-df67b882f0c14735a0192d69dc3ff777
|
||||
|
||||
@@ -27,4 +28,3 @@ Read:
|
||||
10. Clone backend repo on src/backend/custom
|
||||
|
||||
11. Write APIs, and test locally.
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href="/src/favicon.svg"
|
||||
/>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
/>
|
||||
<title>inventorylynx</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="portal"></div>
|
||||
|
||||
<script
|
||||
type="module"
|
||||
src="/src/index.jsx"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,17 @@
|
||||
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"Components/*": ["src/components/*"],
|
||||
"Pages/*": ["src/pages/*"],
|
||||
"Utils/*": ["src/utils/*"],
|
||||
"Assets/*": ["src/assets/*"],
|
||||
"Context/*": ["src/context/*"],
|
||||
"Routes/*": ["src/routes/*"],
|
||||
"Hooks/*": ["src/hooks/*"],
|
||||
"Src/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
|
||||
{
|
||||
"name": "adminportal",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"tw": "npx tailwindcss -i ./src/index.css -o ./src/output.css --watch",
|
||||
"build": "vite build",
|
||||
"commit": "git add . && git commit -m \"Update\" && git pull && git push",
|
||||
"commit:script": "node git-script add,commit,pull,push message=\"Update | API SECTION RESTRUCTURE IN PROGRESS\" origin=wireframe",
|
||||
"preview": "vite preview",
|
||||
"generate-pwa-assets": "pwa-assets-generator --preset minimal public/mkd_logo.png"
|
||||
},
|
||||
"dependencies": {
|
||||
"@craftjs/core": "^0.2.0-beta.11",
|
||||
"@editorjs/attaches": "^1.3.0",
|
||||
"@editorjs/checklist": "^1.5.0",
|
||||
"@editorjs/code": "^2.8.0",
|
||||
"@editorjs/delimiter": "^1.3.0",
|
||||
"@editorjs/editorjs": "^2.26.5",
|
||||
"@editorjs/embed": "^2.5.3",
|
||||
"@editorjs/header": "^2.7.0",
|
||||
"@editorjs/image": "^2.8.1",
|
||||
"@editorjs/inline-code": "^1.4.0",
|
||||
"@editorjs/link": "^2.5.0",
|
||||
"@editorjs/list": "^1.8.0",
|
||||
"@editorjs/marker": "^1.3.0",
|
||||
"@editorjs/nested-list": "^1.3.0",
|
||||
"@editorjs/paragraph": "^2.9.0",
|
||||
"@editorjs/personality": "^2.0.2",
|
||||
"@editorjs/quote": "^2.5.0",
|
||||
"@editorjs/raw": "^2.4.0",
|
||||
"@editorjs/simple-image": "^1.5.1",
|
||||
"@editorjs/table": "^2.2.1",
|
||||
"@editorjs/underline": "^1.1.0",
|
||||
"@editorjs/warning": "^1.3.0",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/serialize": "^1.1.2",
|
||||
"@emotion/utils": "^1.2.1",
|
||||
"@fontsource/inter": "^5.0.15",
|
||||
"@fontsource/poppins": "^4.5.10",
|
||||
"@fontsource/roboto-mono": "^5.0.16",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@fullcalendar/core": "^5.11.3",
|
||||
"@fullcalendar/daygrid": "^5.11.3",
|
||||
"@fullcalendar/interaction": "^5.11.3",
|
||||
"@fullcalendar/list": "^5.11.3",
|
||||
"@fullcalendar/react": "^5.11.2",
|
||||
"@fullcalendar/timegrid": "^5.11.3",
|
||||
"@headlessui/react": "^1.7.14",
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@legendapp/state": "^0.23.4",
|
||||
"@mantine/core": "^6.0.19",
|
||||
"@mantine/hooks": "^6.0.19",
|
||||
"@react-google-maps/api": "^2.19.2",
|
||||
"@react-pdf-viewer/core": "^3.12.0",
|
||||
"@splidejs/react-splide": "^0.7.12",
|
||||
"@stripe/react-stripe-js": "^2.1.0",
|
||||
"@stripe/stripe-js": "^1.52.1",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@uppy/audio": "^1.1.1",
|
||||
"@uppy/aws-s3": "^3.2.1",
|
||||
"@uppy/aws-s3-multipart": "^3.4.1",
|
||||
"@uppy/compressor": "^1.0.2",
|
||||
"@uppy/core": "^3.7.1",
|
||||
"@uppy/dashboard": "^3.4.1",
|
||||
"@uppy/drag-drop": "^3.0.2",
|
||||
"@uppy/drop-target": "^2.0.1",
|
||||
"@uppy/dropbox": "^3.1.1",
|
||||
"@uppy/facebook": "^3.1.3",
|
||||
"@uppy/file-input": "^3.0.3",
|
||||
"@uppy/golden-retriever": "^3.1.0",
|
||||
"@uppy/google-drive": "^3.1.1",
|
||||
"@uppy/image-editor": "^2.1.2",
|
||||
"@uppy/instagram": "^3.1.3",
|
||||
"@uppy/onedrive": "^3.1.1",
|
||||
"@uppy/progress-bar": "^3.0.3",
|
||||
"@uppy/react": "^3.1.2",
|
||||
"@uppy/remote-sources": "^1.0.3",
|
||||
"@uppy/screen-capture": "^3.1.1",
|
||||
"@uppy/tus": "^3.4.0",
|
||||
"@uppy/webcam": "^3.3.1",
|
||||
"@uppy/xhr-upload": "^3.5.0",
|
||||
"apexcharts": "^3.40.0",
|
||||
"axios": "^1.5.0",
|
||||
"bootstrap": "^5.2.3",
|
||||
"codemirror": "^5.65.11",
|
||||
"codemirror-console": "^3.0.4",
|
||||
"codemirror-console-ui": "^3.0.4",
|
||||
"emoji-picker-textarea": "^1.0.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"framer-motion": "^10.16.4",
|
||||
"fullcalendar": "^5.11.3",
|
||||
"html-to-image": "^1.11.11",
|
||||
"jodit-react": "^1.3.39",
|
||||
"jszip": "^3.10.1",
|
||||
"moment": "^2.29.4",
|
||||
"nanoid": "^4.0.2",
|
||||
"openai": "^4.24.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"pdfjs-dist": "^3.4.120",
|
||||
"pluralize": "^8.0.0",
|
||||
"pretty-rating-react": "^2.2.0",
|
||||
"qr-scanner": "^1.4.2",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
"react-addons-update": "^15.6.3",
|
||||
"react-apexcharts": "^1.4.0",
|
||||
"react-calendar": "^4.2.1",
|
||||
"react-codemirror2": "^7.2.1",
|
||||
"react-confirm-alert": "^3.0.6",
|
||||
"react-contenteditable": "^3.3.7",
|
||||
"react-dnd": "^10.0.2",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-google-autocomplete": "^2.7.3",
|
||||
"react-google-maps": "^9.4.5",
|
||||
"react-hook-form": "^7.46.1",
|
||||
"react-icons": "^4.11.0",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
"react-input-emoji": "^5.4.1",
|
||||
"react-loading-skeleton": "^3.3.1",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-outside-click-handler": "^1.3.0",
|
||||
"react-pdf": "^7.6.0",
|
||||
"react-qr-reader": "^2.2.1",
|
||||
"react-quill": "^2.0.0",
|
||||
"react-ratings-declarative": "^3.4.1",
|
||||
"react-router": "^6.15.0",
|
||||
"react-router-dom": "^6.11.1",
|
||||
"react-select": "^5.8.0",
|
||||
"react-slick": "^0.29.0",
|
||||
"react-spinners": "^0.13.8",
|
||||
"react-timeago": "^7.2.0",
|
||||
"react-toggle": "^4.1.3",
|
||||
"redux": "^4.2.1",
|
||||
"slick-carousel": "^1.8.1",
|
||||
"swiper": "^9.3.1",
|
||||
"tw-elements": "^1.0.0-beta2",
|
||||
"twilio-video": "^2.27.0",
|
||||
"uppy": "^3.20.0",
|
||||
"use-debounce": "^9.0.4",
|
||||
"xlsx": "^0.18.5",
|
||||
"yup": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@editorjs/link-autocomplete": "^0.1.0",
|
||||
"@editorjs/opensea": "^1.0.2",
|
||||
"@editorjs/translate-inline": "^1.0.0-rc.0",
|
||||
"@types/react": "^18.2.6",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"@vite-pwa/assets-generator": "^0.0.8",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"@vitejs/plugin-react-refresh": "^1.3.6",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.40.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^13.2.2",
|
||||
"postcss": "^8.4.23",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-tailwindcss": "^0.2.8",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"vite": "^4.3.5",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-pwa": "^0.16.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
import React from "react";
|
||||
import {AuthProvider} from "Context/Auth";
|
||||
import {GlobalProvider} from "Context/Global";
|
||||
import Main from "./routes/Routes";
|
||||
import "@uppy/core/dist/style.css";
|
||||
import "@uppy/dashboard/dist/style.css";
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
import "react-loading-skeleton/dist/skeleton.css";
|
||||
import { loadStripe } from "@stripe/stripe-js";
|
||||
import { Elements } from "@stripe/react-stripe-js";
|
||||
|
||||
|
||||
const stripePromise = loadStripe("pk_test_51Ll5ukBgOlWo0lDUrBhA2W7EX2MwUH9AR5Y3KQoujf7PTQagZAJylWP1UOFbtH4UwxoufZbInwehQppWAq53kmNC00UIKSmebO");
|
||||
|
||||
function App() {
|
||||
return (
|
||||
|
||||
<AuthProvider>
|
||||
<GlobalProvider>
|
||||
<Router>
|
||||
<Elements stripe={stripePromise}>
|
||||
<Main />
|
||||
</Elements>
|
||||
</Router>
|
||||
</GlobalProvider>
|
||||
</AuthProvider>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
// export { default as LoginBg } from "./login-bg.jpg";
|
||||
export { default as LoginBgNew } from "./login-new-bg.png";
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 669 KiB |
@@ -0,0 +1,18 @@
|
||||
|
||||
<svg width="410" height="404" viewBox="0 0 410 404" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z" fill="url(#paint0_linear)"/>
|
||||
<path d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z" fill="url(#paint1_linear)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="6.00017" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#41D1FF"/>
|
||||
<stop offset="1" stop-color="#BD34FE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="194.651" y1="8.81818" x2="236.076" y2="292.989" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFEA83"/>
|
||||
<stop offset="0.0833333" stop-color="#FFDD35"/>
|
||||
<stop offset="1" stop-color="#FFA800"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,24 @@
|
||||
|
||||
|
||||
import React from "react";
|
||||
|
||||
export const CaretLeft = ({ className = "" }) => {
|
||||
return (
|
||||
<svg
|
||||
className={`${className}`}
|
||||
width="8"
|
||||
height="14"
|
||||
viewBox="0 0 8 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7 13L1 7L7 1"
|
||||
stroke="black"
|
||||
stroke-width="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
import React from "react";
|
||||
|
||||
export const CloseIcon = ({ className = "" }) => {
|
||||
return (
|
||||
<svg className={`${className}`} width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M19.4059 16.6337C20.198 17.4257 20.198 18.6139 19.4059 19.4059C19.0099 19.802 18.5149 20 18.0198 20C17.5248 20 17.0297 19.802 16.6337 19.4059L10 12.7723L3.36634 19.4059C2.9703 19.802 2.47525 20 1.9802 20C1.48515 20 0.990099 19.802 0.594059 19.4059C-0.19802 18.6139 -0.19802 17.4257 0.594059 16.6337L7.22772 10L0.594059 3.36634C-0.19802 2.57426 -0.19802 1.38614 0.594059 0.594059C1.38614 -0.19802 2.57426 -0.19802 3.36634 0.594059L10 7.22772L16.6337 0.594059C17.4257 -0.19802 18.6139 -0.19802 19.4059 0.594059C20.198 1.38614 20.198 2.57426 19.4059 3.36634L12.7723 10L19.4059 16.6337Z"
|
||||
fill="#636363"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
import React from 'react'
|
||||
|
||||
export const DangerIcon = ( { className } ) => {
|
||||
return (
|
||||
<svg className={ `${ className }` } width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M40 80C62.1333 80 80 62.1333 80 40C80 17.8667 62.1333 0 40 0C17.8667 0 0 17.8667 0 40C0 62.1333 17.8667 80 40 80ZM36.1932 46.9993V18.1818H43.9169V46.9993H36.1932ZM44.3697 54.4567C44.3697 55.6108 43.9879 56.5607 43.2244 57.3065C42.4432 58.0522 41.3867 58.4251 40.055 58.4251C38.7411 58.4251 37.6935 58.0522 36.9123 57.3065C36.131 56.5607 35.7404 55.6108 35.7404 54.4567C35.7404 53.2848 36.1399 52.326 36.9389 51.5803C37.7202 50.8345 38.7589 50.4616 40.055 50.4616C41.3512 50.4616 42.3988 50.8345 43.1978 51.5803C43.979 52.326 44.3697 53.2848 44.3697 54.4567Z" fill="#CF2A2A" />
|
||||
</svg>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
|
||||
import React, { useId } from "react";
|
||||
import MoonLoader from "react-spinners/MoonLoader";
|
||||
|
||||
const override = {
|
||||
borderColor: "red",
|
||||
};
|
||||
|
||||
export const Spinner = ({ size = 20, color = "#ffffff" }) => {
|
||||
const id = useId();
|
||||
return (
|
||||
<MoonLoader
|
||||
color={color}
|
||||
loading={true}
|
||||
cssOverride={override}
|
||||
size={size}
|
||||
// aria-label="Loading Spinner"
|
||||
data-testid={id}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
import { lazy } from "react";
|
||||
|
||||
export const CloseIcon = lazy(() => import("./CloseIcon").then((module) => ({ default: module.CloseIcon })));
|
||||
export const DangerIcon = lazy(() => import("./DangerIcon").then((module) => ({ default: module.DangerIcon })));
|
||||
export const Spinner = lazy(() => import("./Spinner").then((module) => ({ default: module.Spinner })));
|
||||
export const CaretLeft = lazy(() => import("./CaretLeft").then((module) => ({ default: module.CaretLeft })));
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
|
||||
|
||||
import React, { useState } from "react";
|
||||
import classes from "./AddButton.module.css";
|
||||
|
||||
const AddButton = ({
|
||||
onClick,
|
||||
children = "Add New",
|
||||
showPlus = true,
|
||||
className,
|
||||
showChildren = true,
|
||||
}) => {
|
||||
const [animate, setAnimate] = useState(false);
|
||||
|
||||
const onClickHandle = () => {
|
||||
if (onClick) {
|
||||
onClick();
|
||||
}
|
||||
setAnimate(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onAnimationEnd={() => setAnimate(false)}
|
||||
onClick={onClickHandle}
|
||||
className={`${animate && "animate-wiggle"} ${
|
||||
classes.button
|
||||
} relative flex h-[2.125rem] w-fit min-w-fit items-center justify-center overflow-hidden rounded-md border border-primaryBlue bg-indigo-600 px-[.6125rem] py-[.5625rem] text-sm font-medium leading-none text-white shadow-md shadow-indigo-600 ${className}`}
|
||||
>
|
||||
{showPlus ? "+" : null} {showChildren ? children : null}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddButton;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
.button {
|
||||
position: relative;
|
||||
border: none;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
-webkit-transition-duration: 0.4s; /* Safari */
|
||||
transition-duration: 0.4s;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
.button:after {
|
||||
content: "";
|
||||
background-color: #6752e0;
|
||||
/* background: #4f46e5; */
|
||||
display: block;
|
||||
position: absolute;
|
||||
padding-top: 300%;
|
||||
padding-left: 350%;
|
||||
margin-left: -20px !important;
|
||||
margin-top: -120%;
|
||||
opacity: 0;
|
||||
transition: all 0.8s;
|
||||
}
|
||||
|
||||
.button:active:after {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
opacity: 1;
|
||||
transition: 0s;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
export {default as AddButton } from "./AddButton";
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const AddTags = ({tags, tag, setTagData}) => {
|
||||
// console.log('addtag--->',{tags, tag, setTagData})
|
||||
return (
|
||||
<li className="px-2 py-1 text-sm inline-flex items-center gap-2 rounded bg-blue-500 text-white"><span>{tag}</span> <button onClick={() => setTagData(tags.filter(tags=> tags.name !== tag))} className="flex items-center justify-center h-5 w-5 rounded-full bg-blue-100 hover:bg-blue-200 duration-300"><span className="leading-0 -mt-1 text-black">×</span></button></li>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddTags;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user