├── Procfile ├── images ├── 1.jpg ├── 2.jpg ├── 3.jpg ├── 4.jpg ├── header.png ├── web1.png ├── web2.png ├── web3.png └── flowchart.jpg ├── src ├── public │ ├── images │ │ ├── about.jpg │ │ └── home.png │ ├── script │ │ └── main.js │ └── css │ │ └── styles.css ├── commands │ ├── loadCommands.js │ ├── unsubscribe.js │ ├── help.js │ ├── notifyMe.js │ ├── README.md │ ├── set.js │ ├── common.js │ └── check.js ├── models │ ├── cache.js │ ├── district.js │ └── dataSchema.js ├── config.js ├── api │ ├── helper.js │ └── api.js ├── discord.js ├── index.js ├── cron.js ├── dbCrud.js └── index.html ├── .jshintrc ├── .github ├── workflows │ └── test.yml └── dependabot.yml ├── LICENSE ├── package.json ├── .gitignore └── README.md /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start -------------------------------------------------------------------------------- /images/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UKnowWhoIm/cowin-discord-bot/HEAD/images/1.jpg -------------------------------------------------------------------------------- /images/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UKnowWhoIm/cowin-discord-bot/HEAD/images/2.jpg -------------------------------------------------------------------------------- /images/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UKnowWhoIm/cowin-discord-bot/HEAD/images/3.jpg -------------------------------------------------------------------------------- /images/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UKnowWhoIm/cowin-discord-bot/HEAD/images/4.jpg -------------------------------------------------------------------------------- /images/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UKnowWhoIm/cowin-discord-bot/HEAD/images/header.png -------------------------------------------------------------------------------- /images/web1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UKnowWhoIm/cowin-discord-bot/HEAD/images/web1.png -------------------------------------------------------------------------------- /images/web2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UKnowWhoIm/cowin-discord-bot/HEAD/images/web2.png -------------------------------------------------------------------------------- /images/web3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UKnowWhoIm/cowin-discord-bot/HEAD/images/web3.png -------------------------------------------------------------------------------- /images/flowchart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UKnowWhoIm/cowin-discord-bot/HEAD/images/flowchart.jpg -------------------------------------------------------------------------------- /src/public/images/about.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UKnowWhoIm/cowin-discord-bot/HEAD/src/public/images/about.jpg -------------------------------------------------------------------------------- /src/public/images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UKnowWhoIm/cowin-discord-bot/HEAD/src/public/images/home.png -------------------------------------------------------------------------------- /src/commands/loadCommands.js: -------------------------------------------------------------------------------- 1 | // Import all command files 2 | import "./check.js"; 3 | import "./help.js"; 4 | import "./notifyMe.js"; 5 | import "./set.js"; 6 | import "./unsubscribe.js"; -------------------------------------------------------------------------------- /src/models/cache.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const dataSchema = new mongoose.Schema({ 4 | district: { type: Number, require: true, unique: true }, 5 | data: { type: Object, require: true } 6 | }); 7 | 8 | export const CacheModel = mongoose.model("Cache", dataSchema); 9 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esversion": 11, 4 | "camelcase": true, 5 | "eqeqeq": true, 6 | "evil": true, 7 | "freeze": true, 8 | "indent": 4, 9 | "newcap": true, 10 | "quotmark": "double", 11 | "maxlen": 120, 12 | "eqnull": true, 13 | "funcscope": true, 14 | "globals": {} 15 | } -------------------------------------------------------------------------------- /src/models/district.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const dataSchema = new mongoose.Schema({ 4 | districtID: { type: Number, require: true, unique: true }, 5 | districtName: { type: String, require: true }, 6 | state: {type: String, require: true} 7 | }); 8 | 9 | export const DistrictModel = mongoose.model("DistrictModel", dataSchema); -------------------------------------------------------------------------------- /src/models/dataSchema.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const dataSchema = new mongoose.Schema({ 4 | userID: { type: String, require: true }, 5 | district: { type: String }, 6 | pin: { type: Number }, 7 | age: { type: Number }, 8 | notify: { type: Boolean, default: false }, 9 | }); 10 | 11 | export const DataModel = mongoose.model("DataModel", dataSchema); 12 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | test: 7 | if: "! contains(toJSON(github.event.commits.*.message), '[skip-ci]')" 8 | name: Run Linter 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: "16" 15 | - run: npm install 16 | - run: npm test -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import { config } from "dotenv"; 2 | config(); 3 | 4 | export const DEBUG = (process.env.DEBUG ?? "true") !== "false"; 5 | 6 | export const mongoURL = process.env.MONGODB_SRV; 7 | 8 | export const BOT_TOKEN = process.env.BOT_TOKEN; 9 | 10 | export const GUILD_ID = process.env.GUILD_ID; 11 | 12 | export const API_BASE = process.env.API_BASE; 13 | 14 | export const IS_PROXY = (process.env.IS_PROXY ?? "false") !== "false"; -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /src/commands/unsubscribe.js: -------------------------------------------------------------------------------- 1 | import { updateUserData } from "../dbCrud.js"; 2 | import { Command, getUserID, sendReply } from "./common.js"; 3 | 4 | const cmdName = "unsubscribe"; 5 | 6 | const commandData = { 7 | "name": cmdName, 8 | "description": "Unsubscribe from hourly alerts" 9 | }; 10 | 11 | function unsubscribe(bot, interaction){ 12 | updateUserData(getUserID(interaction), {"notify": false}); 13 | sendReply(bot, interaction, 14 | "You have been unsubscribed from hourly notifications"); 15 | } 16 | 17 | new Command(cmdName,commandData, unsubscribe); -------------------------------------------------------------------------------- /src/commands/help.js: -------------------------------------------------------------------------------- 1 | import { sendReply, Command } from "./common.js"; 2 | 3 | const helpText = `Available Commands 4 | 5 | /set district : Set desired district 6 | /set age : Set vaccinator's age. 7 | /notify-me: Notify user hourly if there are slots available for a week. 8 | /check : Check vaccine availability now for these parameters.`; 9 | 10 | const cmdName = "help"; 11 | 12 | const commandData = { 13 | "name": cmdName, 14 | "description": "Help", 15 | }; 16 | 17 | function showHelp(bot, interaction){ 18 | sendReply(bot, interaction, helpText); 19 | } 20 | 21 | new Command(cmdName, commandData, showHelp); -------------------------------------------------------------------------------- /src/api/helper.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { API_BASE } from "../config.js"; 3 | 4 | const api = axios.create({ 5 | baseURL: API_BASE, 6 | headers: { 7 | accept: "application/json", 8 | "User-Agent": 9 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:89.0) Gecko/20100101 Firefox/89.0", 10 | }, 11 | }); 12 | 13 | const getStatesPath = "admin/location/states/"; 14 | const getDistrictsByStatePath = "admin/location/districts"; 15 | const getCalenderByPinPath = "appointment/sessions/public/calendarByPin?"; 16 | const getCalenderByDistrictPath = 17 | "appointment/sessions/public/calendarByDistrict?"; 18 | 19 | export { 20 | api, 21 | getStatesPath, 22 | getDistrictsByStatePath, 23 | getCalenderByPinPath, 24 | getCalenderByDistrictPath, 25 | }; 26 | -------------------------------------------------------------------------------- /src/public/script/main.js: -------------------------------------------------------------------------------- 1 | const showMenu = (toggleId, navId) => { 2 | const toggle = document.getElementById(toggleId), 3 | nav = document.getElementById(navId); 4 | 5 | if (toggle && nav) { 6 | toggle.addEventListener("click", () => { 7 | nav.classList.toggle("show"); 8 | }); 9 | } 10 | }; 11 | showMenu("nav-toggle", "nav-menu"); 12 | 13 | const navLink = document.querySelectorAll(".nav__link"); 14 | 15 | function linkAction() { 16 | /*Active link*/ 17 | navLink.forEach((n) => n.classList.remove("active")); 18 | this.classList.add("active"); 19 | 20 | /*Remove menu mobile*/ 21 | const navMenu = document.getElementById("nav-menu"); 22 | navMenu.classList.remove("show"); 23 | } 24 | navLink.forEach((n) => n.addEventListener("click", linkAction)); 25 | -------------------------------------------------------------------------------- /src/discord.js: -------------------------------------------------------------------------------- 1 | import { Client } from "discord.js"; 2 | import { Command } from "./commands/common.js"; 3 | import "./commands/loadCommands.js"; 4 | import { DEBUG, BOT_TOKEN, GUILD_ID } from "./config.js"; 5 | 6 | const bot = new Client(); 7 | 8 | const getApp = (guildId) => { 9 | const app = bot.api.applications(bot.user.id); 10 | 11 | if(DEBUG) 12 | return app.guilds(guildId); 13 | 14 | return app; 15 | 16 | }; 17 | 18 | const botCmdMap = Command.getCallBackMap(); 19 | 20 | bot.once("ready", async () => { 21 | 22 | console.log(`Logged in as ${bot.user.tag}!`); 23 | 24 | Command.initialize(getApp(GUILD_ID).commands.post); 25 | 26 | bot.ws.on("INTERACTION_CREATE", async (interaction) => { 27 | const command = interaction.data.name; 28 | 29 | if(botCmdMap[command]) 30 | botCmdMap[command](bot, interaction); 31 | }); 32 | }); 33 | 34 | bot.login(BOT_TOKEN); -------------------------------------------------------------------------------- /src/commands/notifyMe.js: -------------------------------------------------------------------------------- 1 | import { readUserData, updateUserData } from "../dbCrud.js"; 2 | import { Command, sendReply } from "./common.js"; 3 | 4 | const cmdName = "notify-me"; 5 | 6 | const commandData = { 7 | "name": cmdName, 8 | "description": "Send hourly alerts if slot is available" 9 | }; 10 | 11 | async function addUserToListners(bot, interaction){ 12 | const user = interaction.member.user; 13 | 14 | const userInDB = await readUserData(user.id); 15 | 16 | if(!userInDB?.district) 17 | return sendReply(bot, interaction, 18 | "Set your district using /set command to recieve hourly notifications"); 19 | if(!userInDB?.age) 20 | return sendReply(bot, interaction, 21 | "Set your age using /set command to recieve hourly notifications"); 22 | 23 | updateUserData(user.id, {"notify": true}); 24 | sendReply(bot, interaction, `You have subscribed to hourly notifications`); 25 | 26 | } 27 | 28 | new Command(cmdName, commandData, addUserToListners); -------------------------------------------------------------------------------- /src/commands/README.md: -------------------------------------------------------------------------------- 1 | ## Discord Commands 2 | 3 | This directoy contains all commands used by the COWIN-bot. All commands are slash commands to improve UX. 4 | 5 | ## Table of Contents 6 | 7 | 1. [/check](#check) 8 | 2. [/help](#help) 9 | 3. [/notify-me](#notify-me) 10 | 4. [/set](#set) 11 | 12 | ### check 13 | 14 | This command is used to manually check for available slots with the given parameters. 15 | 16 | Usage 17 | ``` 18 | /check 19 | ``` 20 | - If district and age isn't provided, values from db will be loaded. 21 | - If user has no record in db, error message will be shown. 22 | 23 | ### help 24 | 25 | Shows a list of available commands. 26 | 27 | Usage 28 | ``` 29 | /help 30 | ``` 31 | 32 | ### notify-me 33 | 34 | Enables hourly notifications for the user if slot is avaiilable. 35 | 36 | Requires age and ditrict to be set using [/set](#set) command. Will prompt to set values if empty. 37 | 38 | Usage 39 | ``` 40 | /notify-me 41 | ``` 42 | 43 | ### set 44 | This command is used to set the user's age and district in the db. 45 | 46 | Usage 47 | ``` 48 | /set age 49 | /set district 50 | ``` 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sidharth Ajithkumar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cowin-discord-bot", 3 | "version": "0.3.0", 4 | "description": "A discord app to get upcoming COWIN appointments based on commands", 5 | "main": "./src/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "lint": "jshint src/*.js src/commands/*.js src/api/*.js src/models/*.js", 9 | "test": "npm run lint", 10 | "start": "node ./src/index.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/UKnowWhoIm/cowin-discord-bot.git" 15 | }, 16 | "keywords": [ 17 | "discord-bot", 18 | "cowin", 19 | "vaccine" 20 | ], 21 | "author": "UKnowWhoIm", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/UKnowWhoIm/cowin-discord-bot/issues" 25 | }, 26 | "homepage": "https://github.com/UKnowWhoIm/cowin-discord-bot#readme", 27 | "dependencies": { 28 | "axios": "^0.21.1", 29 | "discord.js": "^12.5.3", 30 | "dotenv": "^10.0.0", 31 | "express": "^4.17.1", 32 | "jshint": "^2.13.0", 33 | "luxon": "^1.27.0", 34 | "mongoose": "^5.12.13", 35 | "node-cron": "^3.0.0", 36 | "path": "^0.12.7", 37 | "string-similarity": "^4.0.4" 38 | }, 39 | "devDependencies": { 40 | "@types/agenda": "^4.1.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { fileURLToPath } from "url"; 3 | import { dirname, join } from "path"; 4 | import mongoose from "mongoose"; 5 | import { job } from "./cron.js"; 6 | import cron from "node-cron"; 7 | import { DEBUG, mongoURL } from "./config.js"; 8 | 9 | const app = express(); 10 | const port = process.env.PORT ?? "3000"; 11 | 12 | async function start() { 13 | try { 14 | await mongoose.connect(mongoURL, { 15 | useCreateIndex: true, 16 | useNewUrlParser: true, 17 | useUnifiedTopology: true, 18 | useFindAndModify: false, 19 | }); 20 | } catch (error) { 21 | console.log(error); 22 | } 23 | } 24 | 25 | start(); 26 | if (DEBUG) console.log("DEBUG Mode"); 27 | 28 | const __dirname = dirname(fileURLToPath(import.meta.url)); // jshint ignore:line 29 | 30 | app.use(express.static(__dirname + "/public")); 31 | app.get("/", (_, res) => { 32 | res.sendFile(join(__dirname + "/index.html")); 33 | }); 34 | 35 | app.listen(port, () => { 36 | console.log(`Example app listening at http://localhost:${port}`); 37 | }); 38 | 39 | // In debug mode send notif every minute 40 | cron.schedule(DEBUG ? "*/5 * * * *" : "*/15 * * * *", () => { 41 | console.log("Starting notifications"); 42 | job(); 43 | }, 44 | { 45 | "timezone": "Asia/Kolkata" 46 | } 47 | ); 48 | 49 | import "./discord.js"; 50 | -------------------------------------------------------------------------------- /src/cron.js: -------------------------------------------------------------------------------- 1 | import { DateTime } from "luxon"; 2 | import { bulkCreateCacheDistrict, clearCache, getUsersByFilter } from "./dbCrud.js"; 3 | import { getCalenderByDistrict, getDataFromResponse } from "./api/api.js"; 4 | import { Client } from "discord.js"; 5 | import { processResults } from "./commands/common.js"; 6 | import { BOT_TOKEN } from "./config.js"; 7 | 8 | async function initilizeBot() { 9 | const TOKEN = BOT_TOKEN; 10 | const bot = new Client(); 11 | await bot.login(TOKEN); 12 | return bot; 13 | } 14 | 15 | function padLeft(num) { 16 | // pad 0 to left 17 | if (num < 10) return `0${num}`; 18 | return `${num}`; 19 | } 20 | 21 | function getDate() { 22 | const date = new Date(); 23 | return `${padLeft(date.getDate())}-${padLeft( 24 | date.getMonth() + 1 25 | )}-${date.getFullYear()}`; 26 | } 27 | 28 | function processDataForStorage(cached) { 29 | // Convert to list of objects for db storage 30 | let finalData = []; 31 | for (const key of Object.keys(cached)) { 32 | finalData.push({ 33 | district: key, 34 | data: cached[key], 35 | }); 36 | } 37 | return finalData; 38 | } 39 | 40 | export async function job() { 41 | await clearCache(); 42 | let cache = {}; 43 | const bot = await initilizeBot(); 44 | const subscribedUsers = await getUsersByFilter({ notify: true }); 45 | for (const user of subscribedUsers) { 46 | if (cache[user.district] === undefined) { 47 | const apiFetch = await getCalenderByDistrict( 48 | user.district, 49 | getDate(), 50 | null, 51 | false 52 | ); 53 | if (apiFetch.status) cache[user.district] = apiFetch.result; 54 | } 55 | if (cache[user.district]) { 56 | const results = getDataFromResponse( 57 | cache[user.district], 58 | user.age, 59 | DateTime.now().toFormat("dd-MM-yyyy"), 60 | ); 61 | processResults(bot, null, results, user.userID); 62 | } 63 | } 64 | bulkCreateCacheDistrict(processDataForStorage(cache)); 65 | } 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | app.yaml 3 | 4 | .gcloudignore 5 | 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | 82 | # Next.js build output 83 | .next 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | -------------------------------------------------------------------------------- /src/commands/set.js: -------------------------------------------------------------------------------- 1 | import { Command, sendReply, parseDistrict, getUserID, getAge } from "./common.js"; 2 | import { createUser, updateUserData, readUserData } from "../dbCrud.js"; 3 | 4 | const name = "set"; 5 | 6 | const commandData = { 7 | "name": "set", 8 | "description": "Set age/district of vaccinator", 9 | "options": [ 10 | { 11 | "name": "age", 12 | "description": "Set Vaccinator's age", 13 | "type": 1, // Subcommand 14 | "options": [ 15 | { 16 | "name": "age", 17 | "description": "Vaccinator's age", 18 | "required": true, 19 | "type": 4 // Integer 20 | } 21 | ] 22 | }, 23 | { 24 | "name": "district", 25 | "description": "Set Preferred District", 26 | "type": 1, // Subcommand 27 | "options": [ 28 | { 29 | "name": "district", 30 | "description": "Preferred district", 31 | "required": true, 32 | "type": 3 // String 33 | } 34 | ] 35 | }, 36 | ] 37 | }; 38 | 39 | function extractValueOfSubCommand(data){ 40 | return data.options[0].options[0].value; 41 | } 42 | 43 | function extractSubCommand(data){ 44 | return data.options[0].name; 45 | } 46 | 47 | async function setAgeOrDistrict(bot, interaction){ 48 | const data = interaction.data; 49 | const subCommand = extractSubCommand(data); 50 | let subValue = extractValueOfSubCommand(data); 51 | let userData = {}; 52 | // data = {subCommand: subValue} makes the key "subCommand" 53 | let msg; 54 | 55 | if(subCommand === "age"){ 56 | subValue = getAge(subValue); 57 | msg = `Vaccination age slot set to ${subValue}`; 58 | } 59 | else if(subCommand === "district"){ 60 | let districtData = await parseDistrict(subValue); 61 | if(!districtData.code) 62 | return sendReply(bot, interaction, "District not found"); 63 | subValue = districtData.code; 64 | msg = `District set to ${districtData.name}`; 65 | } 66 | 67 | userData[subCommand] = subValue; 68 | const userInDb = await readUserData(getUserID(interaction)); 69 | if(userInDb){ 70 | await updateUserData(userInDb.userID, userData); 71 | } 72 | else{ 73 | await createUser(interaction.member.user.id, userData); 74 | } 75 | sendReply(bot, interaction, msg); 76 | } 77 | 78 | new Command(name, commandData, setAgeOrDistrict); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Cowin Assist](images/header.png) 2 | 3 | ### Table of contents 4 | 5 | 6 | 7 | - [What is Cowin Assist?](#what-is-cowin-assist) 8 | - [Features](#features) 9 | - [Workflow](#workflow) 10 | - [How to add the bot to your server?](#how-to-add-the-bot-to-your-server) 11 | - [Demo](#demo) 12 | - [BOT](#bot) 13 | - [Web Interface](#web-interface) 14 | - [Contributing](#contributing) 15 | - [Requirements](#requirements) 16 | - [License](#license) 17 | 18 | 19 | ### What is Cowin Assist? 20 | 21 | Cowin Assist is a discord chat-bot that lets the user track the availability of covid vaccination centres in India via the [CoWin API](https://apisetu.gov.in/public/api/cowin#/). It has a bunch of commands through which the user can set/update their desired filters to receive up to date information regarding the vaccine centres. 22 | 23 | ### Features 24 | 25 | The Cowin Assist lets you, 26 | 27 | - Add the discord bot to a server, 28 | - Register your district and age (just once, we'll save it for you), 29 | - Update your saved data if needed, 30 | - Check for the availability of a vaccine centre using the data provided, 31 | - Navigate to COWIN site if slot available, 32 | - Subscribe for an hourly update on open slots if available, 33 | 34 | For detailed information regarding the slash commands check [this](commands/README.md) out. 35 | 36 | ### Workflow 37 | 38 | For detailed understanding of the working of the project refer the [flowchart](images/flowchart.jpg) that we have prepared. 39 | 40 | ### How to add the bot to your server? 41 | 42 | 1. Ensure that you have the latest version of the discord client 43 | 2. Visit the **Cowin Assist Bot's** [website](https://cowin-discord-bot.el.r.appspot.com/) and click on the **Add to server** button. 44 | 3. Then login or register with your discord server. 45 | 4. After logging in, select the server from the list where you wish to display the Bot. 46 | 5. Provide authorization for the Bot to regulate and control your discord server. 47 | 6. Check the **Add the bot to a server** and press the **Authorize** option. 48 | 49 | ### Demo 50 | 51 | #### Bot 52 | 53 |
54 | 55 | 56 | 57 | 58 |
59 | 60 | #### Web Interface 61 | 62 |
63 | 64 | 65 |
66 | 67 | ### Contributing 68 | 69 | #### Requirements 70 | 71 | - Node.js - 14.17.0 72 | - Discord.js - 12.5.3 73 | - MongoDB 74 | 75 | ### License 76 | 77 | This project is licensed under the permissive open-source [MIT license](LICENSE). 78 | -------------------------------------------------------------------------------- /src/dbCrud.js: -------------------------------------------------------------------------------- 1 | import { CacheModel } from "./models/cache.js"; 2 | import { DataModel } from "./models/dataSchema.js"; 3 | import { DistrictModel } from "./models/district.js"; 4 | 5 | // set 6 | export async function createUser(userID, userData) { 7 | const info = { 8 | userID: userID, 9 | district: userData.district, 10 | pin: userData.pin, 11 | age: userData.age, 12 | notify: userData.notify, 13 | }; 14 | 15 | try { 16 | let obj = await DataModel.create(info); 17 | obj.save(); 18 | return String(obj); 19 | } catch (error) { 20 | console.log(error); 21 | } 22 | } 23 | 24 | // check 25 | export async function readUserData(userID) { 26 | try { 27 | let obj = await DataModel.findOne({ userID: userID }); 28 | 29 | return obj; 30 | } catch (error) { 31 | console.log(error); 32 | } 33 | } 34 | 35 | // set 36 | export async function updateUserData(userID, data) { 37 | const filter = { userID: userID }; 38 | try { 39 | await DataModel.findOneAndUpdate(filter, data, { upsert: true }); 40 | } catch (error) { 41 | console.log(error); 42 | } 43 | } 44 | 45 | export async function deleteUserData(userID) { 46 | try { 47 | await DataModel.deleteOne(userID); 48 | } catch (error) { 49 | console.log(error); 50 | } 51 | } 52 | 53 | export async function getUsersByFilter(filter) { 54 | try { 55 | let obj = await DataModel.find(filter); 56 | 57 | return obj; 58 | } catch (error) { 59 | console.log(error); 60 | } 61 | } 62 | 63 | export async function getDistricts() { 64 | try { 65 | return await DistrictModel.find({}); 66 | } catch (error) { 67 | console.log(error); 68 | } 69 | } 70 | 71 | export async function getAllCachedDistricts() { 72 | try { 73 | return await CacheModel.find({}); 74 | } catch (error) { 75 | console.log(error); 76 | } 77 | } 78 | 79 | export async function getCachedDistrict(districtId) { 80 | try { 81 | return await CacheModel.findOne({ district: districtId }); 82 | } catch (error) { 83 | console.log(error); 84 | } 85 | } 86 | 87 | export async function createCacheDistrict(data) { 88 | try { 89 | let obj = await CacheModel.create(data); 90 | await obj.save(); 91 | } catch (error) { 92 | console.log(error); 93 | } 94 | } 95 | 96 | export async function bulkCreateCacheDistrict(data) { 97 | try { 98 | await CacheModel.insertMany(data); 99 | } catch (error) { 100 | console.log(error); 101 | } 102 | } 103 | 104 | export async function clearCache() { 105 | try { 106 | await CacheModel.deleteMany({}); 107 | } catch (error) { 108 | console.log(error); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/commands/common.js: -------------------------------------------------------------------------------- 1 | import { findBestMatch } from "string-similarity"; 2 | import { getDistricts } from "../dbCrud.js"; 3 | 4 | export class Command { 5 | static instances = []; // jshint ignore:line 6 | 7 | constructor(cmdName, cmdData, cmdCallBack) { 8 | this.name = cmdName; 9 | this.data = cmdData; 10 | this.callback = cmdCallBack; 11 | Command.instances.push(this); 12 | } 13 | 14 | static initialize(callback) { 15 | // Create all commands 16 | Command.instances.forEach(async (instance) => { 17 | await callback({ 18 | "data": instance.data 19 | }); 20 | }); 21 | } 22 | 23 | static getCallBackMap() { 24 | let map = {}; 25 | Command.instances.forEach((instance) => { 26 | map[instance.name] = instance.callback; 27 | }); 28 | return map; 29 | } 30 | } 31 | 32 | 33 | export function sendReply(bot, interaction, msg) { 34 | bot.api.interactions(interaction.id, interaction.token).callback.post({ 35 | data: { 36 | type: 4, 37 | data: { 38 | content: msg 39 | } 40 | } 41 | }); 42 | } 43 | export const getUserID = (interaction) => interaction.member.user.id; 44 | 45 | export const getAge = (age) => { 46 | switch (true) { 47 | case age >= 40 && age <= 44: 48 | return 40; 49 | case age >= 45: 50 | return 45; 51 | default: 52 | return 18; 53 | } 54 | }; 55 | 56 | export async function sendDM(bot, userId, msg) { 57 | const user = await bot.users.fetch(userId); 58 | user.send(msg); 59 | } 60 | 61 | export async function parseDistrict(district) { 62 | // TODO: return multiple matches if it exists. 63 | const minDiceCoeff = 0.8; 64 | const districtsInDb = await getDistricts(); 65 | const districtNames = districtsInDb.map((districtDoc) => districtDoc.districtName.toLowerCase()); 66 | const matches = findBestMatch(district.toLowerCase(), districtNames).ratings 67 | .filter((match) => match.rating >= minDiceCoeff); 68 | 69 | return { 70 | "name": matches[0]?.target, 71 | "code": districtsInDb.filter((district) => 72 | district.districtName.toLowerCase() === matches[0]?.target 73 | )[0]?.districtID 74 | }; 75 | } 76 | 77 | function getSessionText(sessions) { 78 | let result = ""; 79 | sessions.forEach((session) => result += ` 80 | -------- DATE: ${session.date} -------- 81 | Age Limit: ${session.ageLimit} 82 | Vaccine: ${session.vaccine} 83 | Dose1 Capacity: ${session.dose1Capacity} 84 | Dose2 Capacity: ${session.dose2Capacity}`); 85 | return result; 86 | } 87 | 88 | export async function processResults(bot, interaction, results, userID) { 89 | if (results.length > 0) { 90 | if (!userID) 91 | sendReply(bot, interaction, 92 | "Slots available"); 93 | for (const center of results) 94 | await sendDM(bot, userID ?? getUserID(interaction), ` 95 | <<<<<<<< New Center Available >>>>>>>> 96 | Center Name: ${center.name} 97 | Address: ${center.address} 98 | Pincode: ${center.pincode} 99 | Fee Type: ${center.feeType} ${getSessionText(center.sessions)}`); 100 | 101 | return sendDM(bot, userID ?? getUserID(interaction), 102 | `Register Now: https://www.cowin.gov.in/home 103 | ***I'm a bot, so don't reply***.... 104 | `); 105 | } 106 | if (!userID) 107 | return sendReply(bot, interaction, 108 | "No slots available"); 109 | } -------------------------------------------------------------------------------- /src/commands/check.js: -------------------------------------------------------------------------------- 1 | import { 2 | Command, 3 | getAge, 4 | getUserID, 5 | parseDistrict, 6 | processResults, 7 | sendReply, 8 | } from "./common.js"; 9 | import { 10 | createCacheDistrict, 11 | getCachedDistrict, 12 | readUserData, 13 | } from "../dbCrud.js"; 14 | import { getCalenderByDistrict, getDataFromResponse } from "../api/api.js"; 15 | 16 | const cmdName = "check"; 17 | 18 | const cmdData = { 19 | name: cmdName, 20 | description: "Check availabilty of slots on a particular date", 21 | options: [ 22 | { 23 | name: "date", 24 | description: "Enter date for checking(DD-MM-YYYY)", 25 | required: true, 26 | type: 3, // String 27 | }, 28 | { 29 | name: "district", 30 | description: "Enter district, if omitted will use from history", 31 | required: false, 32 | type: 3, // String 33 | }, 34 | { 35 | name: "age", 36 | description: "Enter age, if omitted will use from history", 37 | required: false, 38 | type: 3, // String 39 | }, 40 | ], 41 | }; 42 | 43 | async function checkAvailability(bot, interaction) { 44 | const options = interaction.data.options; 45 | let date = options.filter((option) => option.name === "date")[0].value; 46 | 47 | let district = options.filter((option) => option.name === "district")[0] 48 | ?.value; 49 | let age = options.filter((option) => option.name === "age")[0]?.value; 50 | 51 | const dateRegex = /^\d{2}-\d{2}-\d{4}$/; 52 | const userInDb = await readUserData(getUserID(interaction)); 53 | 54 | if (!district) { 55 | if (!userInDb?.district) 56 | return sendReply( 57 | bot, 58 | interaction, 59 | "Set your district using /set command or enter them while using /check" 60 | ); 61 | district = userInDb.district; 62 | } else { 63 | let districtData = await parseDistrict(district); 64 | if (!districtData.code) 65 | return sendReply(bot, interaction, "District not found"); 66 | district = districtData.code; 67 | } 68 | if (!age) { 69 | if (!userInDb?.age) 70 | return sendReply( 71 | bot, 72 | interaction, 73 | "Set your age using /set command or enter it while using /check" 74 | ); 75 | age = userInDb.age; 76 | } else age = getAge(age); 77 | 78 | if (!dateRegex.test(date)) 79 | return sendReply( 80 | bot, 81 | interaction, 82 | "Invalid date, please enter date in format dd-mm-yyyy" 83 | ); 84 | 85 | const cachedDistrict = await getCachedDistrict(district); 86 | let result; 87 | 88 | if (cachedDistrict) 89 | result = cachedDistrict.data; 90 | else { 91 | const apiFetch = await getCalenderByDistrict(district, date, age); 92 | if (apiFetch.status) { 93 | result = apiFetch.result; 94 | createCacheDistrict({ district: district, data: result }).then( 95 | console.log(`Cached district ${district}`) 96 | ); 97 | } else 98 | sendReply( 99 | bot, 100 | interaction, 101 | "API is not responding, Please try again later" 102 | ); 103 | } 104 | if (result) 105 | processResults( 106 | bot, 107 | interaction, 108 | getDataFromResponse(result, age, date) 109 | ); 110 | } 111 | 112 | new Command(cmdName, cmdData, checkAvailability); 113 | -------------------------------------------------------------------------------- /src/api/api.js: -------------------------------------------------------------------------------- 1 | import { IS_PROXY } from "../config.js"; 2 | import { DateTime } from "luxon"; 3 | import { 4 | api, 5 | getStatesPath, 6 | getDistrictsByStatePath, 7 | getCalenderByPinPath, 8 | getCalenderByDistrictPath, 9 | } from "./helper.js"; 10 | 11 | function getDataFromResponse(res, age, date) { 12 | let data = []; 13 | let flag = 0; 14 | for (const r of res) { 15 | /* jshint ignore:start */ 16 | flag = 0; 17 | let result = { 18 | centerId: r.center_id, 19 | name: r.name, 20 | address: r.address, 21 | pincode: r.pincode, 22 | blockName: r.block_name, 23 | from: r.from, 24 | to: r.to, 25 | feeType: r.fee_type, 26 | }; 27 | result.sessions = []; 28 | for (const session of r.sessions) { 29 | let d1 = DateTime.fromFormat(session.date, "dd-MM-yyyy").startOf( 30 | "day" 31 | ); 32 | let d2 = DateTime.fromFormat(date, "dd-MM-yyyy").startOf("day"); 33 | if (d1 >= d2) { 34 | let center = { 35 | date: session.date, 36 | totalCapacity: session.available_capacity, 37 | dose1Capacity: session.available_capacity_dose1, 38 | dose2Capacity: session.available_capacity_dose2, 39 | ageLimit: session.min_age_limit, 40 | vaccine: session.vaccine, 41 | slots: session.slots, 42 | }; 43 | if (center.totalCapacity > 0 && age === center.ageLimit) { 44 | result.sessions.push(center); 45 | flag = 1; 46 | } 47 | } 48 | } 49 | /* jshint ignore:end */ 50 | if (flag === 1) 51 | data.push(result); 52 | } 53 | return data; 54 | } 55 | 56 | async function getStates() { 57 | try { 58 | const res = await api.get(getStatesPath); 59 | 60 | if (res.status === 200) { 61 | if (res.data.states !== undefined) 62 | return { 63 | status: true, 64 | result: res.data.states, 65 | }; 66 | else throw new Error("State is undefined"); 67 | } else { 68 | if (res.status === 401) throw new Error("Unauthenticated access!"); 69 | 70 | if (res.status === 500) throw new Error("Internal Server Error"); 71 | } 72 | } catch (error) { 73 | return { 74 | status: false, 75 | result: error.message, 76 | }; 77 | } 78 | } 79 | 80 | async function getDistrictsByStateId(id) { 81 | try { 82 | const res = await api.get(`${getDistrictsByStatePath}/${id}`); 83 | 84 | if (res.status === 200) { 85 | if (res.data.districts !== undefined) 86 | return { 87 | status: true, 88 | result: res.data.districts, 89 | }; 90 | else throw new Error("District is undefined"); 91 | } else { 92 | if (res.status === 401) throw new Error("Unauthenticated access!"); 93 | 94 | if (res.status === 500) throw new Error("Internal Server Error"); 95 | } 96 | } catch (e) { 97 | return { 98 | status: false, 99 | message: e.message, 100 | }; 101 | } 102 | } 103 | 104 | async function getCalenderByPin(pin, date, age, process = true) { 105 | try { 106 | const res = await api.get( 107 | `${getCalenderByPinPath}pincode=${pin}&date=${date}` 108 | ); 109 | 110 | if (res.status === 200) { 111 | const result = res.data.centers; 112 | if (result !== undefined) 113 | return { 114 | status: true, 115 | result: process ? getDataFromResponse(result, age) : result, 116 | }; 117 | else throw new Error("Centers are undefined"); 118 | } else { 119 | if (res.status === 400) throw new Error(res.error); 120 | 121 | if (res.status === 401) throw new Error("Unauthenticated access!"); 122 | 123 | if (res.status === 500) throw new Error("Internal Server Error"); 124 | } 125 | } catch (e) { 126 | return { 127 | status: false, 128 | message: e.message, 129 | }; 130 | } 131 | } 132 | 133 | async function getCalenderByDistrict(id) { 134 | try { 135 | console.log("API CALL"); 136 | const today = DateTime.now().toFormat("dd-MM-yyyy"); 137 | const url = `${getCalenderByDistrictPath}district_id=${id}&date=${today}`; 138 | const res = await api.get(url); 139 | let result = res.data; 140 | 141 | if (IS_PROXY) result = result.data.centers; 142 | else result = result.centers; 143 | 144 | if (res.status === 200) { 145 | if (result !== undefined) { 146 | return { 147 | status: true, 148 | result: result, 149 | }; 150 | } else throw new Error("Centers are undefined"); 151 | } else { 152 | if (res.status === 400) throw new Error(res.error); 153 | 154 | if (res.status === 401) throw new Error("Unauthenticated access!"); 155 | 156 | if (res.status === 500) throw new Error("Internal Server Error"); 157 | } 158 | } catch (e) { 159 | console.log(e); 160 | return { 161 | status: false, 162 | message: e.message, 163 | }; 164 | } 165 | } 166 | 167 | export { 168 | getStates, 169 | getDistrictsByStateId, 170 | getCalenderByPin, 171 | getCalenderByDistrict, 172 | getDataFromResponse, 173 | }; 174 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | Cowin Assist 18 | 19 | 20 | 21 | 49 |
50 | 51 |
52 |
53 |

