Compare commits

..

No commits in common. "master" and "ics_file_method" have entirely different histories.

19 changed files with 358 additions and 665 deletions

View file

@ -1,3 +0,0 @@
node_modules
npm-debug.log
tmp

View file

@ -1,11 +1,9 @@
# Google
SERPAPI_KEY
# Google
GOOGLE_CALENDAR_ID
GOOGLE_PROJECT_NUMBER
GOOGLE_CLIENT_EMAIL
GOOGLE_PRIVATE_KEY
GOOGLE_KEY_ID
GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET

12
.gitignore vendored
View file

@ -2,8 +2,7 @@
node_modules
#javascript build files #
# public/**/*
dist/**/*
public
# Tmp files #
tmp
@ -13,11 +12,4 @@ tmp
.env
config/client_google_auth.json
## Docker ##
build_image.sh
push_dockerhub.sh
# keys #
keys/**/*
cron.log
## output ##

View file

@ -1,16 +0,0 @@
FROM node:16
ENV TZ=Asia/Jerusalem
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
RUN npm install -g typescript
COPY . .
EXPOSE ${PORT}
RUN tsc
CMD [ "npm", "start" ]

95
dist/GameSource.js vendored
View file

@ -1,95 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
require("dotenv").config();
const axios_1 = __importDefault(require("axios"));
const moment_1 = __importDefault(require("moment"));
class GameSource {
async getGamesFromHaifa(logger) {
console.log("Trying to get games from Haifa...");
try {
// Get the current date and time in the required format
const currentDate = (0, moment_1.default)().format("DD/MM/YYYY HH:mm");
// Construct the filters object with the current date
const filters = {
date: {
startDate: currentDate,
endDate: "",
},
league: "",
session: "",
gamesDirection: "1",
};
// Encode the filters for the URL
const filtersParam = encodeURIComponent(JSON.stringify(filters));
// Construct the API URL with the encoded filters
const sourceUrl = `https://api.mhaifafc.com/api/content/games-lobby?filters=${filtersParam}&start=0&limit=20&sortDirection=ASC&app=web&lang=he`;
// Get the authorization token from environment variables
const authorizationToken = process.env.HAIFA_API_AUTH_TOKEN;
// Set up the request headers
const headers = {
Accept: "*/*",
"Accept-Language": "en-US,en;q=0.7",
Authorization: `Bearer ${authorizationToken}`,
"User-Agent": "Mozilla/5.0",
Origin: "https://www.mhaifafc.com",
Referer: "https://www.mhaifafc.com/",
};
// Make the API request
const response = await axios_1.default.get(sourceUrl, {
headers,
responseType: "json",
responseEncoding: "utf8", // Ensure UTF-8 encoding
});
// Extract the games data from the response
const gamesData = response.data.games.items;
const games = [];
// Loop through each game and construct the GoogleCalendarEvent objects
for (const game of gamesData) {
const gameDetails = game.gameDetails;
const gameTime = gameDetails.gameTime; // ISO string
const isFinalGameDate = gameDetails.isFinalGameDate;
const gameLocation = gameDetails.gameLocation;
// Skip games without a game time
if (!gameTime)
continue;
const hostTeam = game.hostTeam;
const guestTeam = game.guestTeam;
// Get team names
const hostTeamName = hostTeam.teamName;
const guestTeamName = guestTeam.teamName;
const summary = `${hostTeamName} vs. ${guestTeamName}`;
// Include a note if the game date is not final
let description = `${hostTeamName} vs. ${guestTeamName}`;
if (!isFinalGameDate) {
description += " (Date and time are subject to change)";
}
// Calculate start and end times
const startDateTime = (0, moment_1.default)(gameTime).toISOString();
const endDateTime = (0, moment_1.default)(gameTime).add(2, "hours").toISOString();
// Add the event to the games array
games.push({
summary: summary,
location: gameLocation,
description: description,
start: {
dateTime: startDateTime,
timeZone: "Asia/Jerusalem",
},
end: {
dateTime: endDateTime,
timeZone: "Asia/Jerusalem",
},
});
}
return games;
}
catch (error) {
console.error(error);
return [];
}
}
}
exports.default = GameSource;

View file

