Compare commits

...

8 Commits

Author SHA1 Message Date
Ayobami b0d86465f2 feat: complete day 20 react task 2025-07-23 23:25:19 +01:00
Ayobami 743187b216 feat: complete day 17 2025-07-22 19:21:32 +01:00
Ayobami 29e6eb82c7 feat: complete day 19 2025-07-22 17:49:50 +01:00
Ayobami dc35cfcb3f feat: complete day 16 2025-07-21 22:23:26 +01:00
Ayobami cbbb0ed4c4 feat: complete day 15 2025-07-18 21:34:29 +01:00
Ayobami 9113ba4c74 feat: complete day 14 2025-07-18 20:12:09 +01:00
Ayobami 7d9350c3df feat: complete day 13 2025-07-17 21:55:47 +01:00
Ayobami 9c84737fed feat: complete day 11 2025-07-17 16:55:13 +01:00
337 changed files with 45687 additions and 392 deletions
+94 -94
View File
@@ -1,4 +1,4 @@
'use strict' "use strict";
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/ /*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
/** /**
* App * App
@@ -8,52 +8,52 @@
* @author Ryan Wong * @author Ryan Wong
* *
*/ */
require('dotenv').config() require("dotenv").config();
const express = require('express') const express = require("express");
const fs = require('fs') const fs = require("fs");
const path = require('path') const path = require("path");
const logger = require('morgan') const logger = require("morgan");
const helmet = require('helmet') const helmet = require("helmet");
const cookieParser = require('cookie-parser') const cookieParser = require("cookie-parser");
const cors = require('cors') const cors = require("cors");
const { ApolloServer } = require('apollo-server-express') const { ApolloServer } = require("apollo-server-express");
const { graphqlUploadExpress } = require('graphql-upload') const { graphqlUploadExpress } = require("graphql-upload");
const body_parser = require('body-parser') const body_parser = require("body-parser");
const db = require('./models') const db = require("./models");
const typeDefs = fs.readFileSync( const typeDefs = fs.readFileSync(
path.join(__dirname, '/types/schema.graphql'), path.join(__dirname, "/types/schema.graphql"),
'utf8' "utf8"
) );
const jwtService = require('./services/JwtService') const jwtService = require("./services/JwtService");
const resolvers = require('./resolvers') const resolvers = require("./resolvers");
const schemaDirectives = require('./directives') const schemaDirectives = require("./directives");
const { AuthenticationError } = require('./services/ErrorService') const { AuthenticationError } = require("./services/ErrorService");
const { errorCodes } = require('./core/strings') const { errorCodes } = require("./core/strings");
const { formatGraphqlError } = require('./utils/formatError') const { formatGraphqlError } = require("./utils/formatError");
const GRAPHQL_PATH = '/graphql' const GRAPHQL_PATH = "/graphql";
const ALLOWED_ROLE_IDS = [2] const ALLOWED_ROLE_IDS = [2];
let app = express() let app = express();
app.use(logger('dev')) app.use(logger("dev"));
if (process.env.MODE === 'development') { if (process.env.MODE === "development") {
logger.token('graphql-query', (req) => { logger.token("graphql-query", (req) => {
const disallowedLogs = ['IntrospectionQuery'] const disallowedLogs = ["IntrospectionQuery"];
if (req.method === 'POST' && req.originalUrl === GRAPHQL_PATH) { if (req.method === "POST" && req.originalUrl === GRAPHQL_PATH) {
const { query, variables, operationName } = req.body const { query, variables, operationName } = req.body;
return !disallowedLogs.includes(operationName) return !disallowedLogs.includes(operationName)
? `GRAPHQL: \nOperation Name: ${operationName} \nQuery: ${query} \nVariables: ${JSON.stringify( ? `GRAPHQL: \nOperation Name: ${operationName} \nQuery: ${query} \nVariables: ${JSON.stringify(
variables variables
)}` )}`
: '' : "";
} }
return '' return "";
}) });
app.use(logger(':graphql-query')) app.use(logger(":graphql-query"));
} }
const server = new ApolloServer({ const server = new ApolloServer({
@@ -62,97 +62,97 @@ const server = new ApolloServer({
resolvers, resolvers,
schemaDirectives, schemaDirectives,
context: async ({ req }) => { context: async ({ req }) => {
const token = req.headers.authorization // const token = req.headers.authorization
if (!token) { // if (!token) {
throw new AuthenticationError( // throw new AuthenticationError(
'Invalid token', // 'Invalid token',
errorCodes.token.INVALID_TOKEN // errorCodes.token.INVALID_TOKEN
) // )
} // }
const cleanToken = token.replace('Bearer ', '') // const cleanToken = token.replace('Bearer ', '')
const verify = jwtService.verifyAccessToken(cleanToken) // const verify = jwtService.verifyAccessToken(cleanToken)
const roleId = verify?.role_id // const roleId = verify?.role_id
const user = verify?.user // const user = verify?.user
const credentialId = verify?.credential_id // const credentialId = verify?.credential_id
if (!verify || !roleId || !user || !credentialId) { // if (!verify || !roleId || !user || !credentialId) {
throw new AuthenticationError( // throw new AuthenticationError(
'Invalid token', // 'Invalid token',
errorCodes.token.INVALID_TOKEN // errorCodes.token.INVALID_TOKEN
) // )
} // }
if (!ALLOWED_ROLE_IDS.includes(+roleId)) { // if (!ALLOWED_ROLE_IDS.includes(+roleId)) {
throw new AuthenticationError( // throw new AuthenticationError(
'Access Denied', // 'Access Denied',
errorCodes.account.UNAUTHORIZED // errorCodes.account.UNAUTHORIZED
) // )
} // }
return { return {
credentialId, credentialId: 1,
user, user: { id: 1, role_id: 1 },
db, db,
role: { role: {
roleId, roleId: 1,
allowedRoleIds: ALLOWED_ROLE_IDS, allowedRoleIds: [1, 2, 3],
// allowedRoleIds: ALLOWED_ROLE_IDS,
}, },
} };
}, },
formatError: formatGraphqlError, formatError: formatGraphqlError,
}) });
if (process.NODE_ENV === 'maintenance') { if (process.NODE_ENV === "maintenance") {
app.all('*', (req, res) => { app.all("*", (req, res) => {
res.status(503).json({ message: 'website under maintenance' }) res.status(503).json({ message: "website under maintenance" });
}) });
} }
app.set('iocContainer', process.env) app.set("iocContainer", process.env);
app.set('db', db) app.set("db", db);
app.use(body_parser.json({ limit: '50mb' })) app.use(body_parser.json({ limit: "50mb" }));
app.use(express.json()) app.use(express.json());
app.use( app.use(
express.urlencoded({ express.urlencoded({
extended: false, extended: false,
}) })
) );
app.use(cors()) app.use(cors());
app.set('view engine', 'eta') app.set("view engine", "eta");
app.set('views', path.join(__dirname, '/views')) app.set("views", path.join(__dirname, "/views"));
app.use(cookieParser()) app.use(cookieParser());
app.use(helmet()) app.use(helmet());
app.use(express.static(path.join(__dirname, '/public'))) app.use(express.static(path.join(__dirname, "/public")));
app.use(express.static(path.join(__dirname, '/uploads'))) app.use(express.static(path.join(__dirname, "/uploads")));
app.use(express.static(path.join(__dirname))); app.use(express.static(path.join(__dirname)));
app.use(graphqlUploadExpress({ maxFileSize: 1000000000, maxFiles: 10 })) app.use(graphqlUploadExpress({ maxFileSize: 1000000000, maxFiles: 10 }));
server.applyMiddleware({ app, path: GRAPHQL_PATH })
server.applyMiddleware({ app, path: GRAPHQL_PATH });
app.use((err, req, res, next) => { app.use((err, req, res, next) => {
res.locals.message = err.message 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 // render the error page
res.status(err.status || 500) res.status(err.status || 500);
res.json({ res.json({
message: err.message, message: err.message,
}) });
}) });
app.use((_, res, next) => { app.use((_, res, next) => {
return res return res
.status(400) .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 = { module.exports = {
app, app,
apollo: server, apollo: server,
} };
+34
View File
@@ -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;
};
+32
View File
@@ -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;
};
+34
View File
@@ -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;
};
+38
View File
@@ -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;
};
+44 -34
View File
@@ -1,4 +1,4 @@
'use strict'; "use strict";
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/ /*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
/** /**
* Sequelize File * Sequelize File
@@ -8,49 +8,59 @@
* @author Ryan Wong * @author Ryan Wong
* *
*/ */
const fs = require('fs'); const fs = require("fs");
const path = require('path'); const path = require("path");
let Sequelize = require('sequelize'); let Sequelize = require("sequelize");
const { DataTypes } = require('sequelize'); const { DataTypes } = require("sequelize");
const basename = path.basename(__filename); const basename = path.basename(__filename);
const config = { const config = {
DB_DATABASE: 'mysql', DB_DATABASE: "mysql",
DB_USERNAME: 'root', DB_USERNAME: "root",
DB_PASSWORD: 'root', DB_PASSWORD: process.env.DB_PASSWORD || "root",
DB_ADAPTER: 'mysql', DB_ADAPTER: "mysql",
DB_NAME: 'day_1', DB_NAME: "day_11",
DB_HOSTNAME: 'localhost', DB_HOSTNAME: "localhost",
DB_PORT: 3306, DB_PORT: 3306,
}; };
let db = {}; let db = {};
let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, { let sequelize = new Sequelize(
dialect: config.DB_ADAPTER, config.DB_NAME,
username: config.DB_USERNAME, config.DB_USERNAME,
password: config.DB_PASSWORD, config.DB_PASSWORD,
database: config.DB_NAME, {
host: config.DB_HOSTNAME, dialect: config.DB_ADAPTER,
port: config.DB_PORT, username: config.DB_USERNAME,
logging: console.log, password: config.DB_PASSWORD,
timezone: '-04:00', database: config.DB_NAME,
pool: { host: config.DB_HOSTNAME,
maxConnections: 1, port: config.DB_PORT,
minConnections: 0, logging: console.log,
maxIdleTime: 100, timezone: "-04:00",
}, pool: {
define: { maxConnections: 1,
timestamps: false, minConnections: 0,
underscoredAll: true, maxIdleTime: 100,
underscored: true, },
}, define: {
}); timestamps: false,
underscoredAll: true,
underscored: true,
},
}
);
// sequelize.sync({ force: true }); sequelize
.sync()
.then(() => console.log("Tables synced successfully!"))
.catch((err) => console.log(err));
fs.readdirSync(__dirname) fs.readdirSync(__dirname)
.filter((file) => { .filter((file) => {
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js'; return (
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
);
}) })
.forEach((file) => { .forEach((file) => {
var model = require(path.join(__dirname, file))(sequelize, DataTypes); var model = require(path.join(__dirname, file))(sequelize, DataTypes);
@@ -66,4 +76,4 @@ Object.keys(db).forEach((modelName) => {
db.sequelize = sequelize; db.sequelize = sequelize;
db.Sequelize = Sequelize; db.Sequelize = Sequelize;
module.exports = db; module.exports = db;
+55
View File
@@ -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;
};
+38
View File
@@ -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;
};
+33
View File
@@ -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
View File
@@ -2,7 +2,10 @@
"name": "day11", "name": "day11",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"scripts": {}, "scripts": {
"start": "node server.js",
"dev": "node --watch --env-file=.env server.js"
},
"keywords": [], "keywords": [],
"author": "Ryan Wong", "author": "Ryan Wong",
"private": true, "private": true,
+63
View File
@@ -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;
+121
View File
@@ -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;
+59
View File
@@ -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
View File
@@ -7,47 +7,58 @@
* @author Ryan Wong * @author Ryan Wong
* *
*/ */
const { GraphQLUpload } = require('graphql-upload'); const { GraphQLUpload } = require("graphql-upload");
const updateUserResolver = require('./update/updateUser'); const updateUserResolver = require("./update/updateUser");
const singleUserResolver = require('./single/singleUser'); const singleUserResolver = require("./single/singleUser");
const typeUserResolver = require('./type/typeUser'); const typeUserResolver = require("./type/typeUser");
const createLinkResolver = require('./create/createLink'); const createLinkResolver = require("./create/createLink");
const typeLinkResolver = require('./type/typeLink'); const typeLinkResolver = require("./type/typeLink");
const singleLinkResolver = require('./single/singleLink'); const singleLinkResolver = require("./single/singleLink");
const deactivateAllLinksResolver = require('./delete/deactivateAllLinks'); const deactivateAllLinksResolver = require("./delete/deactivateAllLinks");
const calendarResolver = require('./custom/calendar'); // const calendarResolver = require("./custom/calendar");
const noteResolver = require('./custom/note'); // const noteResolver = require("./custom/note");
const customImageResolver = require('./custom/image'); // const customImageResolver = require("./custom/image");
const uploadFileMutationResolver = require('./custom/uploadFile'); // const uploadFileMutationResolver = require("./custom/uploadFile");
const connectionStepsResolver = require('./custom/connectionSteps');
// 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 = { module.exports = {
Upload: GraphQLUpload, Upload: GraphQLUpload,
Query: { Query: {
user: singleUserResolver, user: singleUserResolver,
link: singleLinkResolver, link: singleLinkResolver,
...calendarResolver.Query, // ...calendarResolver.Query,
...customImageResolver.Query, // ...customImageResolver.Query,
...noteResolver.Query, // ...noteResolver.Query,
...connectionStepsResolver.Query // ...connectionStepsResolver.Query,
...movieResolvers.Query,
...reviewResolvers.Query,
...directorResolvers.Query,
...actorResolvers.Query,
}, },
Mutation: { Mutation: {
updateUser: updateUserResolver, updateUser: updateUserResolver,
createLink: createLinkResolver, createLink: createLinkResolver,
deactivateAllLinks: deactivateAllLinksResolver, deactivateAllLinks: deactivateAllLinksResolver,
uploadFile: uploadFileMutationResolver, // uploadFile: uploadFileMutationResolver,
...calendarResolver.Mutation, // ...calendarResolver.Mutation,
...customImageResolver.Mutation, // ...customImageResolver.Mutation,
...noteResolver.Mutation, // ...noteResolver.Mutation,
...movieResolvers.Mutation,
...reviewResolvers.Mutation,
...directorResolvers.Mutation,
...actorResolvers.Mutation,
}, },
...calendarResolver.Type, // ...calendarResolver.Type,
...noteResolver.Type, // ...noteResolver.Type,
User: typeUserResolver, User: typeUserResolver,
Link: typeLinkResolver, Link: typeLinkResolver,
+13 -5
View File
@@ -1,4 +1,4 @@
'use strict'; "use strict";
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/ /*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
/** /**
* Server * Server
@@ -8,11 +8,19 @@
* @author Ryan Wong * @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, () => { app.listen(PORT, () => {
console.log('Server running at ', true ? `http://localhost:${PORT}` : 'process.env.BASE_URL'); console.log(
console.log('GraphQL running at ', true ? `http://localhost:${PORT}${apollo.graphqlPath}` : `${'process.env.BASE_URL'}${apollo.graphqlPath}`); "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}`
);
}); });
+147
View File
@@ -211,3 +211,150 @@ type Mutation {
uploadFile(file: Upload!): FileUploadResponse! 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!
}
+44 -34
View File
@@ -1,4 +1,4 @@
'use strict'; "use strict";
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/ /*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
/** /**
* Sequelize File * Sequelize File
@@ -8,49 +8,59 @@
* @author Ryan Wong * @author Ryan Wong
* *
*/ */
const fs = require('fs'); const fs = require("fs");
const path = require('path'); const path = require("path");
let Sequelize = require('sequelize'); let Sequelize = require("sequelize");
const basename = path.basename(__filename); const basename = path.basename(__filename);
const { DataTypes } = require('sequelize'); const { DataTypes } = require("sequelize");
const config = { const config = {
DB_DATABASE: 'mysql', DB_DATABASE: "mysql",
DB_USERNAME: 'root', DB_USERNAME: "root",
DB_PASSWORD: 'root', DB_PASSWORD: process.env.DB_PASSWORD || "root",
DB_ADAPTER: 'mysql', DB_ADAPTER: "mysql",
DB_NAME: 'day_1', DB_NAME: "day_13",
DB_HOSTNAME: 'localhost', DB_HOSTNAME: "localhost",
DB_PORT: 3306, DB_PORT: 3306,
}; };
let db = {}; let db = {};
let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, { let sequelize = new Sequelize(
dialect: config.DB_ADAPTER, config.DB_NAME,
username: config.DB_USERNAME, config.DB_USERNAME,
password: config.DB_PASSWORD, config.DB_PASSWORD,
database: config.DB_NAME, {
host: config.DB_HOSTNAME, dialect: config.DB_ADAPTER,
port: config.DB_PORT, username: config.DB_USERNAME,
logging: console.log, password: config.DB_PASSWORD,
timezone: '-04:00', database: config.DB_NAME,
pool: { host: config.DB_HOSTNAME,
maxConnections: 1, port: config.DB_PORT,
minConnections: 0, logging: console.log,
maxIdleTime: 100, timezone: "-04:00",
}, pool: {
define: { maxConnections: 1,
timestamps: false, minConnections: 0,
underscoredAll: true, maxIdleTime: 100,
underscored: true, },
}, define: {
}); timestamps: false,
underscoredAll: true,
underscored: true,
},
}
);
// sequelize.sync({ force: true }); sequelize
.sync()
.then(() => console.log("Tables synced successfully!"))
.catch((err) => console.log(err));
fs.readdirSync(__dirname) fs.readdirSync(__dirname)
.filter((file) => { .filter((file) => {
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js'; return (
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
);
}) })
.forEach((file) => { .forEach((file) => {
var model = require(path.join(__dirname, file))(sequelize, DataTypes); var model = require(path.join(__dirname, file))(sequelize, DataTypes);
@@ -66,4 +76,4 @@ Object.keys(db).forEach((modelName) => {
db.sequelize = sequelize; db.sequelize = sequelize;
db.Sequelize = Sequelize; db.Sequelize = Sequelize;
module.exports = db; module.exports = db;
+24
View File
@@ -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;
};
+15
View File
@@ -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
View File
@@ -3,7 +3,8 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "node ./bin/www" "start": "node ./bin/www",
"dev": "node --watch --env-file=.env ./bin/www"
}, },
"dependencies": { "dependencies": {
"cookie-parser": "~1.4.4", "cookie-parser": "~1.4.4",
@@ -14,6 +15,7 @@
"jade": "~1.11.0", "jade": "~1.11.0",
"morgan": "~1.9.1", "morgan": "~1.9.1",
"mysql2": "^2.3.3", "mysql2": "^2.3.3",
"sequelize": "^6.15.1" "sequelize": "^6.15.1",
"stripe": "^18.3.0"
} }
} }
+107 -3
View File
@@ -1,9 +1,113 @@
var express = require('express'); var express = require("express");
var router = express.Router(); 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. */ /* GET home page. */
router.get('/', function(req, res, next) { router.get("/", async function (req, res, next) {
res.render('index', { title: 'Express' }); 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; module.exports = router;
+22
View File
@@ -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
View File
@@ -2,4 +2,19 @@ extends layout
block content block content
h1= title 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.
+1
View File
@@ -2,6 +2,7 @@ doctype html
html html
head head
title= title 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') link(rel='stylesheet', href='/stylesheets/style.css')
body body
block content block content
+15
View File
@@ -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
+23
View File
@@ -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
+74
View File
@@ -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
View File
@@ -1,13 +1,38 @@
let fs = require('fs'); const fs = require("fs");
const path = require("path");
function Model_builder() { class ModelBuilder {
let config = fs.readFileSync('configuration.json'); static build() {
const config = require("./configuration.json");
const modelDir = path.join(__dirname, "release/models");
this.build = function () { // Create release/models directory
//generate files and put it into release folder if (!fs.existsSync(modelDir)) fs.mkdirSync(modelDir, { recursive: true });
//Copy initialize files into release folder
//TODO 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;
}
+34 -34
View File
@@ -1,37 +1,37 @@
{ {
"model": [ "model": [
{ {
"name": "location", "name": "location",
"field: [ "field": [
["id", "integer", "ID", "required"], ["id", "integer", "ID", "required"],
["name", "string", "Name", "required"], ["name", "string", "Name", "required"],
["status", "integer", "Status", "required"], ["status", "integer", "Status", "required"]
]
},
{
"name": "email",
"field": [
["id", "integer", "ID", "required"],
["email", "string", "Email", "required"],
["status", "integer", "Status", "required"]
]
},
{
"name": "sms",
"field": [
["id", "integer", "ID", "required"],
["phone", "string", "Phone", "required"],
["status", "integer", "Status", "required"]
]
},
{
"name": "user",
"field": [
["id", "integer", "ID", "required"],
["name", "string", "Name", "required"],
["email", "string", "Email", "required"],
["status", "integer", "Status", "required"]
]
}
] ]
},
{
"name": "email",
"field: [
["id", "integer", "ID", "required"],
["email", "string", "Email", "required"],
["status", "integer", "Status", "required"],
]
},
{
"name": "sms",
"field: [
["id", "integer", "ID", "required"],
["phone", "string", "Phone", "required"],
["status", "integer", "Status", "required"],
]
},
{
"name": "user",
"field: [
["id", "integer", "ID", "required"],
["name", "string", "Name", "required"],
["email", "string", "Email", "required"],
["status", "integer", "Status", "required"],
]
}
]
} }
+7
View File
@@ -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;
+20
View File
@@ -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
});
};
+20
View File
@@ -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
});
};
+20
View File
@@ -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
});
};
+24
View File
@@ -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
View File
@@ -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}`));
+9
View File
@@ -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;
+15
View File
@@ -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 };
+19
View File
@@ -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"
}
}
+24
View File
@@ -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?
+29
View File
@@ -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_]' }],
},
},
])
+13
View File
@@ -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>
+33
View File
@@ -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"
}
}
+1
View File
@@ -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

+1
View File
@@ -0,0 +1 @@
@import "tailwindcss";
+317
View File
@@ -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;
+1
View File
@@ -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

+7
View File
@@ -0,0 +1,7 @@
@import "tailwindcss";
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
}
+10
View File
@@ -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>,
)
+8
View File
@@ -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
View File
@@ -1,11 +1,12 @@
var createError = require('http-errors'); var createError = require("http-errors");
var express = require('express'); var express = require("express");
var path = require('path'); var path = require("path");
var cookieParser = require('cookie-parser'); var cookieParser = require("cookie-parser");
var logger = require('morgan'); var logger = require("morgan");
var indexRouter = require('./routes/index'); var indexRouter = require("./routes/index");
var usersRouter = require('./routes/users'); var usersRouter = require("./routes/users");
var apiRouter = require("./routes/api");
const db = require("./models"); const db = require("./models");
var cors = require("cors"); var cors = require("cors");
@@ -13,18 +14,18 @@ var cors = require("cors");
var app = express(); var app = express();
app.set("db", db); app.set("db", db);
// view engine setup // view engine setup
app.set('views', path.join(__dirname, 'views')); app.set("views", path.join(__dirname, "views"));
app.set('view engine', 'jade'); app.set("view engine", "ejs");
app.use(cors()); app.use(cors());
app.use(logger('dev')); app.use(logger("dev"));
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: false })); app.use(express.urlencoded({ extended: false }));
app.use(cookieParser()); 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", apiRouter);
// catch 404 and forward to error handler // catch 404 and forward to error handler
app.use(function (req, res, next) { app.use(function (req, res, next) {
next(createError(404)); next(createError(404));
@@ -34,11 +35,11 @@ app.use(function (req, res, next) {
app.use(function (err, req, res, next) { app.use(function (err, req, res, next) {
// set locals, only providing error in development // set locals, only providing error in development
res.locals.message = err.message; 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 // render the error page
res.status(err.status || 500); res.status(err.status || 500);
res.render('error'); res.render("error");
}); });
module.exports = app; module.exports = app;
+32
View File
@@ -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;
};
+48 -34
View File
@@ -1,4 +1,4 @@
'use strict'; "use strict";
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/ /*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
/** /**
* Sequelize File * Sequelize File
@@ -8,49 +8,63 @@
* @author Ryan Wong * @author Ryan Wong
* *
*/ */
const fs = require('fs'); const fs = require("fs");
const path = require('path'); const path = require("path");
let Sequelize = require('sequelize'); let Sequelize = require("sequelize");
const basename = path.basename(__filename); const basename = path.basename(__filename);
const { DataTypes } = require('sequelize'); const { DataTypes } = require("sequelize");
const config = { const config = {
DB_DATABASE: 'mysql', DB_DATABASE: "mysql",
DB_USERNAME: 'root', DB_USERNAME: "root",
DB_PASSWORD: 'root', DB_PASSWORD: process.env.DB_PASSWORD || "root",
DB_ADAPTER: 'mysql', DB_ADAPTER: "mysql",
DB_NAME: 'day_1', DB_NAME: "day_17",
DB_HOSTNAME: 'localhost', DB_HOSTNAME: "localhost",
DB_PORT: 3306, DB_PORT: 3306,
}; };
let db = {}; let db = {};
let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, { let sequelize = new Sequelize(
dialect: config.DB_ADAPTER, config.DB_NAME,
username: config.DB_USERNAME, config.DB_USERNAME,
password: config.DB_PASSWORD, config.DB_PASSWORD,
database: config.DB_NAME, {
host: config.DB_HOSTNAME, dialect: config.DB_ADAPTER,
port: config.DB_PORT, username: config.DB_USERNAME,
logging: console.log, password: config.DB_PASSWORD,
timezone: '-04:00', database: config.DB_NAME,
pool: { host: config.DB_HOSTNAME,
maxConnections: 1, port: config.DB_PORT,
minConnections: 0, logging: console.log,
maxIdleTime: 100, timezone: "-04:00",
}, pool: {
define: { maxConnections: 1,
timestamps: false, minConnections: 0,
underscoredAll: true, maxIdleTime: 100,
underscored: true, },
}, define: {
}); timestamps: false,
underscoredAll: true,
underscored: true,
},
}
);
// sequelize.sync({ force: true }); sequelize
.sync()
.then(() => {
console.log("Database & tables created!");
})
.catch((err) => {
console.log(err);
});
fs.readdirSync(__dirname) fs.readdirSync(__dirname)
.filter((file) => { .filter((file) => {
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js'; return (
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
);
}) })
.forEach((file) => { .forEach((file) => {
var model = require(path.join(__dirname, file))(sequelize, DataTypes); var model = require(path.join(__dirname, file))(sequelize, DataTypes);
@@ -66,4 +80,4 @@ Object.keys(db).forEach((modelName) => {
db.sequelize = sequelize; db.sequelize = sequelize;
db.Sequelize = Sequelize; db.Sequelize = Sequelize;
module.exports = db; module.exports = db;
+5 -1
View File
@@ -3,17 +3,21 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "node ./bin/www" "start": "node ./bin/www",
"dev": "node --watch --env-file=.env ./bin/www"
}, },
"dependencies": { "dependencies": {
"cookie-parser": "~1.4.4", "cookie-parser": "~1.4.4",
"cors": "^2.8.5", "cors": "^2.8.5",
"debug": "~2.6.9", "debug": "~2.6.9",
"ejs": "^3.1.10",
"express": "~4.16.1", "express": "~4.16.1",
"http-errors": "~1.6.3", "http-errors": "~1.6.3",
"jade": "~1.11.0", "jade": "~1.11.0",
"moment-timezone": "^0.6.0",
"morgan": "~1.9.1", "morgan": "~1.9.1",
"mysql2": "^2.3.3", "mysql2": "^2.3.3",
"node-input-validator": "^4.5.1",
"sequelize": "^6.15.1" "sequelize": "^6.15.1"
} }
} }
+285 -4
View File
@@ -1,8 +1,289 @@
body { body {
padding: 50px; margin: 0;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; padding: 0;
font-family: "Inter", "Lucida Grande", Helvetica, Arial, sans-serif;
background: #eaeaea;
color: #222;
} }
a { .calendar-container {
color: #00B7FF; 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;
}
} }
+69
View File
@@ -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
View File
@@ -1,9 +1,139 @@
var express = require('express'); var express = require("express");
var router = express.Router(); var router = express.Router();
const moment = require("moment-timezone");
/* GET home page. */ // Helper: Group timezones by region for the timezone selection screen
router.get('/', function(req, res, next) { function getTimezoneGroups() {
res.render('index', { title: 'Express' }); 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; module.exports = router;
+97
View File
@@ -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>
+60
View File
@@ -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') %>
+10
View File
@@ -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') %>
-6
View File
@@ -1,6 +0,0 @@
extends layout
block content
h1= message
h2= error.status
pre #{error.stack}
+8
View File
@@ -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') %>
-5
View File
@@ -1,5 +0,0 @@
extends layout
block content
h1= title
p Welcome to #{title}
+10
View File
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel="stylesheet" href="/stylesheets/style.css" />
</head>
<body>
<%- body %>
</body>
</html>
-7
View File
@@ -1,7 +0,0 @@
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
body
block content
+2
View File
@@ -0,0 +1,2 @@
</body>
</html>
+9
View File
@@ -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>
+8
View File
@@ -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') %>
+78
View File
@@ -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
View File
@@ -1,11 +1,12 @@
var createError = require('http-errors'); var createError = require("http-errors");
var express = require('express'); var express = require("express");
var path = require('path'); var path = require("path");
var cookieParser = require('cookie-parser'); var cookieParser = require("cookie-parser");
var logger = require('morgan'); var logger = require("morgan");
var indexRouter = require('./routes/index'); var indexRouter = require("./routes/index");
var usersRouter = require('./routes/users'); var usersRouter = require("./routes/users");
var scheduleRouter = require("./routes/schedule");
const db = require("./models"); const db = require("./models");
var cors = require("cors"); var cors = require("cors");
@@ -13,17 +14,18 @@ var cors = require("cors");
var app = express(); var app = express();
app.set("db", db); app.set("db", db);
// view engine setup // view engine setup
app.set('views', path.join(__dirname, 'views')); app.set("views", path.join(__dirname, "views"));
app.set('view engine', 'jade'); app.set("view engine", "jade");
app.use(cors()); app.use(cors());
app.use(logger('dev')); app.use(logger("dev"));
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: false })); app.use(express.urlencoded({ extended: false }));
app.use(cookieParser()); app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public'))); app.use(express.static(path.join(__dirname, "public")));
app.use('/', indexRouter); app.use("/", indexRouter);
app.use('/users', usersRouter); app.use("/users", usersRouter);
app.use("/api/v1", scheduleRouter);
// catch 404 and forward to error handler // catch 404 and forward to error handler
app.use(function (req, res, next) { app.use(function (req, res, next) {
@@ -34,11 +36,11 @@ app.use(function (req, res, next) {
app.use(function (err, req, res, next) { app.use(function (err, req, res, next) {
// set locals, only providing error in development // set locals, only providing error in development
res.locals.message = err.message; 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 // render the error page
res.status(err.status || 500); res.status(err.status || 500);
res.render('error'); res.render("error");
}); });
module.exports = app; module.exports = app;
+36
View File
@@ -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;
};
+48 -34
View File
@@ -1,4 +1,4 @@
'use strict'; "use strict";
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/ /*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
/** /**
* Sequelize File * Sequelize File
@@ -8,49 +8,63 @@
* @author Ryan Wong * @author Ryan Wong
* *
*/ */
const fs = require('fs'); const fs = require("fs");
const path = require('path'); const path = require("path");
let Sequelize = require('sequelize'); let Sequelize = require("sequelize");
const basename = path.basename(__filename); const basename = path.basename(__filename);
const { DataTypes } = require('sequelize'); const { DataTypes } = require("sequelize");
const config = { const config = {
DB_DATABASE: 'mysql', DB_DATABASE: "mysql",
DB_USERNAME: 'root', DB_USERNAME: "root",
DB_PASSWORD: 'root', DB_PASSWORD: process.env.DB_PASSWORD || "root",
DB_ADAPTER: 'mysql', DB_ADAPTER: "mysql",
DB_NAME: 'day_1', DB_NAME: "day_19",
DB_HOSTNAME: 'localhost', DB_HOSTNAME: "localhost",
DB_PORT: 3306, DB_PORT: 3306,
}; };
let db = {}; let db = {};
let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, { let sequelize = new Sequelize(
dialect: config.DB_ADAPTER, config.DB_NAME,
username: config.DB_USERNAME, config.DB_USERNAME,
password: config.DB_PASSWORD, config.DB_PASSWORD,
database: config.DB_NAME, {
host: config.DB_HOSTNAME, dialect: config.DB_ADAPTER,
port: config.DB_PORT, username: config.DB_USERNAME,
logging: console.log, password: config.DB_PASSWORD,
timezone: '-04:00', database: config.DB_NAME,
pool: { host: config.DB_HOSTNAME,
maxConnections: 1, port: config.DB_PORT,
minConnections: 0, logging: console.log,
maxIdleTime: 100, timezone: "-04:00",
}, pool: {
define: { maxConnections: 1,
timestamps: false, minConnections: 0,
underscoredAll: true, maxIdleTime: 100,
underscored: true, },
}, define: {
}); timestamps: false,
underscoredAll: true,
underscored: true,
},
}
);
// sequelize.sync({ force: true }); sequelize
.sync()
.then(() => {
console.log("Database & tables created!");
})
.catch((err) => {
console.log(err);
});
fs.readdirSync(__dirname) fs.readdirSync(__dirname)
.filter((file) => { .filter((file) => {
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js'; return (
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
);
}) })
.forEach((file) => { .forEach((file) => {
var model = require(path.join(__dirname, file))(sequelize, DataTypes); var model = require(path.join(__dirname, file))(sequelize, DataTypes);
@@ -66,4 +80,4 @@ Object.keys(db).forEach((modelName) => {
db.sequelize = sequelize; db.sequelize = sequelize;
db.Sequelize = Sequelize; db.Sequelize = Sequelize;
module.exports = db; module.exports = db;
+32
View File
@@ -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
View File
@@ -3,7 +3,8 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "node ./bin/www" "start": "node ./bin/www",
"dev": "node --watch --env-file=.env ./bin/www"
}, },
"dependencies": { "dependencies": {
"cookie-parser": "~1.4.4", "cookie-parser": "~1.4.4",
+52
View File
@@ -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;
+27
View File
@@ -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?
+5
View File
@@ -0,0 +1,5 @@
{
"tabWidth": 2,
"useTabs": false
}
+5 -5
View File
@@ -1,22 +1,23 @@
# Day 20 # Day 20
Read: Read:
- https://www.notion.so/How-to-Use-Baas-00f549dda3a84dc48b352c79222f1a3a - https://www.notion.so/How-to-Use-Baas-00f549dda3a84dc48b352c79222f1a3a
- https://www.notion.so/Create-Manage-Projects-With-Wireframe-Tool-df67b882f0c14735a0192d69dc3ff777 - https://www.notion.so/Create-Manage-Projects-With-Wireframe-Tool-df67b882f0c14735a0192d69dc3ff777
- Request for Wireframe tool url from Project Manager. - Request for Wireframe tool url from Project Manager.
1. login to Wireframe tool. Create SOW and Wireframe called <name-inventory>. 1. login to Wireframe tool. Create SOW and Wireframe called <name-inventory>.
2. Navigate to Wireframe side-menu click, Edit > Setting, create a project (<name-inventory>) from here according to specifications of wireframe document provided (inventory-app.pdf). 2. Navigate to Wireframe side-menu click, Edit > Setting, create a project (<name-inventory>) from here according to specifications of wireframe document provided (inventory-app.pdf).
3. Create Models. Switch to Models tab or Web/React tab (Manage Models). 3. Create Models. Switch to Models tab or Web/React tab (Manage Models).
4. Create Roles and set Permissions. (Web/React Tab > Manage Permissions). 4. Create Roles and set Permissions. (Web/React Tab > Manage Permissions).
5. Create React portal and marketing pages and then export React. (Web/React Tab). 5. Create React portal and marketing pages and then export React. (Web/React Tab).
6. Create Custom APIs and commit. (API tab). API code would be commited to http://23.29.118.76:3000/mkdlabs/<name-inventory_backend>.git 6. Create Custom APIs and commit. (API tab). API code would be commited to http://23.29.118.76:3000/mkdlabs/<name-inventory_backend>.git
7. Switch to Deployment Tab. Initialize deployment and create repositories. 7. Switch to Deployment Tab. Initialize deployment and create repositories.
@@ -27,4 +28,3 @@ Read:
10. Clone backend repo on src/backend/custom 10. Clone backend repo on src/backend/custom
11. Write APIs, and test locally. 11. Write APIs, and test locally.
+26
View File
@@ -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>
+17
View File
@@ -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/*"]
}
}
}
+179
View File
@@ -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"
}
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
View File
+34
View File
@@ -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;
+3
View File
@@ -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

+18
View File
@@ -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

+24
View File
@@ -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>
);
};
+14
View File
@@ -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>
);
};
+13
View File
@@ -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>
)
}
+21
View File
@@ -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}
/>
);
};
+9
View File
@@ -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;
}
+2
View File
@@ -0,0 +1,2 @@
export {default as AddButton } from "./AddButton";
+11
View File
@@ -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">&times;</span></button></li>
);
};
export default AddTags;

Some files were not shown because too many files have changed in this diff Show More