54 | Welcome
I'm 55 | Cowin Assist
57 |

58 |

59 | A discord bot to assist you in being up to date 60 |
with the vaccination centers in your district. 61 |

62 |
63 | Add to Server 64 |
65 | 66 |
67 | 72 |
73 | 74 |
75 | Get Vaccinated 76 |
77 |
78 | 79 | 80 |
81 |

About

82 | 83 |
84 |
85 | Get Vaccinated 86 |
87 |

88 | Cowin Assist is a chat-bot that lets the user track the 89 | availability of covid vaccination centres in India via 90 | the 91 | CoWin API. It has a bunch of commands through which the user can 94 | set/update their desired filters to receive up to date 95 | information regarding the vaccine centres. 96 |

97 |
98 |
99 |
100 | 101 | 102 |
103 |

Features & Commands

104 | 105 |
106 |
107 |

108 |

  • Add the discord bot to a server
  • 109 |
  • 110 | Register your district, pincode and age (just 111 | once, we'll save it for you) 112 |
  • 113 |
  • 114 | Check for the availability of a vaccine center 115 | using the data provided 116 |
  • 117 |
  • Navigate to COWIN site if slot available
  • 118 |
  • 119 | Subscribe for hourly update on open slots if 120 | available 121 |
  • 122 |

    123 |
    124 |
    125 |

    126 |

  • 127 | /check to manually 128 | check for available slots with the given 129 | parameters. 130 |
  • 131 |
  • 132 | /help to show the list 133 | of available commands. 134 |
  • 135 |
  • 136 | /notify-me to enable 137 | hourly notifications for the user if slot is 138 | avaiilable. 139 |
  • 140 |
  • 141 | /set to set the user's 142 | age and district in the db. 143 |
  • 144 |

    145 |
    146 |
    147 |
    148 |
    149 |
    150 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /src/public/css/styles.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap"); 2 | 3 | :root { 4 | --header-height: 3rem; 5 | --font-semi: 600; 6 | } 7 | 8 | :root { 9 | --first-color: #496bfc; 10 | --second-color: #0e2431; 11 | } 12 | 13 | :root { 14 | --body-font: "Poppins", sans-serif; 15 | --big-font-size: 2rem; 16 | --h2-font-size: 1.25rem; 17 | --normal-font-size: 0.938rem; 18 | } 19 | @media screen and (min-width: 768px) { 20 | :root { 21 | --big-font-size: 3.5rem; 22 | --h2-font-size: 2rem; 23 | --normal-font-size: 1rem; 24 | } 25 | } 26 | 27 | :root { 28 | --mb-1: 0.5rem; 29 | --mb-2: 1rem; 30 | --mb-3: 1.5rem; 31 | --mb-4: 2rem; 32 | --mb-5: 2.5rem; 33 | --mb-6: 3rem; 34 | } 35 | 36 | :root { 37 | --z-back: -10; 38 | --z-normal: 1; 39 | --z-tooltip: 10; 40 | --z-fixed: 100; 41 | } 42 | 43 | *, 44 | ::before, 45 | ::after { 46 | box-sizing: border-box; 47 | } 48 | html { 49 | scroll-behavior: smooth; 50 | } 51 | body { 52 | margin: var(--header-height) 0 0 0; 53 | font-family: var(--body-font); 54 | font-size: var(--normal-font-size); 55 | color: var(--second-color); 56 | } 57 | h1, 58 | h2, 59 | p { 60 | margin: 0; 61 | } 62 | ul { 63 | margin: 0; 64 | padding: 0; 65 | list-style: none; 66 | } 67 | a { 68 | text-decoration: none; 69 | color: var(--first-color); 70 | } 71 | img { 72 | max-width: 100%; 73 | height: auto; 74 | display: block; 75 | } 76 | 77 | /*---------CLASS CSS ---------*/ 78 | .section-title { 79 | position: relative; 80 | font-size: var(--h2-font-size); 81 | color: var(--first-color); 82 | margin-top: var(--mb-2); 83 | margin-bottom: var(--mb-4); 84 | text-align: center; 85 | } 86 | .section-title::after { 87 | position: absolute; 88 | content: ""; 89 | width: 64px; 90 | height: 0.18rem; 91 | left: 0; 92 | right: 0; 93 | margin: auto; 94 | top: 2rem; 95 | background-color: var(--first-color); 96 | } 97 | .section { 98 | padding-top: 3rem; 99 | padding-bottom: 2rem; 100 | } 101 | .code { 102 | background: var(--first-color); 103 | border-radius: var(--mb-2); 104 | padding: 0 1rem; 105 | color: #fff; 106 | } 107 | .bd-grid { 108 | max-width: 1024px; 109 | display: grid; 110 | grid-template-columns: 100%; 111 | grid-column-gap: 2rem; 112 | width: calc(100% -2rem); 113 | margin-left: var(--mb-2); 114 | margin-right: var(--mb-2); 115 | } 116 | .l-header { 117 | width: 100%; 118 | position: fixed; 119 | top: 0; 120 | left: 0; 121 | z-index: var(--z-fixed); 122 | background-color: #fff; 123 | box-shadow: 0 1px 4px rgba(146, 161, 176, 0.15); 124 | } 125 | 126 | .nav { 127 | height: var(--header-height); 128 | display: flex; 129 | justify-content: space-between; 130 | align-items: center; 131 | font-weight: var(--font-semi); 132 | } 133 | @media screen and (max-width: 768px) { 134 | .nav__menu { 135 | position: fixed; 136 | top: var(--header-height); 137 | right: -100%; 138 | width: 80%; 139 | height: 100%; 140 | padding: 2rem; 141 | background-color: var(--second-color); 142 | transition: 0.5s; 143 | } 144 | } 145 | .nav__item { 146 | margin-bottom: var(--mb-4); 147 | } 148 | .nav__link { 149 | position: relative; 150 | color: #fff; 151 | } 152 | .nav__link:hover { 153 | position: relative; 154 | } 155 | .nav__link:hover::after { 156 | position: absolute; 157 | content: ""; 158 | width: 100%; 159 | height: 0.18rem; 160 | left: 0; 161 | top: 2rem; 162 | background-color: var(--first-color); 163 | } 164 | .nav__logo { 165 | color: var(--second-color); 166 | } 167 | .nav__toggle { 168 | color: var(--second-color); 169 | font-size: 1.5rem; 170 | cursor: pointer; 171 | } 172 | 173 | .active::after { 174 | position: absolute; 175 | content: ""; 176 | width: 100%; 177 | height: 0.18rem; 178 | left: 0; 179 | top: 2rem; 180 | background-color: var(--first-color); 181 | } 182 | 183 | .show { 184 | right: 0; 185 | } 186 | 187 | .home { 188 | height: calc(100vh -3rem); 189 | row-gap: 1rem; 190 | } 191 | .home__data { 192 | align-self: center; 193 | } 194 | .home__title { 195 | font-size: var(--big-font-size); 196 | margin-bottom: var(--mb-5); 197 | } 198 | .home__title-color { 199 | color: var(--first-color); 200 | } 201 | .home__github { 202 | display: flex; 203 | flex-direction: column; 204 | } 205 | .home__github-icon { 206 | width: max-content; 207 | margin-bottom: var(--mb-2); 208 | font-size: 1.5rem; 209 | color: var(--second-color); 210 | } 211 | .home__github-icon:hover { 212 | color: var(--first-color); 213 | } 214 | .home__img { 215 | position: absolute; 216 | right: 0; 217 | bottom: 0; 218 | width: 295px; 219 | } 220 | 221 | .button { 222 | display: inline-block; 223 | background-color: var(--first-color); 224 | color: #fff; 225 | padding: 0.75rem 2.5rem; 226 | font-weight: var(--font-semi); 227 | border-radius: 0.5rem; 228 | } 229 | .button:hover { 230 | box-shadow: 0 10px 36px rgba(0, 0, 0, 0.15); 231 | } 232 | 233 | .about__container { 234 | row-gap: 2rem; 235 | text-align: center; 236 | } 237 | .about__subtitle { 238 | margin-bottom: var(--mb-2); 239 | } 240 | 241 | .features__container { 242 | row-gap: 2rem; 243 | text-align: center; 244 | } 245 | .features__subtitle { 246 | margin-bottom: var(--mb-2); 247 | } 248 | .features__img { 249 | justify-self: center; 250 | } 251 | .features__img img { 252 | width: 200px; 253 | border-radius: 0.5rem; 254 | } 255 | 256 | .footer { 257 | background-color: var(--second-color); 258 | color: #fff; 259 | text-align: center; 260 | font-weight: var(--font-semi); 261 | padding: 2rem 0; 262 | } 263 | .footer__title { 264 | font-size: 2rem; 265 | margin-bottom: var(--mb-4); 266 | } 267 | .footer__github { 268 | margin-bottom: var(--mb-4); 269 | } 270 | .footer__icon { 271 | font-size: 1.5rem; 272 | color: #fff; 273 | margin: 0 var(--mb-2); 274 | } 275 | 276 | @media screen and (min-width: 768px) { 277 | body { 278 | margin: 0; 279 | } 280 | .section { 281 | padding-top: 4rem; 282 | padding-bottom: 3rem; 283 | } 284 | .section-title { 285 | margin-bottom: var(--mb-6); 286 | } 287 | .section-title::after { 288 | width: 80px; 289 | top: 3rem; 290 | } 291 | 292 | .nav { 293 | height: calc(var(--header-height) + 1rem); 294 | } 295 | .nav__list { 296 | display: flex; 297 | padding-top: 0; 298 | } 299 | .nav__item { 300 | margin-left: var(--mb-6); 301 | margin-bottom: 0; 302 | } 303 | .nav__toggle { 304 | display: none; 305 | } 306 | .nav__link { 307 | color: var(--second-color); 308 | } 309 | 310 | .home { 311 | height: 100vh; 312 | } 313 | .home__data { 314 | align-self: flex-end; 315 | } 316 | .home__github { 317 | padding-top: 0; 318 | padding-bottom: 2.5rem; 319 | flex-direction: row; 320 | align-self: flex-end; 321 | } 322 | .home__github-icon { 323 | margin-bottom: 0; 324 | margin-right: var(--mb-4); 325 | } 326 | .home__img { 327 | width: 457px; 328 | bottom: 15%; 329 | } 330 | 331 | .features__container { 332 | grid-template-columns: repeat(2, 1fr); 333 | align-items: center; 334 | text-align: initial; 335 | } 336 | .features__img img { 337 | width: 300px; 338 | } 339 | } 340 | 341 | @media screen and (min-width: 1024px) { 342 | .bd-grid { 343 | margin-left: auto; 344 | margin-right: auto; 345 | } 346 | .home__img { 347 | right: 20%; 348 | } 349 | } 350 | --------------------------------------------------------------------------------