@ -1,88 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const google_auth_library_1 = require("google-auth-library");
const googleapis_1 = require("googleapis");
require("dotenv").config();
const env = process.env;
class GoogleCalendar {
constructor() {
this.gamesMap = {};
this.clientSecret = env.GOOGLE_CLIENT_SECRET;
this.clientId = env.GOOGLE_CLIENT_ID;
this.calenderId = env.GOOGLE_CALENDAR_ID;
this.clientEmail = env.GOOGLE_CLIENT_EMAIL;
this.googlePrivateKey = env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, "\n");
}
async init() {
console.log("INIT GOOGLE CALENDAR");
const jwtClient = await this.authorize();
this.calendar = googleapis_1.google.calendar({ version: "v3", auth: jwtClient });
}
async authorize() {
console.log("AUTHORIZE GOOGLE CALENDAR");
this.JWT_client = new google_auth_library_1.JWT({
email: this.clientEmail,
key: this.googlePrivateKey,
scopes: ["https://www.googleapis.com/auth/calendar"],
});
const { access_token } = await this.JWT_client.authorize();
this.token = access_token;
if (!this.token) {
throw new Error("Failed to connect to google calendar");
}
console.log("GOOGLE CALENDAR AUTHORIZED SUCCESSFULLY");
return this.JWT_client;
}
async updateNewEvent(upcomingEvents) {
// console.log(upcomingEvents)
setTimeout(async () => {
upcomingEvents.forEach(async (event) => {
console.log("UPDATE NEW EVENT", upcomingEvents);
const options = {
auth: this.JWT_client,
calendarId: this.calenderId,
resource: {
summary: event.summary,
location: event.location,
description: event.description,
start: {
dateTime: event.start.dateTime,
timeZone: "Asia/Jerusalem",
},
end: { dateTime: event.end.dateTime, timeZone: "Asia/Jerusalem" },
sendNotifications: true,
},
};
await this.calendar.events.insert(options, function (err, event) {
if (err) {
console.log("There was an error contacting the Calendar service: " + err);
return;
}
console.log(event.description + " created");
});
});
}, 3000);
}
async isDuplicateEvent(startTime, endTime, title) {
if (this.gamesMap[startTime]) {
console.log("duplicate event");
return true;
}
this.gamesMap[startTime] = true;
console.log("checking for duplicate event");
try {
const response = await this.calendar.events.list({
calendarId: this.calenderId,
timeMin: startTime,
timeMax: endTime,
q: title, // Search for events with the same title
});
return response.data.items.length > 0;
}
catch (error) {
console.error(error.message);
return false;
}
}
}
exports.default = GoogleCalendar;

68
dist/index.js vendored
View file

@ -1,68 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const GameSource_1 = __importDefault(require("./GameSource"));
const GoogleCalendar_1 = __importDefault(require("./GoogleCalendar"));
const dotenv_1 = __importDefault(require("dotenv"));
const node_cron_1 = __importDefault(require("node-cron"));
const fs_1 = __importDefault(require("fs")); // Importing fs for logging
dotenv_1.default.config();
class App {
constructor() {
this.gameSource = new GameSource_1.default();
this.googleCalendar = new GoogleCalendar_1.default();
}
async startCronJob() {
this.writeLog('START Haifa Reminder'); // Log when the cron job starts
const newGamesAdded = [];
await this.googleCalendar.init();
try {
const games = await this.gameSource.getGamesFromHaifa(this.writeLog);
for (const game of games) {
const isDuplicateEvent = await this.googleCalendar.isDuplicateEvent(game.start.dateTime, game.end.dateTime, game.summary);
console.log(game);
if (!isDuplicateEvent) {
newGamesAdded.push(game);
console.log("Event does not exist");
await this.googleCalendar.updateNewEvent([game]);
}
else {
console.log("Event already exists");
}
}
if (newGamesAdded.length > 0) {
console.log("New games added:", newGamesAdded);
}
else {
console.log("No new games were Added!");
}
this.writeLog('Successfully ran project');
}
catch (error) {
this.writeLog("Error in cron job:" + error.message);
}
finally {
this.writeLog('END CRON JOB'); // Log when the cron job ends
}
}
writeLog(message) {
const timestamp = new Date().toISOString();
const logMessage = `${timestamp} - ${message}\n`;
// Write to log file synchronously
fs_1.default.appendFileSync('cron.log', logMessage);
console.log(logMessage); // Optional: also log to console
}
}
const app = new App();
node_cron_1.default.schedule('* * * * *', () => {
console.log('Running startCronJob at 10:00 AM Jerusalem time');
app.startCronJob().catch((error) => {
console.error("Error in scheduled cron job:", error.message);
app.writeLog(`ERROR: ${error.message}`); // Log any errors
});
}, {
scheduled: true,
timezone: "Asia/Jerusalem"
});

2
dist/types/index.js vendored
View file

@ -1,2 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

91
maccabi-haifa-fc.ics Normal file
View file

@ -0,0 +1,91 @@
BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:https://mhaifafc.com/
METHOD:PUBLISH
X-PUBLISHED-TTL:PT1H
BEGIN:VEVENT
UID:cba9cb6a-56ae-4730-ba62-b98032e7767a
SUMMARY:Maccabi Haifa F.C.
DTSTAMP:20230329T074644Z
DTSTART:20230401T170000Z
DTEND:20230401T190000Z
DESCRIPTION:Maccabi Haifa vs. H Be'er Sheva
URL:https://mhaifafc.com/
LOCATION:Sammy Ofer Stadium
STATUS:CONFIRMED
CATEGORIES:
ORGANIZER;CN=Maccabi Haifa F.C.
X-MICROSOFT-CDO-BUSYSTATUS:BUSY
END:VEVENT
BEGIN:VEVENT
UID:cba9cb6a-56ae-4730-ba62-b98032e7767a
SUMMARY:Maccabi Haifa F.C.
DTSTAMP:20230329T074644Z
DTSTART:20230404T173000Z
DTEND:20230404T193000Z
DESCRIPTION:Maccabi Tel aviv vs. Maccabi Haifa
URL:https://mhaifafc.com/
LOCATION:Sammy Ofer Stadium
STATUS:CONFIRMED
CATEGORIES:
ORGANIZER;CN=Maccabi Haifa F.C.
X-MICROSOFT-CDO-BUSYSTATUS:BUSY
END:VEVENT
BEGIN:VEVENT
UID:cba9cb6a-56ae-4730-ba62-b98032e7767a
SUMMARY:Maccabi Haifa F.C.
DTSTAMP:20230329T074644Z
DTSTART:20230408T170000Z
DTEND:20230408T190000Z
DESCRIPTION:Maccabi Haifa vs. Maccabi Netanya
URL:https://mhaifafc.com/
LOCATION:Sammy Ofer Stadium
STATUS:CONFIRMED
CATEGORIES:
ORGANIZER;CN=Maccabi Haifa F.C.
X-MICROSOFT-CDO-BUSYSTATUS:BUSY
END:VEVENT
BEGIN:VEVENT
UID:cba9cb6a-56ae-4730-ba62-b98032e7767a
SUMMARY:Maccabi Haifa F.C.
DTSTAMP:20230329T074644Z
DTSTART:20230415T153000Z
DTEND:20230415T173000Z
DESCRIPTION:Hapoel Jerusalem vs. Maccabi Haifa
URL:https://mhaifafc.com/
LOCATION:Sammy Ofer Stadium
STATUS:CONFIRMED
CATEGORIES:
ORGANIZER;CN=Maccabi Haifa F.C.
X-MICROSOFT-CDO-BUSYSTATUS:BUSY
END:VEVENT
BEGIN:VEVENT
UID:cba9cb6a-56ae-4730-ba62-b98032e7767a
SUMMARY:Maccabi Haifa F.C.
DTSTAMP:20230329T074644Z
DTSTART:20230423T173000Z
DTEND:20230423T193000Z
DESCRIPTION:F.C Ashdod vs. Maccabi Haifa
URL:https://mhaifafc.com/
LOCATION:Sammy Ofer Stadium
STATUS:CONFIRMED
CATEGORIES:
ORGANIZER;CN=Maccabi Haifa F.C.
X-MICROSOFT-CDO-BUSYSTATUS:BUSY
END:VEVENT
BEGIN:VEVENT
UID:cba9cb6a-56ae-4730-ba62-b98032e7767a
SUMMARY:Maccabi Haifa F.C.
DTSTAMP:20230329T074644Z
DTSTART:20230501T173000Z
DTEND:20230501T193000Z
DESCRIPTION:H Be'er Sheva vs. Maccabi Haifa
URL:https://mhaifafc.com/
LOCATION:Sammy Ofer Stadium
STATUS:CONFIRMED
CATEGORIES:
ORGANIZER;CN=Maccabi Haifa F.C.
X-MICROSOFT-CDO-BUSYSTATUS:BUSY
END:VEVENT
END:VCALENDAR

245
package-lock.json generated
View file

@ -7,67 +7,30 @@
"dependencies": {
"axios": "^1.3.4",
"dotenv": "^16.0.3",
"googleapis": "^122.0.0",
"googleapis": "^113.0.0",
"ics": "^3.1.0",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"node-cron": "^3.0.3",
"node-html-parser": "^6.1.5",
"uuid": "^9.0.0"
"node-html-parser": "^6.1.5"
},
"devDependencies": {
"@types/express": "^4.17.17",
"@types/node": "^18.15.5",
"@types/node-cron": "^3.0.11",
"typescript": "^5.0.3"
"@types/node": "^18.15.5"
}
},
"node_modules/@types/body-parser": {
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
"integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
"dev": true,
"node_modules/@babel/runtime": {
"version": "7.21.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz",
"integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==",
"dependencies": {
"@types/connect": "*",
"@types/node": "*"
"regenerator-runtime": "^0.13.11"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@types/connect": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
"integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/express": {
"version": "4.17.17",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz",
"integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==",
"dev": true,
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.33",
"@types/qs": "*",
"@types/serve-static": "*"
}
},
"node_modules/@types/express-serve-static-core": {
"version": "4.17.33",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz",
"integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==",
"dev": true,
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*"
}
},
"node_modules/@types/mime": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
"integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==",
"dev": true
"node_modules/@types/lodash": {
"version": "4.14.192",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.192.tgz",
"integrity": "sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A=="
},
"node_modules/@types/node": {
"version": "18.15.5",
@ -75,34 +38,6 @@
"integrity": "sha512-Ark2WDjjZO7GmvsyFFf81MXuGTA/d6oP38anyxWOL6EREyBKAxKoFHwBhaZxCfLRLpO8JgVXwqOwSwa7jRcjew==",
"dev": true
},
"node_modules/@types/node-cron": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz",
"integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==",
"dev": true
},
"node_modules/@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
"dev": true
},
"node_modules/@types/range-parser": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
"dev": true
},
"node_modules/@types/serve-static": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz",
"integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==",
"dev": true,
"dependencies": {
"@types/mime": "*",
"@types/node": "*"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@ -373,23 +308,23 @@
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"node_modules/gaxios": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz",
"integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.0.tgz",
"integrity": "sha512-aezGIjb+/VfsJtIcHGcBSerNEDdfdHeMros+RbYbGpmonKWQCOVOes0LVZhn1lDtIgq55qq0HaxymIoae3Fl/A==",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^5.0.0",
"is-stream": "^2.0.0",
"node-fetch": "^2.6.9"
"node-fetch": "^2.6.7"
},
"engines": {
"node": ">=12"
}
},
"node_modules/gcp-metadata": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz",
"integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.2.0.tgz",
"integrity": "sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==",
"dependencies": {
"gaxios": "^5.0.0",
"json-bigint": "^1.0.0"
@ -412,16 +347,16 @@
}
},
"node_modules/google-auth-library": {
"version": "8.9.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz",
"integrity": "sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz",
"integrity": "sha512-1M0NG5VDIvJZEnstHbRdckLZESoJwguinwN8Dhae0j2ZKIQFIV63zxm6Fo6nM4xkgqUr2bbMtV5Dgo+Hy6oo0Q==",
"dependencies": {
"arrify": "^2.0.0",
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"fast-text-encoding": "^1.0.0",
"gaxios": "^5.0.0",
"gcp-metadata": "^5.3.0",
"gcp-metadata": "^5.0.0",
"gtoken": "^6.1.0",
"jws": "^4.0.0",
"lru-cache": "^6.0.0"
@ -445,9 +380,9 @@
}
},
"node_modules/googleapis": {
"version": "122.0.0",
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-122.0.0.tgz",
"integrity": "sha512-n8Gt7j9LzSkhQEGPOrcLBKxllTvW/0v6oILuwszL/zqgelNsGJYXVqPJllgJJ6RM7maJ6T35UBeYqI6GQ/IlJg==",
"version": "113.0.0",
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-113.0.0.tgz",
"integrity": "sha512-gsknEobaFuS+ACraCL3w22pAqeg1HselNhiSjcF5p2d2t2HB3QchbRDrFCfZ0wc4gKLYUBdmGzulCmRRL5qNXw==",
"dependencies": {
"google-auth-library": "^8.0.2",
"googleapis-common": "^6.0.0"
@ -527,6 +462,15 @@
"node": ">= 6"
}
},
"node_modules/ics": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/ics/-/ics-3.1.0.tgz",
"integrity": "sha512-O48TZKyLYagLlXoZwDmjetXc6SoT54wFkTu2MEYe7zse8kL+C/dgSynYCjRG1OTAv3iHtGtG0PWKG81LbcrKFA==",
"dependencies": {
"nanoid": "^3.1.23",
"yup": "^0.32.9"
}
},
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@ -565,6 +509,16 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@ -603,45 +557,37 @@
"node": "*"
}
},
"node_modules/moment-timezone": {
"version": "0.5.43",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.43.tgz",
"integrity": "sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==",
"dependencies": {
"moment": "^2.29.4"
},
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/node-cron": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
"integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==",
"dependencies": {
"uuid": "8.3.2"
"node_modules/nanoclone": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz",
"integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA=="
},
"node_modules/nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/node-cron/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/node-fetch": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz",
"integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==",
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz",
"integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
@ -693,15 +639,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/property-expr": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz",
"integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA=="
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/qs": {
"version": "6.11.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
"integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",
"version": "6.11.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz",
"integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==",
"dependencies": {
"side-channel": "^1.0.4"
},
@ -712,6 +663,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -744,24 +700,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/toposort": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/typescript": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.3.tgz",
"integrity": "sha512-xv8mOEDnigb/tN9PSMTwSEqAnUvkoXMQlicOb0IUVDBSQCgBSaAAROUZYy2IcUy5qU6XajK5jjjO7TMWqBTKZA==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=12.20"
}
},
"node_modules/url-template": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
@ -793,6 +741,23 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/yup": {
"version": "0.32.11",
"resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz",
"integrity": "sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==",
"dependencies": {
"@babel/runtime": "^7.15.4",
"@types/lodash": "^4.14.175",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"nanoclone": "^0.2.1",
"property-expr": "^2.0.4",
"toposort": "^2.0.2"
},
"engines": {
"node": ">=10"
}
}
}
}

View file

@ -2,22 +2,15 @@
"dependencies": {
"axios": "^1.3.4",
"dotenv": "^16.0.3",
"googleapis": "^122.0.0",
"googleapis": "^113.0.0",
"ics": "^3.1.0",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"node-cron": "^3.0.3",
"node-html-parser": "^6.1.5",
"uuid": "^9.0.0"
"node-html-parser": "^6.1.5"
},
"scripts": {
"dev": "nodemon dist/index.js",
"build": "npx tsc",
"start": "node dist/index.js"
"dev": "nodemon public/index.js"
},
"devDependencies": {
"@types/express": "^4.17.17",
"@types/node": "^18.15.5",
"@types/node-cron": "^3.0.11",
"typescript": "^5.0.3"
"@types/node": "^18.15.5"
}
}

View file

@ -1,4 +0,0 @@
#!/bin/bash
docker stop haifareminder
docker rm haifareminder
docker run -p 3000:3000 -d --restart=unless-stopped --name haifareminder docker.io/kfda89/haifareminder

View file

@ -1,106 +1,63 @@
require("dotenv").config();
require('dotenv').config();
import axios from "axios";
import { GoogleCalendarEvent } from "./types";
import moment from "moment";
import { parse } from 'node-html-parser';
import moment from 'moment'; // require
// This calss will be the game source.
// search for upcomming games
export default class GameSource {
async getGamesFromHaifa(logger: Function): Promise<GoogleCalendarEvent[]> {
console.log("Trying to get games from Haifa...");
try {
// Get the current date and time in the required format
const currentDate = moment().format("DD/MM/YYYY HH:mm");
async getGamesFromHaifa(): Promise<GoogleCalendarEvent[]> {
const sourceUrl = `https://mhaifafc.com/games?lang=en`;
const result = await axios.get(sourceUrl);
const parsedResult = parse(result.data.toString())
const gameBoxs = parsedResult.querySelectorAll(".game-box");
// Construct the filters object with the current date
const filters = {
date: {
startDate: currentDate,
endDate: "",
const games: GoogleCalendarEvent[] = [];
for (let gameBox of gameBoxs) {
const teamsPlaying = gameBox.querySelectorAll(".team-name").map((team: any) => team.text);
const regex = /[\r\n\s]+/g;
const gameHeader = gameBox.querySelector(".game-header").text.replace(regex, " ").trim();
const headerSplit = gameHeader.split(",");
// In data, if there is no time, it means it's the last game for the calender
const lastGameForCalender = headerSplit.length < 4;
if (lastGameForCalender) break;
const gameDate = headerSplit[2].trim();
const gameTime = headerSplit[3].trim();
const startOriginal = moment(gameDate + gameTime, "DD/MM/YYYYHH:mm").format("DD/MM/YYYY HH:mm");
const endOriginal = moment(gameDate + gameTime, "DD/MM/YYYYHH:mm").add(2, "hours").format("DD/MM/YYYY HH:mm");
const start = [moment(startOriginal, 'DD/MM/YYYYHH:mm').year(), moment(startOriginal, 'DD/MM/YYYYHH:mm').month() + 1, moment(startOriginal, 'DD/MM/YYYYHH:mm').date(), moment(startOriginal, 'DD/MM/YYYYHH:mm').hour(), moment(startOriginal, 'DD/MM/YYYYHH:mm').minute()];
const end = [moment(endOriginal, 'DD/MM/YYYYHH:mm').year(), moment(endOriginal, 'DD/MM/YYYYHH:mm').month() + 1, moment(endOriginal, 'DD/MM/YYYYHH:mm').date(), moment(endOriginal, 'DD/MM/YYYYHH:mm').hour(), moment(endOriginal, 'DD/MM/YYYYHH:mm').minute()]
games.push({
summary: 'Maccabi Haifa F.C.',
location: "Sammy Ofer Stadium",
description: `${teamsPlaying[0]} vs. ${teamsPlaying[1]}`,
start: {
dateTime: start,
timeZone: 'Asia/Jerusalem'
},
league: "",
session: "",
gamesDirection: "1",
};
// Encode the filters for the URL
const filtersParam = encodeURIComponent(JSON.stringify(filters));
// Construct the API URL with the encoded filters
const sourceUrl = `https://api.mhaifafc.com/api/content/games-lobby?filters=${filtersParam}&start=0&limit=20&sortDirection=ASC&app=web&lang=he`;
// Get the authorization token from environment variables
const authorizationToken = process.env.HAIFA_API_AUTH_TOKEN;
// Set up the request headers
const headers = {
Accept: "*/*",
"Accept-Language": "en-US,en;q=0.7",
Authorization: `Bearer ${authorizationToken}`,
"User-Agent": "Mozilla/5.0",
Origin: "https://www.mhaifafc.com",
Referer: "https://www.mhaifafc.com/",
};
// Make the API request
const response = await axios.get(sourceUrl, {
headers,
responseType: "json", // Ensure the response is parsed as JSON
responseEncoding: "utf8", // Ensure UTF-8 encoding
});
// Extract the games data from the response
const gamesData = response.data.games.items;
const games: GoogleCalendarEvent[] = [];
// Loop through each game and construct the GoogleCalendarEvent objects
for (const game of gamesData) {
const gameDetails = game.gameDetails;
const gameTime = gameDetails.gameTime; // ISO string
const isFinalGameDate = gameDetails.isFinalGameDate;
const gameLocation = gameDetails.gameLocation;
// Skip games without a game time
if (!gameTime) continue;
const hostTeam = game.hostTeam;
const guestTeam = game.guestTeam;
// Get team names
const hostTeamName = hostTeam.teamName;
const guestTeamName = guestTeam.teamName;
const summary = `${hostTeamName} vs. ${guestTeamName}`;
// Include a note if the game date is not final
let description = `${hostTeamName} vs. ${guestTeamName}`;
if (!isFinalGameDate) {
description += " (Date and time are subject to change)";
end: {
dateTime: end,
timeZone: 'Asia/Jerusalem'
}
})
}
return games;
}
// Calculate start and end times
const startDateTime = moment(gameTime).toISOString();
const endDateTime = moment(gameTime).add(2, "hours").toISOString();
// Add the event to the games array
games.push({
summary: summary,
location: gameLocation,
description: description,
start: {
dateTime: startDateTime,
timeZone: "Asia/Jerusalem",
},
end: {
dateTime: endDateTime,
timeZone: "Asia/Jerusalem",
},
});
}
return games;
} catch (error) {
console.error(error);
return [];
getOpponentIndexByStadium(stadium: string) {
if (stadium === "Sammy Ofer Stadium") {
return 1;
} else {
return 0;
}
}
}

View file

@ -1,97 +1,65 @@
import { JWT } from "google-auth-library";
import { google } from "googleapis";
import { GoogleCalendarEvent } from "./types/index";
import { JWT } from 'google-auth-library';
import { google } from 'googleapis';
import { GoogleCalendarEvent } from './types/index';
require("dotenv").config();
require('dotenv').config();
const env = process.env;
export default class GoogleCalendar {
gamesMap: any = {};
clientSecret: string = env.GOOGLE_CLIENT_SECRET;
clientId: string = env.GOOGLE_CLIENT_ID;
calenderId: string = env.GOOGLE_CALENDAR_ID;
calendar: any;
clientEmail: string = env.GOOGLE_CLIENT_EMAIL;
googlePrivateKey: string = env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, "\n");
googlePrivateKey: string = env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, '\n');
token: any;
JWT_client: JWT;
async init() {
console.log("INIT GOOGLE CALENDAR");
console.log("INIT GOOGLE CALENDAR")
const jwtClient = await this.authorize();
this.calendar = google.calendar({ version: "v3", auth: jwtClient });
this.calendar = google.calendar({ version: 'v3', auth: jwtClient });
console.log("DONE INIT GOOGLE CALENDAR")
}
async authorize() {
console.log("AUTHORIZE GOOGLE CALENDAR");
console.log("AUTHORIZE GOOGLE CALENDAR")
this.JWT_client = new JWT({
email: this.clientEmail,
key: this.googlePrivateKey,
scopes: ["https://www.googleapis.com/auth/calendar"],
scopes: [
'https://www.googleapis.com/auth/calendar',
]
});
const { access_token } = await this.JWT_client.authorize();
this.token = access_token;
if (!this.token) {
throw new Error("Failed to connect to google calendar");
throw new Error('Failed to connect to google calendar');
}
console.log("GOOGLE CALENDAR AUTHORIZED SUCCESSFULLY");
console.log("GOOGLE CALENDAR AUTHORIZED SUCCESSFULLY")
return this.JWT_client;
}
async updateNewEvent(upcomingEvents: GoogleCalendarEvent[]) {
// console.log(upcomingEvents)
setTimeout(async () => {
upcomingEvents.forEach(async (event: GoogleCalendarEvent) => {
console.log("UPDATE NEW EVENT", upcomingEvents);
upcomingEvents.forEach((event: GoogleCalendarEvent) => {
console.log("UPDATE NEW EVENT", upcomingEvents)
const options = {
auth: this.JWT_client,
calendarId: this.calenderId,
resource: {
summary: event.summary,
location: event.location,
description: event.description,
start: {
dateTime: event.start.dateTime,
timeZone: "Asia/Jerusalem",
},
end: { dateTime: event.end.dateTime, timeZone: "Asia/Jerusalem" },
sendNotifications: true,
},
};
await this.calendar.events.insert(
options,
function (err: any, event: any) {
if (err) {
console.log(
"There was an error contacting the Calendar service: " + err
);
return;
}
console.log(event.description + " created");
resource: event,
}
this.calendar.events.insert(options, function (err: any, event: any) {
if (err) {
console.log('There was an error contacting the Calendar service: ' + err);
return;
}
);
});
}, 3000);
}
console.log(event.description + ' created');
});
})
}, 3000)
async isDuplicateEvent(startTime: string, endTime: string, title: string) {
if (this.gamesMap[startTime]) {
console.log("duplicate event");
return true;
}
this.gamesMap[startTime] = true;
console.log("checking for duplicate event");
try {
const response = await this.calendar.events.list({
calendarId: this.calenderId,
timeMin: startTime,
timeMax: endTime,
q: title, // Search for events with the same title
});
return response.data.items.length > 0;
} catch (error) {
console.error(error.message);
return false;
}
}
}

44
src/Ics.ts Normal file
View file

@ -0,0 +1,44 @@
import { GoogleCalendarEvent } from "./types";
import * as ics from 'ics';
const uuid = require('uuid').v4();
export default class Ics {
generateIcsOutputFromGames = (games: GoogleCalendarEvent[]) => {
let output = [];
games.forEach((game) => {
output.push(this.generateIcsOutputFromGame(game));
});
const { error, value } = ics.createEvents(output);
if (error) {
console.log(error);
return '';
}
return value;
}
generateIcsOutputFromGame = (game: GoogleCalendarEvent) => {
const { summary, location, description, start, end } = game;
const icsEvent: ics.EventAttributes = {
title: summary,
description,
location,
start: [start.dateTime[0], start.dateTime[1], start.dateTime[2], start.dateTime[3], start.dateTime[4]],
end: [end.dateTime[0], end.dateTime[1], end.dateTime[2], end.dateTime[3], end.dateTime[4]],
url: 'https://mhaifafc.com/',
status: 'CONFIRMED',
busyStatus: 'BUSY',
productId: 'https://mhaifafc.com/',
recurrenceRule: '',
attendees: [],
alarms: [],
categories: [],
organizer: { name: 'Maccabi Haifa F.C.', email: '' },
uid: uuid.toString(),
};
return icsEvent;
}
}

View file

@ -1,74 +1,41 @@
import GameSource from "./GameSource";
import GoogleCalendar from "./GoogleCalendar";
import env from "dotenv";
import cron from 'node-cron';
import fs from 'fs'; // Importing fs for logging
import GoogleCalendar from './GoogleCalendar';
import GameSource from './GameSource';
import fs from 'fs';
import Ics from './Ics';
env.config();
class App {
gameSource: GameSource;
googleToken: string;
googleCalendar: GoogleCalendar;
gameSource: GameSource;
ics: Ics;
googleToken: string;
constructor() {
this.gameSource = new GameSource();
this.googleCalendar = new GoogleCalendar();
this.gameSource = new GameSource();
this.ics = new Ics();
}
async startCronJob() {
this.writeLog('START Haifa Reminder'); // Log when the cron job starts
const newGamesAdded = [];
async init() {
console.log("INIT APP")
await this.googleCalendar.init();
try {
const games = await this.gameSource.getGamesFromHaifa(this.writeLog);
for (const game of games) {
const isDuplicateEvent = await this.googleCalendar.isDuplicateEvent(
game.start.dateTime,
game.end.dateTime,
game.summary
);
console.log(game)
if (!isDuplicateEvent) {
newGamesAdded.push(game);
console.log("Event does not exist");
await this.googleCalendar.updateNewEvent([game]);
} else {
console.log("Event already exists");
}
}
if (newGamesAdded.length > 0) {
console.log("New games added:", newGamesAdded);
} else {
console.log("No new games were Added!");
}
this.writeLog('Successfully ran project');
} catch (error) {
this.writeLog("Error in cron job:" + error.message);
} finally {
this.writeLog('END CRON JOB'); // Log when the cron job ends
}
}
writeLog(message) {
const timestamp = new Date().toISOString();
const logMessage = `${timestamp} - ${message}\n`;
// Write to log file synchronously
fs.appendFileSync('cron.log', logMessage);
console.log(logMessage); // Optional: also log to console
async getNewGamesAndUpdateCalendar() {
console.log("GET NEW GAMES AND UPDATE CALENDAR")
const games = await this.gameSource.getGamesFromHaifa();
this.googleCalendar.updateNewEvent(games);
}
}
const app = new App();
cron.schedule('* * * * *', () => {
console.log('Running startCronJob at 10:00 AM Jerusalem time');
app.startCronJob().catch((error) => {
console.error("Error in scheduled cron job:", error.message);
app.writeLog(`ERROR: ${error.message}`); // Log any errors
});
}, {
scheduled: true,
timezone: "Asia/Jerusalem"
});
const start = async () => {
const outputFileLocation = 'maccabi-haifa-fc.ics';
const games = await app.gameSource.getGamesFromHaifa();
const icsEvents = app.ics.generateIcsOutputFromGames(games);
fs.writeFileSync(outputFileLocation, icsEvents);
}
start();

View file

@ -3,11 +3,11 @@ export interface GoogleCalendarEvent {
location: string;
description: string;
start: {
dateTime: string;
dateTime: number[];
timeZone: string;
};
end: {
dateTime: string;
dateTime: number[];
timeZone: string;
};
}

View file

@ -1,3 +0,0 @@
#!/bin/bash
docker run -p 3000:3000 -d --restart=unless-stopped --name haifareminder docker.io/kfda89/haifareminder

View file

@ -5,10 +5,7 @@
"esModuleInterop": true,
"target": "ES2019",
"moduleResolution": "node",
"outDir": "./dist",
"outDir": "./public",
"rootDir": "./src"
},
"include": [
"src/**/*"
]
}
}